章节目录

指向指针和动态多维数组的指针

本节阅读量:

本课是可选内容,适合希望进一步了解C++的高级读者。后续课程不会以本课内容为基础。

指向指针的指针正是您所期望的:一个保存另一个指针地址的指针。


指向指针的指针

使用单个星号声明int的普通指针:

1
int* ptr; // 一个星号,指向int的指针

指向“指向int的指针”的指针使用两个星号声明:

1
int** ptrptr; // 两个星号,指向“指向int的指针”的指针

指向指针的指针像普通指针一样工作:您可以解引用它来获取所指向的值。由于该值本身也是指针,因此可以再次解引用它来获得底层值。这些解引用可以连续进行:

1
2
3
4
5
6
7
int value { 5 };

int* ptr { &value };
std::cout << *ptr << '\n'; // 解引用,获取int值

int** ptrptr { &ptr };
std::cout << **ptrptr << '\n'; // 解引用两次,获取int值

上述程序打印:

1
2
5
5

请注意,不能将指向指针的指针直接设置为原始对象的两次连续取地址操作的结果:

1
2
int value { 5 };
int** ptrptr { &&value }; // 无效

这是因为运算符(operator&)的参数需要左值,但&value的结果是右值。&value已经是一个地址值,而不是一个可以继续取地址的对象。

然而,指向指针的指针可以设置为空:

1
int** ptrptr { nullptr };

指针数组

指向指针的指针有一些用途。最常见的用法是动态分配指针数组:

1
int** array { new int*[10] }; // 分配包含10个int指针的数组

这和标准的动态分配数组一样工作,只是数组元素的类型是“指向int的指针”,而不是整数。


动态分配二维数组

指向指针的指针另一个常见用法,是动态分配多维数组。

固定的二维数组可以很容易地声明为:

1
int array[10][5];

而动态分配二维数组更具挑战性。您可能会尝试这样的操作:

1
int** array { new int[10][5] }; // 无法工作!

但这段代码无法工作。

这里有两种可能的解决方案。如果最右边的数组维度是constexpr,则可以这样做:

1
2
int x { 7 }; // non-constant
int (*array)[5] { new int[x][5] }; // 最右边的数组维度必须是constexpr

此处需要括号,以确保正确的优先级。这也是一个使用自动类型推导的好地方:

1
2
int x { 7 }; // non-constant
auto array { new int[x][5] }; // 如此简单!

不幸的是,如果最右边的数组维度不是编译时常量,这个相对简单的解决方案就不起作用。在这种情况下,做法必须更复杂一些。首先,我们分配一个指针数组(如上所述)。然后,遍历这个指针数组,并为每个数组元素分配一个动态数组。也就是说,我们的动态二维数组是由动态一维数组组成的动态一维数组!

1
2
3
int** array { new int*[10] }; // 动态分配包含10个int指针的一维数组,作为行
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[5]; // 这里是分配的列

然后,我们可以像往常一样访问数组:

1
array[9][4] = 3; // 等价于 (array[9])[4] = 3;

使用这种方法时,因为每一行都是独立动态分配的,所以可以生成动态分配的非矩形二维数组。例如,我们可以制作三角形数组:

1
2
3
int** array { new int*[10] }; // 动态分配包含10个int指针的一维数组,作为行
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[count+1]; // 这里是列

在上面的示例中,请注意,数组[0]是长度为1的数组,数组[1]是长度为2的数组,等等…

使用此方法删除动态分配的二维数组也需要循环:

1
2
3
for (int count { 0 }; count < 10; ++count)
    delete[] array[count];
delete[] array; // 这里需要最后清理

请注意,我们以创建数组的相反顺序删除数组(先删除元素,然后删除数组本身)。如果在删除各行之前先删除数组,则必须访问已释放的内存才能删除这些行。这将导致未定义的行为。

由于分配和释放二维数组比较复杂,并且很容易出错,因此通常更简单的做法是将二维数组(维度为x和y)“展平”为大小为x*y的一维数组:

1
2
3
4
5
6
7
// 不这么做:
int** array { new int*[10] }; // 动态分配包含10个int指针的一维数组,作为行
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[5]; // 这里是列

// 而是
int *array { new int[50] }; // 分配 10x5 的一维数组

然后,可以使用简单的数学将矩形二维数组的行和列索引转换为一维数组的单个索引:

1
2
3
4
5
6
7
int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
     return (row * numberOfColumnsInArray) + col;
}

// 将 array[9,4] 设置为 3
array[getSingleIndex(9, 4, 5)] = 3;

按地址传递指针

就像我们可以使用指针参数来更改传入参数的实际值一样,也可以传递指向指针的指针,并使用它来更改其所指向指针的值(确实有些令人困惑)。

然而,如果我们希望函数能够修改指针参数本身,通常更好的做法是改用对指针的引用。


指向指针的指针的指针

还可以声明指向指针的指针的指针:

1
int*** ptrx3;

这可以用于动态分配三维数组。然而,这样做需要在循环中包含一个循环,并且要获得正确的结果是极其复杂的。

甚至可以声明指向指针的指针的指针的指针:

1
int**** ptrx4;

或者更多,如果你愿意的话。

然而,在实际编程中,这些用处并不大,因为您通常不需要这么多层级的间接访问。


结论

我们建议避免使用指向指针的指针,除非确实没有其他可用选项,因为它们使用起来很复杂,并且可能很危险。使用普通指针时,解引用空指针或悬空指针已经很容易发生;使用指向指针的指针时,这类问题更容易出现,因为您必须执行双重解引用才能获得底层值!


19.2 再谈析构函数

上一节

19.4 void指针

下一节