C语言中的数组数据类型

26

阅读有关C中指针和数组的一些细节后,我有些困惑。

一方面,数组可以被视为数据类型。另一方面,数组往往是一个不可修改的lvalue。我想象编译器会做类似的事情,用常量地址和运行时计算给定索引位置的表达式来替换数组的标识符。

myArray[3] -(compiler)-> AE8349F + 3 * sizeof(<type>)

当说一个数组是一种数据类型时,这意味着什么?我希望你能帮我澄清我对数组的真正理解以及编译器如何处理它。


2
数组是一个对象,它封装了可能超过一个对象的连续内存块。数组与指针有关,但我认为这实际上是另一个问题,您应该在这里阅读相关内容:http://c-faq.com/aryptr/index.html。 - user529758
2
你的想象似乎是正确的(至少对于流行的C语言实现来说)。问题具体是什么? - Carl Norum
6
@HotLicks: 数组对象和任何其他类型的对象一样“真实”。例如,包含N个foo元素的数组的大小为N*sizeof(foo)。另一方面,数组表达式的行为很奇怪,这在comp.lang.c FAQ第6节中得到了详细解释,H2CO3已经在上面提供了链接。 - Keith Thompson
3
您的标题说是C++,但您的问题和标签只提到了C。您是在问关于C++的问题还是不是? - Ben Voigt
3
所有类型信息都是编译器虚构的。硬件本身并没有类型。 - Ben Voigt
显示剩余12条评论
2个回答

21
当说到数组是一种数据类型时,这到底意味着什么?
数据类型是一组具有预定义特征值的数据。数据类型的例子包括整数、浮点数、字符、字符串和指针。
数组是由所有具有相同名称和相同类型的内存位置组成的集合。
如果你想知道为什么数组不可修改,那么我所读到的最好的解释是:
C语言并非源自Dennis Ritchie的思维,而是来自于一种早期语言B(衍生自BCPL)。1 B是一种“无类型”语言;它没有整数、浮点数、文本、记录等不同类型。相反,每个东西都只是一个固定长度的单元或者“单元格”(基本上是一个无符号整数)。内存被视为一条线性单元格阵列。在B中分配数组时,例如:
auto V[10];

编译器分配了11个单元;10个连续单元用于数组本身,另一个单元绑定到V,其中包含第一个单元的位置:

    +----+
V:  |    | -----+
    +----+      |
     ...        |
    +----+      |
    |    | <----+
    +----+
    |    |
    +----+
    |    |      
    +----+
    |    |
    +----+
     ...

当Ritchie在将struct类型添加到C语言时,他意识到这种安排会带来一些问题。例如,他想创建一个结构体类型来表示文件或目录表中的条目:

当Ritchie在为C语言添加struct类型时,他意识到这样的安排会导致一些问题。例如,他想创建一个结构体类型来表示文件或目录表中的条目:

struct {
  int inumber;
  char name[14];
};
他希望这个结构不仅以抽象方式描述条目,而且还要表示实际文件表条目中的位,该条目没有额外的单元或字来存储数组中第一个元素的位置。所以他放弃了它-他没有设置单独的位置来存储第一个元素的地址,而是编写了C代码,使得在计算数组表达式时,第一个元素的地址将被计算出来。 这就是为什么你不能做像这样的事情。
int a[N], b[N];
a = b;

因为在这种情况下,ab都被评估为指针; 这等效于编写3 = 4。实际上没有任何存储数组中第一个元素的地址的内存; 编译器只是在翻译阶段计算它。


1. 这些内容均摘自C语言的发展的论文


如果您想了解更多细节,请阅读此答案


编辑:为了更清晰;可修改l-value,不可修改l-value和r-value之间的区别(简述);

这些表达式之间的区别如下:

  • 可修改的l-value是可寻址的(可以是一元&的操作数)并且可赋值的(可以是=的左操作数)。
  • 不可修改的l-value是可寻址的,但不可赋值。
  • r-value既不可寻址也不可赋值。

1
@Sam 为了将这个问题与你最初的例子联系起来,假设你有 <type> foo[7]; myArray = foo; 那么你会在第二行 (myArray = foo;) 得到一个编译器错误。这就是他们所说的数组是不可修改的左值*。当然,你可以为 myArray[i](对于任何常量或变量 i)赋值,但你不能直接赋值给 myArray。想一想它被编译成什么...根据你的例子,myArray = foo; 将会是 0xAE8349F = 0xDEADBEEF;(假设 foo 位于地址 0xDEADBEEF)。对于任何赋值运算符,常量都不能是左值。 - Dave Lillethun
“lvalue” 不仅仅是它可以放置的位置,最简单的说,它是一个指向对象的表达式。在运算符方面,你通常会听到它被描述为只能放置在赋值运算符的左侧,而不是任何运算符的左侧,但显然不可修改的 lvalue 不能这样做。 - Crowman
@haccks:不好意思,我是在回应DaveLillethun的“'lvalue'只是指它可以出现在运算符的左侧”。 - Crowman
1
@DaveLillethun:名称“lvalue”和“rvalue”特指赋值的左侧和右侧,而不是任何一般运算符。对于2 + 3,既不是2也不是3是lvalue。在C标准中,“lvalue”的确切定义在每个版本中都有所改变,但基本上它是一个(潜在地)指定对象的表达式。 - Keith Thompson
请在给负评之后留下评论。 - haccks
显示剩余7条评论

0

数组是一块连续的内存区域。这意味着它在内存中是按顺序排列的。假设我们定义了如下的一个数组:

int x[4];

sizeof(int) == 32 位时。

这将在内存中以这种方式布局(选择一个任意的起始地址,比如说 0x00000001

0x00000001 - 0x00000004
[element 0]
0x00000005 - 0x00000008
[element 1]
0x00000009 - 0x0000000C
[element 2]
0x0000000D - 0x00000010
[element 3]

你说得对,编译器会替换标识符。记住(如果你已经学过的话,否则你正在学习新知识!)数组本质上是一个指针。在C/C++中,数组名是指向数组第一个元素的指针(或者在我们的例子中指向地址0x00000001的指针)。通过这样做:

std::cout << x[2];

你告诉编译器将2加到那个内存地址,这就是指针算术。 假设你使用一个变量来索引:
int i = 2;
std::cout << x[i];

编译器看到这个:
int i = 2;
std::cout << x + (i * sizeof(int));

它基本上将数据类型的大小乘以给定的索引,然后将其添加到数组的基地址中。编译器基本上采用索引运算符[]并将其转换为指针加法。

如果你真的想让自己头晕,考虑一下这段代码:

std::cout << 2[x];

这是完全有效的。如果你能弄清楚为什么,那么你就掌握了这个概念。


请注意,这里有许多小的不准确之处,这些不准确之处共同导致答案在可靠性方面失效。例如,x[2]并不会将2添加到x的地址上。其次,数组在功能上只是一块连续的内存块,但这并不意味着元素之间没有间隙(考虑对齐)。 - user268396
3
元素之间紧密相连,填充是元素的一部分。 - Mooing Duck
当然,你是正确的。我的打字速度并不比答案好多少,但重点在于答案中描述的关于数组元素内存地址的“图像”(与实际获得的内容相比)并没有考虑到这一点。不考虑填充/对齐问题往往会在人们开始做聪明的事情时引入讨厌的错误... - user268396
@user268396 对你也是一样。 - MGZero
5
请解释为什么 int arr[100]; sizeof arr 并不能给你一个指针的大小。在大多数情况下,数组名会被 转换 成指向第一个元素的指针。请参考 comp.lang.c FAQ 第6节。 - Keith Thompson
显示剩余6条评论

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