C/C++编译器如何区分常规二维数组和指向数组的指针数组?

5

常规的静态分配数组看起来像这样,可以使用以下公式进行访问:

const int N = 3;
const int M = 3;

int a1[N][M] = { {0,1,2}, {3,4,5}, {6,7,8} };

int x = a1[1][2]; // x = 5 
int y = *(a1+2+N*1); // y = 5, this is what [] operator is doing in the background

数组是内存的连续区域。在动态数组分配的情况下,它的外观有所不同,变成了指向数组的指针数组:

int** a2 = new int*[N];
for (int i = 0; i < N; i++) 
   a2[i] = new int[M];

//Assignment of values as in previous example

int x = a2[1][2];
int y = *(*(a2+1))+2); // This is what [] operator is doing in the background, it needs to dereference pointers twice

从我们可以看到,使用[]操作符进行操作在典型连续数组和动态分配数组中完全不同。

  1. 我对[]操作的理解是否正确?
  2. C/C++编译器如何区分应执行哪个[]操作,并在哪里实现它?我能够想象通过重载C++中的[]运算符来自己实现它,但是C/C++是如何处理它的?
  3. 在C语言中使用malloc而不是new会正常工作吗?实际上我没有看到任何不行的理由。

注意:我的第一个例子是错误的。请查看被接受的答案以获取详细信息。 - Mazi
5个回答

7

对于这个数组的声明

int a1[N][M] = { {0,1,2}, {3,4,5}, {6,7,8} };

这些记录

int x = a1[1][2];
int y = *(a1+2+N*1); 

第二个表达式是不正确的。表达式*(a1+2+N*1)的类型为int[3],隐式转换为一个作为初始化器使用的int *类型对象。因此整数变量y被指针初始化。
操作符a1[1]的求值方式类似于*( a1 + 1 )。结果是一个int[3]类型的一维数组。
因此,在应用第二个下标运算符时,您将得到*( *( a1 + 1 ) + 2 )
在使用二维数组和动态分配的数组时的表达式之间的区别在于,这个表达式中的二维数组指示符(a1 + 1)已经隐式转换为其类型为int (* )[3]的第一个元素的指针,而指向动态分配的指针数组的指针仍然具有相同的类型int **
在第一种情况下,对表达式*(a1 + 1 )进行解引用,您将得到int [3]类型的左值,该左值在表达式*( a1 + 1) + 2中再次隐式转换为int *类型的指针。
在第二种情况下,表达式*(a1 + 1)产生了一个int *类型的对象。
在两种情况下均使用指针算术运算。区别在于,在下标运算符中使用数组时,它们会隐式转换为指向它们的第一个元素的指针。
当您动态分配数组时,您已经处理了指向它们的第一个元素的指针。
例如,代替这些分配:
int** a2 = new int*[N];
for (int i = 0; i < N; i++) 
   a2[i] = new int[M];

你可以直接编写

int ( *a2 )[M] = new int[N][M];

谢谢您的解释。我在这里缺失的,也是令人困惑的,是数组上的解引用运算符如何工作。它不返回元素值,而是返回其地址,有效地改变了返回类型,但并未改变指针值本身。从我的示例中可以看出,a1的数值等于*a1的数值。再次感谢! - Mazi

3

Is my understanding of [] operations correct?

int y = *(a1+2+N*1); // y = 5, this is what [] operator is doing in the background

根据定义,将下标操作符翻译为对应的间接运算和指针算术运算的方法是:

int y = *(*(a1+1)+2)

就像 int** 的情况一样。

C/C++编译器如何区分应执行哪个 [] 操作?

编译器使用类型系统。它知道表达式的类型,也知道每种类型的下标操作意味着什么。

在C语言中,使用malloc而不是new会正常工作吗?实际上我没有看到任何不行的理由。

创建数组的方式并不重要。对于所有指针,下标运算符的工作方式都是相同的。


0

