问题

c语言中的sizeof()是如何分辨数组名和指针的?

回答
在 C 语言中,`sizeof()` 操作符的魔法之处在于它能够根据其操作数的类型和大小来返回一个数值。而对于数组名和指针,它们虽然在某些上下文中表现得相似(例如,在函数参数传递时),但在 `sizeof()` 的眼中,它们的身份是截然不同的。这其中的关键在于数组名在绝大多数情况下会发生“衰减”(decay)为指向其首元素的指针,但 `sizeof()` 是一个例外。

让我详细地拆解一下这个过程。

首先,理解 C 语言中数组和指针的基本概念:

数组名: 当你声明一个数组,例如 `int arr[10];` 时,`arr` 这个标识符代表的是整个数组本身。它不仅包含了数组的元素,还蕴含了数组的大小信息。你可以想象它是一个连续的内存块,而 `arr` 是这个内存块的“门牌号”或“起始地址”。
指针: 指针是一个变量,它存储的是另一个变量的内存地址。例如,`int ptr = arr;` 声明了一个整型指针 `ptr`,并将其初始化为指向数组 `arr` 的第一个元素。`ptr` 存储的是 `arr[0]` 的地址,它本身占有固定的内存空间(通常是 4 或 8 字节,取决于系统架构),其大小并不随它指向的数据类型或数量而改变。

为什么 `sizeof()` 对数组名和指针的处理不同?

核心原因在于 `sizeof()` 操作符在处理不同类型的表达式时,有不同的规则。

1. 当 `sizeof()` 操作数是数组名时:
编译器在编译时就已经知道数组的大小。当你写 `sizeof(arr)`,编译器会直接查阅 `arr` 的声明,知道这是一个包含 10 个 `int` 的数组。
它会计算出整个数组占用的总字节数:`数组中元素的数量 每个元素的大小`。
例如,对于 `int arr[10];`,如果 `sizeof(int)` 是 4 字节,那么 `sizeof(arr)` 就是 `10 4 = 40` 字节。
关键点: 在这种情况下,`sizeof()` 不会发生数组到指针的衰减。它看到了一个完整的数组类型,并直接计算其总大小。

2. 当 `sizeof()` 操作数是指针时:
当指针变量(例如 `ptr`)作为 `sizeof()` 的操作数时,`sizeof()` 操作符看到的是一个指针类型,而不是它所指向的数据。
它会返回指针变量本身占用的内存大小。这个大小是固定的,与指针指向的数据类型或它指向多少个元素无关。
例如,对于 `int ptr = arr;`,`sizeof(ptr)` 会返回指针类型的大小,在大多数现代系统中是 4 字节(32 位系统)或 8 字节(64 位系统)。

数组名在何时会“衰减”为指针?

了解 `sizeof()` 的行为,还需要明白数组名在 C 语言中的“衰减”特性。在绝大多数表达式中,数组名都会被隐式地转换为指向数组第一个元素的指针。这发生在:

将数组名传递给函数作为参数时(例如 `void func(int arr[])` 或 `void func(int arr)` 是等价的)。
在算术运算中使用数组名(例如 `arr + 1`)。
在解引用中使用数组名(例如 `arr` 等同于 `arr[0]`)。

举例说明:

```c
include

int main() {
int arr[10]; // 一个包含10个整型的数组
int ptr = arr; // 一个指向数组首元素的指针

printf("Size of arr (the array): %zu bytes ", sizeof(arr));
printf("Size of ptr (the pointer): %zu bytes ", sizeof(ptr));

// 在函数调用时发生衰减
printf("Size of arr when passed to a function: ");
void print_array_size(int a[]); // 这里的 a[] 会衰减为 int a
print_array_size(arr);

// 直接使用数组名进行指针算术,发生衰减
printf("Address of arr: %p ", arr);
printf("Address of arr + 1: %p ", arr + 1); // arr + 1 是指向 arr[1] 的指针

// 通过指针访问数组元素
printf("Value at arr[0] using arr: %d ", arr); // 数组名衰减为指针后解引用
printf("Value at arr[0] using ptr: %d ", ptr);

return 0;
}

// 这个函数接收一个数组(实际上是接收一个指针)
void print_array_size(int a[]) {
printf(" Inside the function, sizeof(a): %zu bytes ", sizeof(a));
}
```

