3.2、对象数组
对象数组与原型/基础类型的数组没有什么不同,除了元素的初始化之外。当你使用new[N]去分配N个对象,就把N个连续的块空间分配出去了,每一个块空间可以放一个单独的对象。对于对象数组,New[]对每一个对象自动调用0参数(也就是缺省)构造函数,而原型数组清爽型缺省是没有被初始化的元素。用这种方式,使用new[]来分配对象数组返回一个指向完全构造并且初始化了的对象的指针。
例如,考虑以下的类:
class Simple
{
public:Simple() { println("Simple constructor called!"); }˜Simple() { println("Simple destructor called!"); }
};
如果你分配了四个Simple对象的数组,Simple的构造函数就被调用了四次。
Simple* mySimpleArray { new Simple[4] };
下图展示了数组的内存图示。可以看到,与基本类型的数组没有什么区别。
3.3、删除数组
当你用new[](也就是new的数组版本)来分配内存时,也要用delete[](也就是delete的数组版本)来释放它。这个版本自动析构数组中的对象并且释放与其关联的内存。
Simple* mySimpleArray { new Simple[4] };
// Use mySimpleArray...
delete [] mySimpleArray;
mySimpleArray = nullptr;
如果你不使用数组版本的delete,你的程序运行可能就会很怪异。对于有些编译器,只有数组的第一个元素的析构函数会被调用,因为编译器只知道你在删除一个指向对象的指针,数组的其他元素就会变成孤儿对象。而对于其他的编译器,内存可能会出现崩溃,因为new与new[]用的内存分配模式完全不一样。
对于用new分配的要用delete,而对于用new[]分配的要用delete[]。
当然了,只有数组的元素为对象时才需要调用析构函数。如果你有一个指针数组,你仍然需要删除为每个对象单独分配的空间。如下代码所示:
const size_t size { 4 };
Simple** mySimplePtrArray { new Simple*[size] };
// Allocate an object for each pointer.
for (size_t i { 0 }; i < size; ++i) { mySimplePtrArray[i] = new Simple{};
}
// Use mySimplePtrArray...
// Delete each allocated object.
for (size_t i { 0 }; i < size; ++i) {delete mySimplePtrArray[i];mySimplePtrArray[i] = nullptr;
}
// Delete the array itself.
delete [] mySimplePtrArray;
mySimplePtrArray = nullptr;
在现代C++中,当有属主介入时,应该避免使用原始C风格的指针。不要把原始指针存储在C风格的数组中,应该把智能指针存储在现代标准构造函数中,比如std::vector。我们马上要讨论的智能指针会自动地在合适的时间将与其关联的内存释放掉。
3.4、多维数组
多维数组将索引值的记号扩展到多个索引。例如,一个跳跳棋游戏可能会使用一个二维的数组来代表一个3乘3的网格。下面的例子展示了在栈上声明的一个数组,用0进行了初始化,以及一些测试代码:
char board[3][3] {};
// Test code
board[0][0] = 'X'; // X puts marker in position (0,0).
board[2][1] = 'O'; // O puts marker in position (2,1).
你可能会想二维数组的第一个下标是X坐标还是Y坐标呢。事实是它并不重要,只要保持一致就行。一个4乘7的网格可以声明为char board[4][7]或者board[7][4]。对于大部分应用来说,第一个下标为x轴第二个下标为y轴可能更容易理解。
3.5、多维栈数组
在内存中,3乘3的栈的二维board数组看起来像下图。
因为内存不是二维的(地址是连续的),计算机标识二维数组也会像一维数组一样。不同之处在于数组的大小有及用什么方法来访问。
多维数组的大小是所有维度相乘,然后乘以数组中的单个元素的大小。在上图中,3乘3的游戏板就是3*3*1=9个字节,假设一个字符是一个字节。对于一个4乘4的字节游戏板来说,数组就是4*7*1=28个字节。
要访问多维数组中的一个值,计算机将每一个下标视为多维数组中的一个子数组。例如,3乘3的网格,board[0]实际上就是下图中高亮的子数组。
当你加上第二个下标,比如board[0][2],计算机就会通过子数组中的第二个下标查找到正确的元素进行访问,如下图所示:
这个技巧可以扩展到N维数组,虽然高于三维的数组难于概念化并且较少被用到。
3.6、多维自由内存空间上的数组
如果需要在运行时决定多维数组的维度,可以使用在自由内存空间上的数组。与一维动态分配的数组通过指针访问一样,多维动态分配的数组也可以通过指针访问。不同的地方在于在二维数组中,需要用一个指向指针的指针;在一个N维的数组中,需要N层的指针。一开始,好像正确的方式是声明并且分配一个动态分配的多维数组如下:
char** board { new char[i][j] }; // BUG! Doesn't compile
该代码编译不通过,因为多维自由内存空间上的数组与栈上数组不一样。其内存构成不是连续的。实际情况是,是在自由内存空间上为第一个下标维度用分配一个连续的数组开始。数组的每一个元素实际上是指向另一个保存了第二个下标维度的元素的数组的指针。下图展示了2乘2动态分配游戏板的构成。
不幸的是,编译器不会为你对子数组进行内存分配。你可以像一维自由内存空间上的数组那样分配第一维的数组。但是每一个子数组必须显示分配。下面的函数正确地为一个二维数组分配了内存:
char** allocateCharacterBoard(size_t xDimension, size_t yDimension)
{char** myArray { new char*[xDimension] }; // Allocate first dimensionfor (size_t i { 0 }; i < xDimension; ++i) {myArray[i] = new char[yDimension]; // Allocate ith subarray}return myArray;
}
同样的,当你想要对自由内存空间上的多维数组进行内存释放时,delete[]语法也不能为你清理子数组。释放数组的代码与分配的代码相对应,如下函数:
void releaseCharacterBoard(char**& myArray, size_t xDimension)
{for (size_t i { 0 }; i < xDimension; ++i) {delete [] myArray[i]; // Delete ith subarraymyArray[i] = nullptr;}delete [] myArray; // Delete first dimensionmyArray = nullptr;
}
上面的为多维数组分配内存的例子并不是一个非常高效的解决方案。它首先为第一维分配了内存,接着为每一个子数组分配了内存。结果就是内存块在内存中散落各处,对于这样的数据结构上的算法来讲有很大的性能影响。如果算法在连续的内存上会跑得更快。好的解决方案是分配一个单独的内存块,足够保存xDimension * yDimension个元素,用像x*yDimension + y的公式来访问(x,y)位置的元素。
既然你已经知道了数组工作的细节,推荐你尽可能避免这些旧的C风格的数组,因为它们不能提供内存安全。在这儿解释这么多,是因为你会在遗留的代码中会碰到。在新的代码中,应该使用C++标准库函数,比如std::array与vector。例如,使用vector<T>来用一维动态数组。对于二维动态数组 ,可以使用vector<vector<T>>,再多维的数组也类似。当然了,直接使用像vector<vector<T>>这样的数据结构也比较烦,特别是要构造它们,也会有前面讨论的同样的内存碎片问题。所以啊,如果在你的应用中确实需要N维动态数组,考虑写一个helper类,提供一个易于使用的接口。例如,对于二维数据,有同样长的行,你可以考虑写(当然也可以重用)一个Matrix<T>或者Table<T>类模板,把内存分配/释放与用户访问元素算法隐藏下来,我们以后会专门讨论写类模板的细节。
不要使用C风格的数组,要使用C++标准库函数,比如std::array,vector等等。