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

上述程序输出:

image

请注意,您不能将指针直接指向另一个指针的值:

int value { 5 };
int** ptrptr { &&value }; // not valid

image

这是因为运算符的地址(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;

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

然而实际上这些很少被使用,因为很少需要如此多的间接层级。

结论

我们建议除非别无选择,否则应避免使用指针的指针,因为它们使用复杂且存在潜在风险。使用普通指针解引用空指针或悬空指针已足够容易——而指针的指针则更易引发错误,因为必须进行双重解引用才能获取底层值!

posted @ 2026-01-17 18:17  游翔  阅读(3)  评论(0)    收藏  举报