当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
1. 对象创建
当创建一个c++对象时会发生两件事:
- 为对象分配内存
- 调用构造函数来初始化那块内存
第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。
2. C动态分配内存方法
为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person{ public: Person(){ mAge = 20; pName = (char*)malloc(strlen("john")+1); strcpy(pName, "john"); } void Init(){ mAge = 20; pName = (char*)malloc(strlen("john")+1); strcpy(pName, "john"); } void Clean(){ if (pName != NULL){ free(pName); } } public: int mAge; char* pName; }; int main(){ //分配内存 Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } //调用初始化函数 person->Init(); //清理对象 person->Clean(); //释放person对象 free(person); return EXIT_SUCCESS; }
问题:
1)程序员必须确定对象的长度。
2)malloc返回一个void*指针,c++不允许将void*赋值给其他任何指针,必须强转。
3)malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
4)用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。
c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和 delete.
3. new operator
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } person->Init();
new操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
4. delete operator
new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。
如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Person{ public: Person(){ cout << "无参构造函数!" << endl; pName = (char*)malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ cout << "有参构造函数!" << endl; pName = (char*)malloc(strlen(name) + 1); strcpy(pName, name); mAge = age; } void ShowPerson(){ cout << "Name:" << pName << " Age:" << mAge << endl; } ~Person(){ cout << "析构函数!" << endl; if (pName != NULL){ delete pName; pName = NULL; } } public: char* pName; int mAge; }; void test(){ Person* person1 = new Person; Person* person2 = new Person("John",33); person1->ShowPerson(); person2->ShowPerson(); delete person1; delete person2; }
5. 用于数组的new和delete
使用new和delete在堆上创建数组非常容易。
//创建字符数组 char* pStr = new char[100]; //创建整型数组 int* pArr1 = new int[100]; //创建整型数组并初始化 int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //释放数组内存 delete[] pStr; delete[] pArr1; delete[] pArr2;
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
class Person{ public: Person(){ pName = (char*)malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName, name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; }; void test(){ //栈聚合初始化 Person person[] = { Person("john", 20), Person("Smith", 22) }; cout << person[1].pName << endl; //创建堆上对象数组必须提供构造函数 Person* workers = new Person[20]; }
6. delete void*可能会出错
如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。
class Person{ public: Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName,name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; }; void test(){ void* person = new Person("john",20); delete person; }
7. 使用new和delete采用相同形式
Person* person = new Person[10]; delete person;
以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。
我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:一、析构函数;二、释放内存。
那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:
本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。
当我们使用一个delete
的时候,我们必须让delete
知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[]
,那么delete
就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论:
如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete表达式中使用[].