在C和C++中,作为函数参数时,int **a和int a[][]有什么确切的区别?

4
在使用矩阵编写程序时,我遇到了这个问题。我使用 int** m 声明了我的矩阵,因为我需要动态分配,并在函数中使用了 int a[][]。我不记得有任何问题。但当我使用一个简单的 m[6][6] 矩阵和 f(int**m, int** m2, rest params) 时,我遇到了问题。
它编译通过了,但在运行程序时(使用 GCC 编译器),它就崩溃了。我尝试通过添加 printf() 进行调试,但它在一个毫无意义的 if() 块崩溃了。将第一个函数参数从 int a[][] 修改为 int* a[6] 后,程序可以运行了;稍后修改第二个参数,我的程序第一次尝试就成功了。通过更仔细地调试,我发现在 int m[i][j] 中保存的内容与在 if 中检查的内容不同,是垃圾值而非我所放入的内容,我只是放置了一个 1 或者 0 来标志某些东西。
多年以来,除非 GCC 在做类似于这样的事情时产生编译器错误,否则我只是按照脑海中最先出现的方法编写代码。
使用 int**int [][] 来声明变量/获取函数参数,有哪些逻辑?我所使用的大多数预定义函数在函数头中使用了 int**
我知道,int[][] 并不等同于 int**,而是 int* [],但我错过了什么?int[][] 是一个多维数组,表示数组的数组,所有三种写法似乎都一样。对于 int[][],几乎总是要求仅让第一个参数为空,例如,对于 int array[][][][],我需要在函数参数中放置 int array a[][n1][n2][n3],对吗?它需要知道多维数组的维度,除了第一个维度之外,因为在声明函数参数时可以使用 int*int[] 而不会有问题。

我使用 int** 声明了我的矩阵,但是 int ** 并不会创建一个二维数组。请参考这篇文章:正确分配多维数组 - Andrew Henle
1
多年的痛苦让我意识到在C++中实际上从不需要使用int **int [][],这也是C++开始变得有趣的时刻。而且我很高兴我不必使用C ;) - 463035818_is_not_a_number
6个回答

10

在C和C++中,int **aint a[][]作为函数参数的确切区别是什么?

int *a是一个指向int的指针。

int **a是一个指向指向int的指针的指针。

int a[]在所有其他情况下都将是未指定数量的int数组,但作为函数参数声明符,它被调整为指向int的指针,即在这种情况下,它与写作int *a相同。

int a[][]将是未指定大小的数组的未指定数量的数组,但这样的类型是不合法的,因为数组元素不能是未指定大小的数组。

int *a[]在所有其他情况下将是未指定数量的指向int的指针的数组,但作为函数参数声明符,它被调整为指向指向int的指针的指针,即在这种情况下,它与写作int **a相同。

int (*a)[N]是指向N个int的数组的指针。

int a[][N]将在所有其他情况下是未指定数量的N个int数组的数组,但作为函数参数声明符,它被调整为指向N个int数组的指针,即在这种情况下,它与写作int (*a)[N]相同。


一些例子:

void fun_1D(int*);        // argument is pointer to int
void fun_1D(int[]);       // same as above
void fun_1D(int[10]);     // same as above; note that 10 is ignored

int arr_1D[20];           // array of int
fun_1D(arr_1D);           // implicit conversion
fun_1D(&arr_1D[0]);       // same as above

void fun_2D(int (*)[20]); // note that 20 is not ignored
void fun_2D(int[][20]);   // same as above
void fun_2D(int[10][20]); // same as above; note that 10 is ignored

int arr_2D[20][20];       // array of array of int
fun_2D(arr_2D);           // implicit conversion
fun_2D(&arr_2D[0]);       // same as above

fun_1D(arr_2D[i]);        // implicit conversion
fun_1D(&arr_2D[i][0]);    // same as above

void fun_ptrs(int**);     // argument is pointer to pointer to int
void fun_ptrs(int*[]);    // same as above
void fun_ptrs(int*[10]);  // same as above; note that 10 is ignored

int *arr_ptr[20];         // array of pointers
fun_ptrs(arr_ptr);        // implicit conversion
fun_ptrs(&arr_ptr[0]);    // same as above

fun_1D(arr_ptr[i]);       // no conversion needed