输出示例 (在 64 位系统上):

```
Size of arr (the array): 40 bytes
Size of ptr (the pointer): 8 bytes
Size of arr when passed to a function:
Inside the function, sizeof(a): 8 bytes
Address of arr: 0x7ffee8012340
Address of arr + 1: 0x7ffee8012344
Value at arr[0] using arr: 0
Value at arr[0] using ptr: 0
```

从输出中我们可以清晰地看到:

`sizeof(arr)` 返回了 40 字节,这是 `int` 类型的数组(10 个元素 4 字节/元素)的总大小。
`sizeof(ptr)` 返回了 8 字节,这是指针变量本身的大小(在 64 位系统上)。
在 `print_array_size` 函数内部,`sizeof(a)` 返回了 8 字节。这是因为当 `arr` 被传递给函数时,它发生了衰减,变成了指向第一个元素的指针,函数接收到的实际上是一个 `int `。

总结 `sizeof()` 如何分辨:

`sizeof()` 的行为取决于它直接操作的表达式的类型。

如果表达式是数组类型(例如直接使用数组名 `arr` 而不是 `arr` 或 `arr + n`),`sizeof()` 会计算整个数组的总字节数。
如果表达式是指针类型(例如 `ptr` 或 `arr + n`),`sizeof()` 会计算指针变量本身占用的字节数。

C 语言的设计允许这种行为,它在需要完整数组信息时(如 `sizeof`)保留数组的身份,而在需要作为序列进行操作时(如传递给函数、指针算术)将其简化为指向首元素的指针。这种机制使得 C 语言在内存管理和底层操作上既灵活又高效。

网友意见

user avatar

sizeof 的值是编译期计算的,只能获取编译期能确定的内容,不能获取运行期间才知道的信息。

函数参数是运行期传入,数组参数也允许传入指针,编译期间不知道传入的参数是什么,所以只能把它当做指针。因此有「数组在函数参数中退化为指针」一说。

user avatar

简单来说,C语言的sizeof()之所以能分辨出数组和指针,是因为编译器在编译的时候当然知道哪个变量是数组和哪个变量是指针。当你使用sizeof()的时候,你首先应该知道sizeof()并不是一个函数,它是C语言的关键字,或者说是一个运算符,C语言程序不是在运行时才执行sizeof()的,而是在编译时就对sizeof()做了完整的处理。这么说可能有点晦涩,我们看一个例子:

       // mytest.c #include <stdio.h>  int main(int argc, char *argv[]) {         int a[10];         int *p = a;         int sza = sizeof(a);         int szp = sizeof(p);          printf("sza=%d, szp=%d
", sza, szp);          return 0; }     

很简单的一个mytest.c程序,就是有一个数组a,和一个指针p(指向a),然后分别通过sizeof得到数组a和指针p的大小。在x86_64的系统上,我们得到:

       # gcc -o mytest mytest.c -Wall # ./mytest sza=40, szp=8     

(记住这个运行结果,后面用到)

那么这个40和这个8是怎么得来的呢?我们可以将编译过程细化一下,首先我们做预编译:

       # gcc -o mytest.i mytest.c -Wall -E     

经过预编译我们得到一个预编译展开的原文件,里面展开了include的文件,以及一些宏定义等。我们关注sizeof有没有在预编译阶段被处理:

       # cat mytest.i ... # 3 "mytest.c" int main(int argc, char *argv[]) {  int a[10];  int *p = a;  int sza = sizeof(a);  int szp = sizeof(p); ...     

显然sizeof没有在预编译阶段被处理,它还保持原样。那么我们进行下一步——编译。就是把C原程序编译为汇编程序:

       # gcc -o mytest.s mytest.i -Wall -S     

