2.2、我的好朋友malloc怎么样了?
如果你是一个C程序员,对啦,我就是,你可能会想,malloc()函数怎么样了。在C语言中,malloc()用于分配一定数量的内存字节。总的来讲,使用malloc()简单直接。在C++中malloc()函数依然存在,但你要尽量避免使用它。New比malloc()有优势的地方就是new不仅仅分配内存,它还构造对象。
例如,考虑以下两行代码,使用了一个叫做Foo的假想类:
Foo* myFoo { (Foo*)malloc(sizeof(Foo)) };
Foo* myOtherFoo { new Foo{} };
执行完这些代码后,myFoo与myOtherFoo指向了能够放置Foo对象的自由内存空间。使用这两个指针都能访问到Foo的成员变量与成员函数。不同的地方在于被 myFoo指向的Foo对象不是一个合适的对象,因为它的构造函数没有被调用到。Malloc()函数只是设定了一定数量的内存。它不知道也不关心对象。而恰恰相反,对new的调用分配了合适的内存并且调用了合适的构造函数来构造这个对象。
同样的不同也存在于free()函数与delete操作符之间。对于free(),对象的析构函数没有被调用。而对于delete,析构函数被调用且对象被合适地清理。
在C++中要避免使用malloc()与free()。
2.3、当内存分配失败
如果不是全部的话,也是大部分程序员在写代码的时候,问题假设New总是成功的。道理是如果new失败的话,就意味着内存很低,生命非常非常糟糕。也经常是一种难以理解的状态吧,因为不知道你的程序怎么样了,在这种情况下还能做什么呢?
缺省情况下,当new失败时要抛出异常,例如,对于申请的内存不够。如果异常没有被捕获,程序就会中止。在许多程序中,这种行为是可以被接受的。我们以后还会详细讲解异常,提供从内存不足的情况下优雅恢复的可能的方法。
还有一种版本的new,它不抛出异常。当内存分配失败时它返回一个nullptr,与C中malloc()的行为类似。使用这种版本的new的语法如下:
int* ptr { new(nothrow) int };
这种语法有一点奇怪:你把nothrow当做new的参数来用,实际上还就是。
当然了,与抛出异常的版本一样,你仍然会有同样的问题—当结果是nullptr时怎么做?编译器并不要求你去检测这个结果,所以nothrow版本的new比抛出异常的版本的更可能带来其他的问题。基于这个原因,建议使用标准版本的new。如果从内存不足中恢复对你的程序非常重要,我们以后讨论的技巧会给你需要的工具。
3、数组
数组就是同类型的多个变量放到一个带有索引的单个变量中。使用数组对于初级程序员来说是很自然的,因为对于以数字标识的值来说很容易理解。内存内的数组也是这种思想模型。
3.1、原型数组
当你的程序为一个数组分配内存时,它分配了一块连续的内存片,每一小块可以放置数组的一个元素。例如,一个本地的int数组可以在栈上声明如下:
int myArray[5];
每个原型数组中的元素都没有初始化;也就是说,在内存中可以是任何值。下图显示了数组生成时的内存状态。当在栈上生成数组时,在编译时一定要知道其值的大小。
有些编译器允许在栈上有变长数组。这不是C++的标准属性,所以建议使用时要仔细。
当在栈上生成一个数组时,可以用一个初始化列表来提供初始值:
int myArray[5] { 1, 2, 3, 4, 5 };
如果初始化列表中包含的元素比数组的少,余下的元素都为0,例如:
int myArray[5] { 1, 2 }; // 1, 2, 0, 0, 0
把所有元素都初始化为0,简单如下:
int myArray[5] { }; // 0, 0, 0, 0, 0
当使用初始化列表时,编译器可以自动推断出元素的数量,就不需要显示地给出数组的大小了:
int myArray[] { 1, 2, 3, 4, 5 };
在自由内存空间上声明数组也一样,除了使用指针指向数组的位置之外。以下代码为5个未初始化的int分配了内存,保存了一个指向叫做myArrayPtr的变量的指针:
int* myArrayPtr { new int[5] };
下图展示了与栈上数组类似的自由内存空间数组,只是位置不同而已。myArrayPtr变量指向数组的第0个元素。
与new操作符一样,new[]接受nothrow变量返回nullptr,而不是抛出异常,当分配失败时:
int* myArrayPtr { new(nothrow) int[5] };
在自由内存空间上动态创建的数组也可以用初始化列表来初始化:
int* myArrayPtr { new int[] { 1, 2, 3, 4, 5 } };
对new[]的每一次调用都应该对应地去调用delete[]来清理内存。注意在delete[]后面的空的花括号!
delete [] myArrayPtr;
myArrayPtr = nullptr;
把数组放到自由内存空间的优势是可以在运行时定义大小。例如,以下代码片断接收了从一个假想叫做askUserForNumberOfDocuments()的函数的一定数量的文件,使用这个结果生成Document对象的数组。
Document* createDocumentArray()
{
size_t numberOfDocuments { askUserForNumberOfDocuments() };
Document* documents { new Document[numberOfDocuments] };
return documents;
}
记住对每一个new[]的调用都应该对应地调用delete[],所以在这个例子中,createDocumentArray()的调用者很重要的一点就是要使用delete[]来清理返回的内存。另一个问题就是C风格的数组是不知道它们的大小的;这样的话,createDocumentArray()的调用者也不知道返回数组中到底有多少元素!
在以前的函数中,documents是一个动态分配的数组。不要将其与动态数组相混淆。数组本身并不是动态的,因为一旦分配了其大小就不会发生改变。动态内存让你给出运行时的分配空间的大小,但是它不会自动调整大小来放置数据。
数据结构确实在动态调整其大小,也确实知道其真实的大小,例如,标准库构造函数。应该使用这样的构造函数,而不应该使用C风格的数组,因为它们在使用上更安全。
在C++中有一个函数叫做realloc(),是从C语言中继承来的。别用它,在C中,realloc()用于通过分配新的内存空间来有效改变数组的大小,把旧的数据拷贝到新的位置,删除原来的内存块。在C++中这个方法是非常危险的,因为用户定义的对象对于位字节拷贝不灵光。
永远不要在C++中使用realloc()!它已不再是你的朋友。