// broken examples
fun_2D(arr_ptr);          // int*[20] is not int(*)[20]
fun_ptrs(arr_2D);         // int[20][20] is not int**

注意,声明为数组的函数参数在左值到右值转换时会被调整为相同的指向数组将要衰变为的指针类型。

一些简单的经验法则:

  • 数组不是指针。
  • 指针不是数组。
  • 一个写作数组的函数参数实际上不是一个数组。它实际上被调整为指向这种数组元素的指针。在此调整之后,函数参数永远不是数组。这不适用于任何其他情况,除了函数参数。
  • 并非每种类型都可以是数组的元素类型。未指定长度的数组就是这样的类型。
  • 不存在“数组长度未指定”的对象。它们只能用于引用在其他地方定义的数组的extern变量声明中,或在从数组的初始值推断出实际大小的定义中,或在函数参数声明中,其中该数组被调整为指向元素的指针。

如果我在main中声明int a[6][6]并调用期望int** a的函数,它能正常工作吗?

不行,因为int[6][6]不是int**,也不能衰变为int**。正如我上面解释的那样,int[6][6]会衰变为int(*)[6]int(*)[6]int**不能相互转换。一个是指向数组的指针,另一个是指向指针的指针。

反过来呢?

不行,因为int[6][6]参数会被调整为int(*)[6]。参见上文解释其不兼容原因。

似乎不接受int a[][]

正确。如我在最上面第四段(引文不计)所解释的。

如果我有函数f1(int *a)、f2(int a[])和f2(int a[6]),在这些情况下,sizeof(a)会返回什么?

正如我上面解释的,它们都声明了一个类型为int*的参数。sizeof asizeof(int*)相同,因为那就是这个类型。


我知道它们各自的意思,我想要问的是在一个地方以一种方式声明数组,然后在函数参数中以另一种方式使用它的上下文。如果我在main函数中声明int a[6][6],并调用需要int** a的函数,它会工作吗?反过来,如果main函数中有int ** m,而函数需要int a[][6],似乎int a[][]在当前的C/C++标准中不被接受,是吗? - Mihnea
另一个问题:对于主函数中的int* a;,sizeof(a)将是指针的大小,即整数值的大小。当我应用sizeof时,int a[20]将返回4 * 20字节。如果我有函数f1(int *a)和f2(int a[])以及f2(int a[6]),在这些情况下,sizeof(a)会返回什么? - Mihnea
谢谢。最后一个问题是:编写一个在一个或两个矩阵上执行操作的函数的最佳通用方法是什么?如果我使用f(int** m, int n, int m),而有人创建了一个int a[6][6]的矩阵,他将无法使用我的函数,对吗?没有解决办法吗?他必须创建一个带有动态分配的中间矩阵,将数据复制到其中,并使用该矩阵进行函数调用吗? - Mihnea
1
@Mihnea,“编写一个函数来处理一个或两个矩阵的最佳通用方法是什么?”编写一个代表矩阵的类。将数组分配为平坦的一维数组,其中行是顺序的(std::vector非常适合此操作)。更好的方法是使用现成的矩阵库。 - eerorika
C风格不使用类? - Mihnea

7
int **a

这是指向整型的指针指针。
int a[][]

这是一个整数数组的数组,但这不是有效的,因为第二个数组的维度必须在声明时就已知,即第二个数组必须是完整的,因为它后面不能被完成,就像这样。

int a[][DIM]

1
是的,只有最左边的维度可以留空。 - Ian Abbott

5

C++继承了C语言中数组衰变成指针的行为。

这种行为非常有帮助,直到遇到问题时,我们才会发现出现了一些奇怪的事情,于是我们尝试了解它的作用并测试它的行为,然后我们意识到它有点不可思议。

但要保持冷静,一旦你意识到一个数组衰变成指针是什么意思,一切就又变得清晰了。

以下是一个例子,用于说明其中的差异。

static void func1(int** p) {
    (void)p;
}

static void func2(int (&a)[2][2]) {
    (void)a;
}