a1a2是不同类型的,因此operator []的行为将取决于该类型如何定义运算符。在这种情况下,您正在处理符合C++规范的内置编译器行为,但它也可以是std::unique_ptr<>,或者是重载了operator[]MyClass


0
每个操作都会产生某种特定类型的结果。每种类型定义了它所支持的操作类型。
请注意,数组有能力衰变为指向数组元素的指针。因此,some_array + int_value会得到指向元素的指针
以下是展示每个步骤类型的代码:https://godbolt.org/z/jeKWh5WWW
#include <type_traits>

const int N = 3;
const int M = 4;

int a1[N][M] = { {0,1,2,0}, {3,4,5,0}, {6,7,8,0} };
int** a2 = new int*[N];

static_assert(
    std::is_same_v<decltype(a1[0][0]), int&>, 
    "value type is reference to int");

static_assert(
    std::is_same_v<decltype(a1[0]), int(&)[M]>, 
    "row type is reference to int aray");

static_assert(
    std::is_same_v<decltype(a1 + 1), int(*)[M]>, 
    "advanced pointer is pointer to array of ints");

static_assert(
    !std::is_same_v<decltype(a1[0]), int*&>, 
    "row type is reference to int pointer");



static_assert(
    std::is_same_v<decltype(a2[0][0]), int&>, 
    "value type is reference to int");

static_assert(
    !std::is_same_v<decltype(a2[0]), int(&)[M]>,
    "row type is not reference to int aray");

static_assert(
    std::is_same_v<decltype(a2 + 1), int**>, 
    "advanced pointer is pointer to pointer to int");

static_assert(
    std::is_same_v<decltype(a2[0]), int*&>, 
    "row type is reference to int pointer");

我认为这是其他答案的好补充。


0
C/C++编译器如何区分应该执行哪个[]操作,并确定它们的实现位置?
内置的“[]”运算符(即非用户定义重载)始终执行一项任务:将其两个操作数相加并取消引用结果。 `E1 [E2]`被定义为 `(*((E1)+(E2)))`。以下是其工作原理:
  • 如果 E1E2 是一个数组,它会自动转换为指向其第一个元素的指针。这不是 [] 运算符本身的一部分;它是 C 和 C++ 语言内置的一部分。在 C 中,具体规则是,每当一个数组在表达式中被使用时,除了作为 sizeof 的操作数、一元 & 的操作数或用于初始化数组的字符串字面值之外,它都会被转换为指向其第一个元素的指针。
  • 因此,无论代码是用指针还是数组编写的,[] 始终具有指针操作数。你可以编写一个数组,但 [] 总是接收一个指针。
  • + 运算符通过将指针调整给定数量的元素来添加整数到指针:给定一个指向数组的元素 j 的指针和一个要添加到它的整数 k,它会产生一个指向数组的元素 j+k 的指针。
  • 从元素的指针中,* 运算符会产生所引用元素的 lvalue。

自动数组转换、+* 的组合意味着 A[i] 会产生一个数组 A 中元素 i 的左值。

下面是当 A 声明为 SomeType A[m][n] 的数组时,如何处理表达式 A[i][j]

  • A[i][j] 中,A 是一个由 m 个数组和每个数组中有 n 个元素组成的数组。它会自动转换为指向其第一个元素(索引为 0 的元素)的指针。
  • 然后,A[i] 会产生该数组中元素 i 的左值。换句话说,A[i] 的结果是一个数组;它是一个由 nSomeType 对象组成的数组。
  • 由于 A[i] 的结果是一个数组,因此它会自动转换为指向其第一个元素的指针。
  • 然后,A[i][j] 会产生该数组中元素 j 的左值。

由于指针算术运算以所指向的类型为单位进行操作,因此它包括元素大小的缩放。这就是使得计算 A[i] 时按照子数组大小为 n 的比例进行缩放的原因。

如果使用 malloc 而不是 new,在 C 语言中会正常工作吗?实际上我没有看到任何不行的理由。

当然,如果正确执行。


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