【转载】C语言 sizeof深度解析

【转载】C语言 sizeof深度解析

原文网址:
作者:醉卧沙场
链接:https://www.zhihu.com/question/464844221/answer/1940453834
来源:知乎

简单来说,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\n", 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§;-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§里的变量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\n",sizeof(a));
}

int main()
{
    int a[10];

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

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值