sizeof 的值是编译期计算的,只能获取编译期能确定的内容,不能获取运行期间才知道的信息。
函数参数是运行期传入,数组参数也允许传入指针,编译期间不知道传入的参数是什么,所以只能把它当做指针。因此有「数组在函数参数中退化为指针」一说。
简单来说,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语言?
但是这里面有两个需要注意的地方:
我们知道在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函数内。当然你可以想办法扩大一个数组的作用域,比如声明一个全局数组。总之任何时候超过作用域范围的变量名则失去其在原作用域里的意义。
这句话什么意思呢?分两种情况,一种是在不支持不定长度数组的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语言的设计造成的,我也不想再费更多篇幅解释说为什么一定要是这样的行为,或者为什么不能是你想像的那样的行为。编程语言们说了“我拿什么跟你玩,不是看你要什么,而是要看我有什么”