现在我们得到了汇编程序mytest.s,我们看一下sizeof怎么样了:

       # cat mytest.s ...       9 main:      10 .LFB0:      11         .cfi_startproc      12         pushq   %rbp      13         .cfi_def_cfa_offset 16      14         .cfi_offset 6, -16      15         movq    %rsp, %rbp      16         .cfi_def_cfa_register 6      17         subq    $80, %rsp      18         movl    %edi, -68(%rbp)      19         movq    %rsi, -80(%rbp)      20         leaq    -64(%rbp), %rax      21         movq    %rax, -8(%rbp)      22         movl    $40, -12(%rbp)      23         movl    $8, -16(%rbp)      24         movl    -16(%rbp), %edx      25         movl    -12(%rbp), %eax      26         movl    %eax, %esi      27         movl    $.LC0, %edi      28         movl    $0, %eax      29         call    printf      30         movl    $0, %eax      31         leave      32         .cfi_def_cfa 7, 8      33         ret      34         .cfi_endproc ...     

为了便于说明,我把行号也显示出来了:

            17         subq    $80, %rsp     

这一行为main函数创建函数栈,那些临时变量(比如a[10],p,sza,szp)就这样分配了空间。

            18         movl    %edi, -68(%rbp)      19         movq    %rsi, -80(%rbp)     

这两行是把main函数的两个参数argc和argc保存在栈中,对此问题没有影响,可以忽略。

            20         leaq    -64(%rbp), %rax      21         movq    %rax, -8(%rbp)     

这两行相当于取数组a的首地址放入rax寄存器,然后在把这个地址赋值给-8(%rbp),这个-8(%rbp)就是变量p在main函数栈中的地址。

            22         movl    $40, -12(%rbp)      23         movl    $8, -16(%rbp)     

这两行就是我们要找的,这两行就相当于:

       int sza = sizeof(a); int szp = sizeof(p);     

-12(%rbp)就是变量sza在main函数栈中的地址,-16(%rbp)就是变量szp在main函数栈中的地址。所以这两句的意思就是把立即数40赋值给sza,把立即数8赋值给szp。

23行往后就是准备printf的参数以及调用printf函数的过程了,我们不关系printf怎么调用,所以不往后说。我们光看第22和23行,可能令一部分初学者比较吃惊的是sizeof()的结果竟然在这里就已经被得到并写进在程序里了。我什么都没有执行后面的汇编、链接、运行等操作,就在编译C语言原程序到汇编程序的这个过程,sizeof就已经被处理完了。

所以说sizeof()并不是如部分人所想的在程序运行时去获取变量的大小,它是在编译的时候就由编译器直接完成计算了。编译器通过语法分析,直接知道sizeof(a)和sizeof(p)里的变量a和p分别是什么类型的变量,并直接计算出它们的大小后替换掉sizeof()部分。所以你还要问c语言中的sizeof()是如何分辨数组名和指针的吗?编译器自己当然能正确的理解自己所支持的变量类型,并正确的计算出类型大小了。


经评论区小伙伴提醒,我才发现这个问题的主题虽然问的是sizeof,但是问题描述的最后却落点不是在sizeof上。实际上这个问题想问,C语言如何区分数组名和指针,跟sizeof没有必然的关系。所以下面我稍微补充一下回答,关于C语言是怎么知道数组名是数组而不是指针的,其实说简单了还是和上面的解释一样,因为C语言编译器在解析C语言语句(编译)的时候根据语法的描述自然而然的知道哪个是数组名,哪个是指针。比如你写:

       int main() {     int a[10];     int *pa = a;     ... }     

C语言编译器按照语法解析的时候看到“int a[10]”当然就知道a是一个数组了,而且数组长度是10个int型整数的长度,pa是一个指针。程序这么明显的写在那,你还问C语言编译器怎么知道的,它要是连这个都搞不清楚它还怎么编译C语言?

但是这里面有两个需要注意的地方:

  • 1. C语言编译器只在一个数组的作用域内将数组名视为一个数组。