int main() {
    // x is a 2-dimensional array of int objects.
    int x[2][2] = {{10, 20}, {30, 40}};
    // x[0][0] is 10, and is at 0x1000 (say, for example)
    // x[0][1] is 20, and is at 0x1004
    // x[1][0] is 30, and is at 0x1008
    // x[1][1] is 40, and is at 0x100C

    // y is a 1-dimensional array of pointers.
    int* y[2] = { &x[0][0], &x[1][0] };
    // y[0] is 0x1000, and is at 0x1010 (say, for example)
    // y[1] is 0x1008, and is at 0x1018

    // x cannot decay into an int**, because it is not an array of pointers.
    // y can decay into an int**, because it is an array of pointers.
    func1(y);

    // x can be used for an int[2][2] reference parameter.
    // y cannot be used for an int[2][2] reference parameter.
    func2(x);
}

好的,逻辑上讲,当我声明x[2][2]时,我不能使用它来调用一个期望int ** matrix的函数,因为我可以进行像matrix++这样的操作或修改指针...终于有了一个回答,指出了我想要的,即在声明/函数调用中无法互换它们。但是,当我编写操作矩阵的函数时,最好使用f(int **matrix, int m, int n),而不是静态声明矩阵吗?因为f(int a[][m], int n)意味着硬编码其中一个维度,不再使我的函数通用,对吗? - Mihnea

3
在C++中声明数组时,你正在分配一个连续的内存块来存储该数组的成员。
 int  data[6];    // Block of 6 integers.

即使对于多维数组也是如此。

 int  threeD[2][3][4];    // Allocates a Block of 24 integers.
                          // The compiler keeps track of the number of
                          // of dimensions and does the maths for you 
                          // to calculate the correct offset.

现在当一个数组被传递给函数时,该数组会转化为指向第一个元素的指针。

如果你正在动态分配一个多维数组,通常会将其作为数组的数组来完成。这些数组不是连续的内存空间。但你需要以这种方式来使用方括号运算符[],就像通常做的那样。

int***  dynamicThreeD = new int**[2];
for(int l1 = 0 ; l1 < 2 ; ++l1) {
    dynamicThreeD[l1] = new int*[3];
    for(int l2 = 0 ; l2 < 3 ; ++l2) {
        dynamicThreeD[l1][l2] = new int[4];
    }
}

int threedD[2][3][4];

虽然这两种类型在访问方式上看起来相同:
dynamicThreeD[1][2][3] = 8;
threeD[1][2][3]        = 8;

这两者不同。在 [] 中的 dynamicThreeD 访问数组并检索下一个指针以引用下一个元素。而 threeD 对象则进行索引计算 (3) + (4 * 2) + (4*3 * 1),然后使用该计算结果作为与第一个元素的偏移量。
dynamicThreeD[1][2][3] = 8;
// Equivalent to 
int** tmp1 = dynamicThreeD[1];
int*  tmp2 = tmp1[2];
tmp2[3] = 8;



threeD[1][2][3]        = 8;
// Equivalent to 
int index = (3) + (4 * 2) + (4*3 * 1)

// OK The compiler does not take the address like this
//    But I needed to do some type gymnastics to convert the type
//    at the language level.
//
//    But you get the same effect like this.
//    As the value is just offset by an index from the beginning of
//    the array.
(&threed[0][0][0])[index] = 8;

这样做的副作用是,多维数组可以更有效地工作。1)我们只需要进行一次内存访问(而不是三次)即可获取/设置数据。2)由于数据本地性和缓存,您将获得更好的缓存命中率。
另一方面,在并行系统中使用多维数组可能会很麻烦,如果您有多个写入器,则缓存一致性将成为一个问题,因为所有成员都在同一缓存行中时。在这种情况下,数组的数组具有优势,因为每行可以位于内存的不同部分并且可以独立进行操作(注:我过于简单地阐述了一个非常复杂的问题)。

1

在其他良好的回答中,我想关注您最后的关注点。


“对于int[][],几乎总是要求只让第一个参数为空,就像int array [][][][],我需要把a[][n1][n2][n3]放在函数参数中,是吗?”
是的,只有第一维可以不指定。但如果你想让其他维度在运行时变量/确定,在C语言中至少可以使用:”
int foo (int n1, int n2, int a[][n1][n2]) { ...

0

通过增加一个间接级别,您可以传递固定大小的维度,因为sizeof(* x),sizeof(x [0] [0])等知道间接类型的维度。 但通常在C中,数组[]只是指针*,尤其是在动态情况下,需要作为第二个参数、全局变量、宏或结构体中的已知大小进行传递,直到类出现。当然,对于char*和char**argv,有使用null term的事情。对于指针,您需要支付8字节的开销。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接