19-4 指针的指针与动态多维数组(可选)
本节课为选修内容,面向希望深入学习C++的高级读者。后续课程不会基于本节内容展开。
指针的指针正如其名:它是一个存储另一个指针地址的指针。
指针的指针
普通整型指针使用单个星号声明:
int* ptr; // pointer to an int, one asterisk
指向整型的指针的指针使用两个星号声明
int** ptrptr; // pointer to a pointer to an int, two asterisks
指针的指针与普通指针的工作原理相同——你可以通过解引用操作获取其指向的值。由于该值本身也是指针,因此可以再次解引用以获取底层值。这些解引用操作可以连续进行:
int value { 5 };
int* ptr { &value };
std::cout << *ptr << '\n'; // Dereference pointer to int to get int value
int** ptrptr { &ptr };
std::cout << **ptrptr << '\n'; // dereference to get pointer to int, dereference again to get int value
上述程序输出:

请注意,您不能将指针直接指向另一个指针的值:
int value { 5 };
int** ptrptr { &&value }; // not valid

这是因为运算符的地址(operator&)需要左值,而&value是右值。
然而,指针的指针可以被设置为空:
int** ptrptr { nullptr };
指针数组
指针的指针有若干用途。最常见的用途是动态分配一个指针数组:
int** array { new int*[10] }; // allocate an array of 10 int pointers
这与标准的动态分配数组完全相同,区别在于数组元素的类型是“整数指针”而非整数。
二维动态分配数组
指针的另一常见用途是实现动态分配的多维数组(参见17.12节——C风格多维数组的回顾)。
与二维固定数组不同,后者可轻松声明如下:
int array[10][5];
动态分配二维数组稍显棘手。你可能会尝试如下写法:
int** array { new int[10][5] }; // won’t work!
但这行不通。
这里有两种可能的解决方案。如果最右侧的数组维度是 constexpr,你可以这样做:
int x { 7 }; // non-constant
int (*array)[5] { new int[x][5] }; // rightmost dimension must be constexpr
括号是必需的,这样编译器才能知道我们希望 array 成为指向 5 个 int 数组的指针(在此情况下,这是 7 行多维数组的第一行)。若不加括号,编译器会将其解释为 int* array[5],即 5 个 int* 的数组。
这里正是使用自动类型推导的好时机:
int x { 7 }; // non-constant
auto array { new int[x][5] }; // so much simpler!
遗憾的是,当数组最右侧维度不是编译时常量时,这个相对简单的解决方案就行不通了。这种情况下,我们不得不采用更复杂的方法:首先分配一个指针数组(如上所述),然后遍历该指针数组,为每个数组元素分配动态数组。我们的动态二维数组,本质上是由动态一维数组组成的动态一维数组!
int** array { new int*[10] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
array[count] = new int[5]; // these are our columns
然后我们可以像往常一样访问数组:
array[9][4] = 3; // This is the same as (array[9])[4] = 3;
采用这种方法,由于每个数组列都是独立动态分配的,因此可以创建非矩形的动态分配二维数组。例如,我们可以创建一个三角形数组:
int** array { new int*[10] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
array[count] = new int[count+1]; // these are our columns
在上例中,请注意 array[0] 是长度为 1 的数组,array[1] 是长度为 2 的数组,依此类推……
使用此方法释放动态分配的二维数组时,同样需要使用循环:
for (int count { 0 }; count < 10; ++count)
delete[] array[count];
delete[] array; // this needs to be done last
请注意,我们删除数组的顺序与创建时相反(先删除元素,再删除数组本身)。如果先删除数组再删除数组列,则必须访问已释放的内存来删除数组列,这将导致未定义行为。
由于二维数组的分配与释放操作复杂且易出错,通常更简便的做法是将二维数组(尺寸为x×y)展平为一维数组(尺寸为x*y):
// Instead of this:
int** array { new int*[10] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
array[count] = new int[5]; // these are our columns
// Do this
int *array { new int[50] }; // a 10x5 array flattened into a single array
通过简单的数学运算,可以将矩形二维数组的行索引和列索引转换为一维数组的单一索引:
int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
return (row * numberOfColumnsInArray) + col;
}
// set array[9,4] to 3 using our flattened array
array[getSingleIndex(9, 4, 5)] = 3;
按地址传递指针
正如我们可以使用指针参数来改变底层参数的实际值,我们也可以传递一个指向函数指针的指针,并利用该指针来改变其指向的指针值(是否感到困惑了?)。
然而,若需函数修改指针参数所指向的内容,通常更推荐使用指针引用实现。相关内容详见第12.11节——按地址传递(第二部分)。
指向指针的指针的.....的指针
也可以声明一个指向指针的指针的指针:
int*** ptrx3;
这可用于动态分配三维数组。但此操作需要嵌套循环,且实现正确性极其复杂。
甚至可以声明指向指针的指针的指针的指针:
int**** ptrx4;
或者更高,如果你愿意的话。
然而实际上这些很少被使用,因为很少需要如此多的间接层级。
结论
我们建议除非别无选择,否则应避免使用指针的指针,因为它们使用复杂且存在潜在风险。使用普通指针解引用空指针或悬空指针已足够容易——而指针的指针则更易引发错误,因为必须进行双重解引用才能获取底层值!

浙公网安备 33010602011771号