动静态内存分配
静态内存分配:编译器在处理程序源代码时(即编译时)分配。
动态内存分配:程序执行时调用运行时刻库函数来分配。
静态与动态内存分配的两个主要区别:
- 静态对象是有名字的变量,我们直接对其进行操作。而动态对象是没有名字的变量,我们通过指针间接地对它进行操作。
- 静态对象的分配与释放由编译器自动处理。程序员需要理解这一点,但不需要做任何事情。相反,动态对象地分配与释放,必须由程序员显示地管理,相对来说比较容易出错 。
new/delete
在C语言中进行内存的动态分配与释放,我们使用malloc()和free()函数。C++中不再使用C语言中的malloc()和free()函数进行内存的动态分配与释放。因为,malloc()函数在运行时从自由内存中分配存储单元。在C++中创建对象时会发生两件事情:(1)为对象分配内存;(2)调用构造函数来初始化那个内存。而构造函数不允许向它传递内存地址来进行初始化。
在C++中使用new和delete来进行内存的动态分配与释放。new会触发类对象的构造函数,delete会触发类对象的析构函数。
动态申请内存操作符 new
语法格式:类型名T *指针变量名P = new 类型名T(初值列表);
功能:在程序执行期间,申请用于存放T类型对象的连续的、未命名的内存空间,并依初值列表赋以初值。
结果值:成功:T类型的指针,指向新分配的内存。失败:0(NULL)
new运算符是一个一元运算符。它隐含地生成一个函数来调用函数operator new(),从这个函数返回被分配内存对象的指针。
在new表达式中,可以在类型名后使用空圆括号:new 数据类型T();功能:将初始化为数据类型T的默认值。 若new 数据类型T,没有在T后加(),则表明没有初始化数据类型T的默认值
new 数据类型T;与new 数据类型T();的区别
eg:int *ptrInt1 = new int; // Value is unknown
int *ptrInt1 = new int(); // Value is zero
说明:第一种情况下,在空闲存储区中创建了一个int类型对象,但其值未知。在第二种情况下,在空闲存储区中创建了一个int类型对象并初始化为int在全局作用域中的值(默认值)。而所有的全局基本类型在默认情况下的 初始值为0,故第二种情况在创建了对象后将其值初始化为0。
释放内存操作符delete
语法格式:delete 指针变量名P;delete [ ] 指针变量名P;
功能:释放指针P所指向的内存。P必须是new操作的返回值。delete表达式首先调用析构函数,然后释放内存(经常调用free( ))。若正在删除的指针是0,则将不发生任何事情,是安全的。故开发人员经常在删除指针后立即把指针赋值为0以免对它删除两次(对一个对象删除两次可能会产生某些问题)。
eg:delete pi;注意这时释放了pi所指的目标的内存空间,也就是撤销了该目标,称动态内存释放(dynamic memory
deallocation),但指针pi本身并没有撤销,该指针所占内存空间并未释放。 删除一个指针p(delete p;)实际意思是删除了p所指的目标(变量或对象等),释放了它所占的自由存储区空间,而不是删除p本身,p成了空悬指针。空悬指针是程序错误的一个根源)。建议这时将p置空(NULL)。
如何释放单个对象的空闲空间
语法格式:delete 指针变量名P;指针变量名P = NULL;
功能:释放指针P所指向的内存。P必须是new操作的返回值。
删除基于0的指针(空指针)总是一种安全的操作。
如何为对象数组分配空闲空间
语法格式:数据类型T *指针变量名P = new 数据类型T[元素个数];
功能:在空闲空间分配对象数组,指针P指向数组的第一个元素。如果在方括号中的表达式的值为0,则就会分配没有元素的数组,new表达式返回的指针是非0值,并且与其他指向任何对象的指针都不同。如果这个值在运行期间被确定是无效的,则结果不可预知。
保持平衡
涉及到new以及delete的主要规则相当简单,使用delete来平衡每一个new。如果在new语句中使用了方括号(即分配了对象数组),必须在delete后使用方括号。无论何时使用new,一定要确保使用正确的delete格式。
浅拷贝与深拷贝
浅拷贝:实现对象间数据元素的一一对应复制。
深拷贝:当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制。
深拷贝实例
// test.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> using namespace std; // string类,深度复制实例 class student { char *pName; //为了演示深复制,不用string类 public: student(); //默认构造函数 student(char *pname); //带参数构造函数 student(student &s); //复制(拷贝)构造函数 ~student(); //析构函数 student & operator=(student &s); //复制赋值操作符 }; student::student() { cout<<"Constructor"; pName=NULL; cout<<"默认"<<endl; } student::student(char *pname) { cout<<"Constructor"; if(pName=new char[strlen(pname)+1]) strcpy(pName,pname); cout<<pName<<endl; } student::~student() { cout<<"Destructor"<<pName<<endl; // if(pName) // pName[0]='\0';//===>>*pName='\0';//间接的把不是指针的变量置为空('')。当然,此处没必要也不需要判断来间接设置其指向的变量为空 delete[] pName;//释放字符串 } student::student(student &s) { cout<<"Copy Constructor"; if(s.pName) { if(pName=new char[strlen(s.pName)+1]) strcpy(pName,s.pName); } else pName=NULL; cout<<pName<<endl; } student & student::operator=(student &s) { cout<<"Copy Assign operator"; delete[] pName;//如原来已分配,应先撤销,再重分配。先删除自己分配的空间 if(s.pName)//否则会发生内存泄漏 { if(pName=new char[strlen(s.pName)+1]) strcpy(pName,s.pName);} else pName=NULL; cout<<pName<<endl; return *this;//this是一个指针,它的作用域在类的内部。而函数返回的是对象,所以 return *this; } int main(void) { student s1("范英明"),s2("沈俊"),s3; student s4=s1; s3=s2; return 0;//加断点调试,你回发现堆变量删除释放(由系统自动释放)的顺序与其创建的顺序相同。 } void test() { int val=66; int *pval = &val; int **ppval = &pval; cout<<"val="<<val<<'\n'<<"**ppval="<<**ppval<<'\n';//输出: 66 66 **ppval=18; cout<<"val="<<val<<'\n'<<"**ppval="<<**ppval<<endl; //输出: 18 18 }
对象成员的另一种访问方式
指 针,声明形式:类名 *对象指针名;eg:
Point A(5, 10);
Piont *ptr = NULL;
ptr = &A;
通过指针访问对象成员,对象指针名->成员名,ptr->getx() 相当于 (*ptr).getx();
指针与引用的优劣
- 指针是个变量,可以把它再赋值成其它的地址(水性杨花) 。然而建立引用时必须进行初始化并且决不会再指向其他不同的变量(从一而终) 。
- 有空指针,无空引用。
- 由于指针也是变量,所以可以有指针变量的引用。eg:int * a = NULL; int * & p = a;// (表示 int * 的引用p 初始化为 a) int b = 5; p = &b ; // p 是a 的一个别名,是一个指针
- 引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。eg:int a; int & r = a; // (表示 int * 的引用p 初始化为 a) int & * p = & r ; // 错, 企图定义一个引用的指针由于指针也是变量,所以可以有指针变量的引用
- 使用引用和指针都可以使函数返回多个值。
- 在参数传递方面:引用比指针直观,可读性强,尽量使用引用代替指针,以下两条除外。a,如果必须传递数组,使用指针加元素个数。b,如果参数可能无效(null),使用指针,并判断(p == null)。如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
- 返回值方面:如果返回动态分配的对象或内存,必须使用指针,引用可能引起内存泄漏。 operator = 的返回值必须是 Cxxx &
- 可以使用指针或引用的时候,除了传递参数和返回值,其它地方都应该是指针更好一些。
- 必须使用引用的地方:operator = 的返回值必须是 Cxxx &;运算符重载的时候:operator + (const Cxxx& lhs, const Cxxx & rhs); 不能用指针。如果想重载[ ]并且想让他和数组用法完全一致,那么返回的类型必须是引用类型,此时对于类或结构体应注意对象是否需要深度拷贝。
实例
#include "stdafx.h" #include <iostream> using namespace std; void GetMemory1(char* p,int num) { p=new char[num]; } //如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针” void GetMemory2(char** p,int num) { *p=new char[num]; } //或者改用“指向指针的引用” void GetMemory3(char* &p,int num) { p=new char[num]; } //或者改用函数返回值类传递动态内存 char* GetMemory4(int num) { char *p=new char[num]; return p;//必须放回的是堆内存存储区,静态全局存储区,或者返回只读的常量存储区 } char* GetMemory5() { char p[]="hello world";//数组p是栈内存,没有用new,"hello world"给数组p赋值,数组p是被分配连续的内存空间 return p;//return语句返回不能是指向"栈内存"的指针,因为该内存在函数结束时自动消亡 } char* GetMemory6() { char* p="hello world";//"hello world"是字符串常量,位于常量存储区,它在程序生命期内恒定不变。无论什么时候调用GetMemory6,它返回的始终是同一个“只读”的内存块。 return p; } void test() { // char* str=NULL; // GetMemory1(str,100);//str仍然为NULL // strcpy(str,"hello");//运行有误 // cout<<str<<endl; // delete [] str; // str=NULL; // char* str=NULL; // GetMemory2(&str,100); // strcpy(str,"hello"); // cout<<str<<endl; // delete [] str; // str=NULL; // char* str=NULL; // GetMemory3(str,100); // cout<<str<<endl; // strcpy(str,"hello"); // cout<<str<<endl; // delete [] str; // str=NULL; // char* str=NULL; // str=GetMemory4(100); // strcpy(str,"hello"); // cout<<str<<endl; // delete [] str; // str=NULL; // char* str=NULL; // str=GetMemory5();//GetMemory5()返回后str不再是NULL指针,但str的内容不是"hello world",而是垃圾信息,可以是乱码 // strcpy(str,"hello"); // cout<<str<<endl; char* str=NULL; str=GetMemory6();//GetMemory6()返回的是一个"只读"的内存块 cout<<str<<endl; //输出"hello world" strcpy(str,"hello"); cout<<str<<endl; } int _tmain(int argc, _TCHAR* argv[]) { test(); return 0; }
那些总结
- 指针p指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被修改的。
- return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。