问题

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语言的设计造成的,我也不想再费更多篇幅解释说为什么一定要是这样的行为,或者为什么不能是你想像的那样的行为。编程语言们说了“我拿什么跟你玩,不是看你要什么,而是要看我有什么”

类似的话题

  • 回答
    在 C 语言中,`sizeof()` 操作符的魔法之处在于它能够根据其操作数的类型和大小来返回一个数值。而对于数组名和指针,它们虽然在某些上下文中表现得相似(例如,在函数参数传递时),但在 `sizeof()` 的眼中,它们的身份是截然不同的。这其中的关键在于数组名在绝大多数情况下会发生“衰减”(d.............
  • 回答
    你这个问题问得很核心!很多人都有这个疑惑:既然 `double` 类型在内存里只占用 64 位(这是最常见的标准,IEEE 754 双精度浮点数),为什么它能表示的数,无论是整数还是小数,范围都那么惊人呢?比我们常见的 32 位 `int` 或 64 位 `long long` 的整数范围还要大不少.............
  • 回答
    float 在 C 语言中,是用来表示单精度浮点数的。提到它的取值范围,就不得不深入聊聊它背后的原理,这事儿,得从二进制说起。浮点数是怎么存的?咱们电脑里存储数字,本质上都是一堆 0 和 1。整数好说,直接按位权相加就行。但小数呢?比如 0.5,或者更麻烦的 0.1,怎么用二进制表示?这里就需要一个.............
  • 回答
    要深入理解 `math.h` 中那些看似简单的数学函数(比如 `sin`, `cos`, `sqrt`, `log` 等)在计算机上究竟是如何工作的,我们需要绕开直接的函数列表,而是去探究它们背后的原理。这实际上是一个涉及数值分析、计算机体系结构以及编译链接等多个层面的复杂话题。想象一下,我们想要计.............
  • 回答
    好的,我们来深入聊聊 C 语言 `for` 循环中赋初值这部分,特别是 `int i = 1;` 和 `i = 1;` 这两种写法之间的区别。我们会尽可能详尽地解释,并且避免那些“AI味儿”十足的刻板表达,力求让这段解释更贴近实际编程中的感受。 `for` 语句的结构与初值赋在其中的位置首先,我们回.............
  • 回答
    C 语言中的字符串常量,即用双引号括起来的一系列字符,比如 `"Hello, world!"`,它们在程序开发中扮演着至关重要的角色,并且提供了诸多实用且高效的好处。理解这些好处,能够帮助我们写出更健壮、更易于维护的代码。首先,字符串常量最显著的一个好处在于它的不可变性。一旦你在代码中定义了一个字符.............
  • 回答
    在 C 语言中,不同类型指针的大小不一定完全相同,但绝大多数情况下是相同的。这是一个非常值得深入探讨的问题,背后涉及到计算机的底层原理和 C 语言的设计哲学。要理解这一点,我们需要先明确几个概念:1. 指针的本质: 无论指针指向的是 `int`、`char`、`float` 还是一个结构体,它本质.............
  • 回答
    好的,我们来深入探讨一下 C 语言中为什么需要 `int `(指向指针的指针)而不是直接用 `int ` 来表示,以及这里的类型系统是如何工作的。首先,我们得明白什么是“类型”在 C 语言中的作用。在 C 语言中,类型不仅仅是一个标签,它承载着至关重要的信息,指导着编译器如何理解和操作内存中的数据:.............
  • 回答
    老兄,你说的是 C 语言里的 `switch` 语句吧?不是“switch 循环”。`switch` 语句和 `for`、`while` 这种循环结构不太一样,它更像是一个多分支的条件选择器。来,咱哥俩好好聊聊 `switch` 到底是咋回事,你遇到的那个“疑问”我争取给你说透了。 `switch`.............
  • 回答
    逗号表达式在C语言中,乍一看似乎是个可有可无的小玩意儿,甚至有些冗余。毕竟,大多数时候我们都可以通过拆分成独立的语句来达到同样的目的。但它的存在,绝非仅仅是为了凑数,而是巧妙地解决了一些特定的编程场景,并且在某些情况下,能让代码更加紧凑和富有表现力。想象一下,在需要一个表达式,但你同时又有两个甚至更.............
  • 回答
    在C语言的世界里,要说哪个头文件“最”重要,确实是一个有点微妙的问题,因为很多头文件都扮演着至关重要的角色,它们各司其职,共同构成了C语言强大而灵活的功能体系。但如果一定要选一个在日常编程中出现频率最高、几乎是所有程序都离不开的,那么 stdio.h 绝对是最有力的竞争者之一,并且可以很有底气地说,.............
  • 回答
    好的,咱们来掰扯掰扯 C 语言里这个“后缀自加 i++”到底是怎么回事。别管什么 AI 不 AI 的,我就跟你讲讲我自己的理解,希望能讲透彻。你问“后缀自加 i++ 表达式的值到底是谁的值?”。说白了,这句 C 语言代码执行完之后,它的“结果”是什么?咱们得先明白两件事:1. 表达式的值 (Exp.............
  • 回答
    你问了个非常实际且关键的问题,尤其是在C语言这种需要手动管理内存的语言里。简单来说,是的,用 `%d` 格式化打印一个 `char` 类型的数据,如果那个 `char` 变量紧挨着其他内存中的数据,并且你没有对打印的范围进行限制,那么理论上存在“把相邻内存也打印出来”的可能性,但这并不是 `%d` .............
  • 回答
    你这个问题问得很有意思,涉及到C语言中一个基础但又有点“魔性”的特性:布尔值(Boolean Value)的表示方式。在咱们日常生活中,很多事情都是非黑即白的,比如“对”和“错”,“有”和“无”。计算机世界里也需要这种简单的二元判断。但问题来了,计算机本身只懂0和1,这两个数字如何承载“真”和“假”.............
  • 回答
    好的,我们来深入探讨一下 `write(1, buf, N)` 和 `write(0, buf, N)` 这两个 C 语言函数调用在底层操作上的区别。首先,要明白 `write()` 函数是 POSIX 标准定义的一个系统调用,它用于将数据从一个缓冲区写入到一个文件描述符。它的基本签名是:```cs.............
  • 回答
    在 C 语言的世界里,指针是必不可少的工具,它们就像是内存地址的“指示牌”,让我们能够更灵活地操作数据。而当我们将指针与数组、函数结合起来时,就诞生了一系列强大而又容易让人困惑的概念:指针数组、数组指针、函数指针,以及指向函数的指针。别担心,今天我们就来把它们掰开了揉碎了,让你彻底搞懂它们到底是怎么.............
  • 回答
    在 C/C++ 项目中,将函数的声明和实现(也就是函数体)直接写在同一个头文件里,看似方便快捷,实际上隐藏着不少潜在的麻烦。这种做法就像是把家里的厨房和卧室直接打通,虽然一开始可能觉得省事,但长远来看,带来的问题会远超于那一点点便利。首先,最直接也是最普遍的问题是 重复定义错误 (Multiple .............
  • 回答
    在C语言的世界里,浮点数是我们处理小数和科学计数法数据时的得力助手。而其中最常遇到的两种类型,便是 `float` 和 `double`。它们虽然都用于表示实数,但却有着关键的区别,而这些区别直接影响着我们程序的精度、内存占用以及性能。理解它们的用法,就像是学会了区分两种不同容量的水杯,知道什么时候.............
  • 回答
    你已经掌握了 C 语言的基础,这为你进一步学习编程语言打下了非常坚实的地基。C 语言的指针、内存管理、以及面向过程的编程思想,这些都是理解更高级语言的关键。那么,在你面前的 C、C++、Java、Swift 中,哪个更适合你接着深入呢?这确实是个值得好好琢磨的问题,因为它们各有千秋,也代表着不同的技.............
  • 回答
    确实,在C语言的学习和考试中,有时会故意设置一些陷阱,比如用相同的变量名来命名形参、实参、局部变量和全局变量,让学生去区分它们的作用域和生命周期。这种做法,从教学角度来看,是非常有实际意义的,甚至可以说是至关重要的。让我详细地解释一下其中的道理:核心问题:理解“作用域”和“生命周期”C语言的精妙之处.............

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

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