我们知道在C语言中任何变量都是有作用域(范围)的,数组当然也不例外。数组名只在其作用域范围内有效,在一个数组变量的作用范围内编译器看到这个变量名时知道它是个数组,但是超过其作用域时编译器则不再把这个名字当作原来的数组了。这也是这个题目中描述的这个程序……

       void printSize(int a[10]) {     printf("%d
",sizeof(a)); }  int main() {     int a[10];      printf("%d
",sizeof(a));     printSize(a);      return 0; }     

为什么在main函数中sizeof(a)可以得到数组长度,而子函数中sizeof(a)得到的就是指针长度的原因。虽然这个问题的作者还特别想以"void printSize(int a[10])"的方式告诉编译器a是什么,但是很可惜这种写法是不支持的,编译器要么给你一个警告,告诉你你的int a[10]是个错误的参数写法,会被当作int *a处理。要么就干脆给你一个错误好了。

所以在main函数中声明定义的一个临时数组,在main函数内看到这个数组名时编译器知道它是数组,但是超出main函数范围后编译器看到这个“数组名”就不把它当作那个数组了。因为那个数组的作用域就在main函数内。当然你可以想办法扩大一个数组的作用域,比如声明一个全局数组。总之任何时候超过作用域范围的变量名则失去其在原作用域里的意义。

  • 2. C语言的数组长度都是在编译的时候基本就确定了(确定一个数值或一个算式)

这句话什么意思呢?分两种情况,一种是在不支持不定长度数组的C语言编译器上,数组的长度是在声明时就固定的,这个也是我们平时常见常用的,像上面那个“int a[10]”一样,这样编译的时候编译器直接就知道数组的长度是10个int的长度,直接在编译时将sizeof(a)替换成10个int的长度的具体数值就行。

第二种情况是后来支持C99标准的编译器开始尝试支持不定长度的数组,比如这样:

       int main() {     int size = 10;     int myarray[size];     ... }     

或者:

       void func(int size) {     int myarray[size];     .... }  int main() {     int size = 10;     func(size);     ... }     

甚至还可以是这样:

       #include <stdio.h> #include <stdlib.h>  void func(int size) {     int myarray[size];     printf("size of myarray = %ld
", sizeof(myarray)); }  int main(int argc, char *argv[]) {     int size=atoi(argv[1]);     size += 10;              func(size);     return 0; }     

那这样的数组长度怎么处理呢?其实和上面定长的数组一样,还是在数组的作用域内将数组名视为数组,sizeof这个数组名可以得到其长度,而超出数组作用范围的地方则不能通过sizeof得到其长度。

那这个带一个变量的数组长度怎么确认呢?我们已经知道,对于定长的数组,在编译的时候编译器直接得到数组长度的具体数值。现在长度变成了一个带未知数的算式了,所以sizeof数组就不能直接替换为一个数值了,而是需要按照带有未知数的算式处理。比如上面的程序,数组的长度显然由argv[1]的数值加10得到,而argv[1]是一个未知数,我设其是x,所以sizeof(myarray)就是“x+10”,而在计算机中你可以将这个x理解为一个确定地址的内存空间,或者干脆理解为一个确定的寄存器。虽然“算式”是确认的,但算式中的未知数的具体数值只能等运行时才能代入计算。

比如编译器先确认将atoi(argv[1])的数值先保存在edi寄存器中,然后给edi里的数值加10,最后再让edi里的数值乘以sizeof(int)就是数组myarray的长度了。于是凡是写sizeof(myarray)的地方都可以用edi寄存器来代替(或者用edi*sizeof(int)替换也行)。当然编译器不一定像我这样处理,但是基本是这个意思,编译器一定会在编译的时候想办法把sizeof的结果处理好。

这个问题已经说了挺多了,就到这吧。很多地方其实都是C语言的设计造成的,我也不想再费更多篇幅解释说为什么一定要是这样的行为,或者为什么不能是你想像的那样的行为。编程语言们说了“我拿什么跟你玩,不是看你要什么,而是要看我有什么”

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有