Chinaunix

标题: C语言中本地变量local variable的作用域与生存期 [打印本页]

作者: sagasw    时间: 2010-12-20 16:56
标题: C语言中本地变量local variable的作用域与生存期
《狂人C》中191页提到“每次运行到auto变量j所在block,会为j寻找存储空间,离开j所在代码模块,j的内存被释放掉。

这是不正确的。

结论应该是:对于C语言而言,本地变量会在栈开始处申请,栈销毁时结束生命。但是本地变量的作用域与所在block相关。之所以编译不通过,是因为这种block之外访问block之内变量的语法是错误的,离开本地变量所在block{},它的作用域无效,但不是说销毁了。

作用域错误是语法层面的。而生存期(存在销毁)与程序栈相关,是运行时的概念,它们是两码事。而且,有没有auto这个 keyword修饰,结果都一样,所以auto用不用没有任何意义。

关于C语言程序栈以及调用规范,可以参考我前面翻译的文章:http://sunxiunan.com/?p=1229 也可以查找wiki关于calling conversion方面的资料。

下面我们通过这段代码说明一下:

https://gist.github.com/748095

int main (int argc, char *argv[])
{

int a = 99;
int b = a;
int* pLocala = NULL;
int* pLocalb = NULL;
int d = 1;
printf("hello world \n");

do{

int a = 3;
int b = 23;
pLocala = &a;
pLocalb = &b;
b = a + 5;
printf("%d %p\n", b, &a);
}while(0);

int e = 1;
b = a;

printf("%d %p\n", b, &a);
printf("%p %p %p %p %p %p %p %p\n", &e, pLocalb, pLocala, &d, &pLocalb, &pLocala, &b, &a);
return(0);
}

如果你运行这段代码(无论是VC++还是GCC),最后printf出来的结果应该是连续的,而&pLocala 和&pLocalb其实就是一种while循环中定义的变量a和b所在地址,可以看出来它们夹在变量d与e中间。

我们可以用汇编代码看的更清楚一些,在VC2010生成的汇编代码(C源代码与前面略有不同),完整输出参考
https://gist.github.com/748116

————————————————

;    COMDAT _wmain

_TEXT    SEGMENT

_b$4410 = -32                        ; size = 4

_a$4409 = -20                        ; size = 4

_a$ = -8                        ; size = 4

_argc$ = 8                        ; size = 4

_argv$ = 12                        ; size = 4

  ; 7    : {

……

; 8    :     int a = 0;

   mov    DWORD PTR _a$[ebp], 0

$LN3@wmain:

; 9    :

; 10   :     do

; 11   :     {

; 12   :         auto int a = 3;

    mov    DWORD PTR _a$4409[ebp], 3

; 13   :         auto int b = 0;

    mov    DWORD PTR _b$4410[ebp], 0

……

——————————————

可以看到中间;12 : auto int a = 3;使用的是这样的汇编代码

mov    DWORD PTR _a$4409[ebp], 3

很显然_a$4409是在程序栈开始位置定义,而不是书中提到的”每次进入block时定义”。

http://sunxiunan.com/?p=1780
作者: hellioncu    时间: 2010-12-20 17:07
确实是这样的,如果是class变量,一开始的地方会调用构造,离开时析构,但栈中的空间还是在的
作者: sagasw    时间: 2010-12-20 17:13
确实是这样的,如果是class变量,一开始的地方会调用构造,离开时析构,但栈中的空间还是在的
hellioncu 发表于 2010-12-20 17:07


C语言哪来的Class?
作者: wb112200    时间: 2010-12-20 20:41
c语言中 变量的作用域是函数 c++中是离变量最近的一个block{} 吧..
作者: wb112200    时间: 2010-12-20 20:51
刚测试了下 在Lnix 下 C和C++ 中 局部变量的作用域是相同的 上边的回答有问题 抱歉!~~~
作者: pmerofc    时间: 2010-12-20 20:58
提示: 作者被禁止或删除 内容自动屏蔽
作者: OwnWaterloo    时间: 2010-12-20 21:55
本帖最后由 OwnWaterloo 于 2010-12-20 22:08 编辑

同6楼。
无论是"用汇编解释C语言", 还是用"仅用C标准解释问题", 都是片面的。

不能将某个编译器的实现当作是C语言,因为"gcc没有警告"这样的理由是站不住脚的。

例如, 从lz的汇编代码可以看出: 在lz使用的机器上, 指针是32位整数。但C语言的指针绝对不是32位整数。

那C语言指针究竟是什么?
其实绝大多数时候, 并不需要了解指针的实际实现方式, 只需要了解指针抽象出的概念就可以编程
这样既可以避免研究繁琐的汇编代码, 还可以避免无意间写出平台相关代码。


再举一例, 不能因为看到一匹马的颜色, 就说所有马都是这个颜色。
大多数时候, 只需要将马作为交通工具, 颜色并不重要。



回到书中的例子, 书中的说法(我猜测)是为了避免出现这样的代码:

  1. T* p;
  2. {
  3.       T v;
  4.       p = &v;
  5. }
  6. *p;
复制代码
除开极端情况 —— 无论编译器如何实现栈空间的分配, 无论在block结束后, 该空间是否真的被回 —— 应该认为上述代码是错误的。
因为C语言只保证automatic对象在所属block结束前有效



最后, 追根究底一下, 是否所有的局部automatic变量都在函数开始处分配?
显然不是。

举例: c99支持VLA:

  1. void f(void* p);

  2. void g(int x)
  3. {
  4.       if (x)
  5.       {
  6.             char buf[x];
  7.             f(buf);
  8.       }
  9. }
复制代码
gcc -O2 -S -masm=intel  -std=c99

_g:
        push    ebp
        mov     ebp, esp
        push    ebx
        sub     esp, 4

        mov     eax, DWORD PTR [ebp+8]
        test    eax, eax
        jne     L4

        mov     ebx, DWORD PTR [ebp-4]
        leave
        ret
        .p2align 4,,7
L4:

        add     eax, 30
        mov     ebx, esp
        and     eax, -16
        call    __alloca

        lea     eax, [esp+19]
        and     eax, -16
        mov     DWORD PTR [esp], eax
        call    _f
        mov     esp, ebx
        mov     ebx, DWORD PTR [ebp-4]
        leave
        ret

显然, buf是在进入block后分配的。


我记得该书主要关注c89。
那退回c89, 也是存在这样一种情况:

  1. #ifndef S1
  2. #define S1 1212
  3. #endif

  4. #ifndef S2
  5. #define S2 326
  6. #endif

  7. void f(void* p);

  8. void g(int x, int y)
  9. {
  10.       if (x)
  11.       {
  12.             char xx[S1];
  13.             f(xx);
  14.       }
  15.       if (y)
  16.       {
  17.             char yy[S2];
  18.             f(yy);
  19.       }
  20. }
复制代码
lz可以试试不同的S1, S2组合生成何种代码。

我列出一种情况:
cl /O1 /FAs /c /GS- /DS1=1212 /DS2=1212

得到的汇编代码:
;        COMDAT _g
_TEXT        SEGMENT
_yy$595 = -1212                                                ; size = 1212
_xx$593 = -1212                                                ; size = 1212

...

可以看出, xx和yy的空间是复用的。
也就是说, 在离开xx的block之后, 空间不再属于xx, 随后将用作yy
是否可以认为xx的空间已被回收挪为它用


总结, 排除极端情况下, 用汇编去解释C语言是费力不讨好的。
如果能在C语言的抽象层次上完成工作, 将automatic变量的空间认作 (尽管它可能是, 也可能不是) 在block结束后回收, 并不会有什么坏处, 还可以编写编译器无关代码。
即使是高手, 也应该尽可能避免写出编译器相关的代码, 更别说面向初学者的书籍。
作者: sagasw    时间: 2010-12-20 23:08
回复 7# OwnWaterloo


gcc我不熟悉,但是对于cl,发现你用了一个很有趣的优化参数O1
现在机器上没有vc环境,明天再试。

另外,我同意汇编并不等同于C,因为汇编相当于实现相关。
但是,还是觉得很多情况下,如果不看汇编,光靠描述说明,很难真正理解,
用汇编去解释部分问题,是可以加深对概念的了解。

非常感谢指出文章的不足,我会针对gcc以及vc各种变化对文章加以补充。
作者: KBTiller    时间: 2010-12-20 23:15
谢谢几位网友发表见解
事实上
auto类别的局部变量的含义是在用到这个局部变量时,“自动”地为它找个存储空间,离开它的作用范围后,它所占据的空间自动地还给计算机。这里所谓的不再用到是指,这个局部变量所在的代码模块已经被执行完了。当程序再次开始执行auto类别的局部变量所在的代码模块中的代码时,会再次为这个变量寻找一个存储空间,基本上这个空间和上一次它所占据的空间是不一样的,即使一样也毫无意义,因为原来所占据的内存空间既然还给了操作系统,那么里面的内容——原来的值,可能早就面目全非了。

的文字和表达上确实有问题
我考虑换种表达方法

希望几位网友给些建议
作者: KBTiller    时间: 2010-12-20 23:23
我刚刚想到了这样一个有点极端的例子

  1. {
  2. auto type v;
  3. ……
  4. goto L1 ;
  5. ……
  6. L2 :……
  7. ……
  8. }
  9. L1 :……
  10. ……
  11. goto L2 ;
  12. ……
复制代码


我那段文字似乎很难解释这个
作者: sagasw    时间: 2010-12-21 09:00
回复 7# OwnWaterloo

果然如我所料,如果没有O1这个编译选项,VC++2010是不会压缩空间的。
如果加了O1这个"mini space"优化选项,出来的结果也不是OwnWaterloo提到的,临时变量都没有了,优化得很彻底。
我用的是VC2010,过早的优化果然罪恶!(开个玩笑)

无论如何,还是要谢谢OwnWaterloo的说法:勿以汇编释语言。
其实我也同意这个看法,所以尽量以K&R来作为解读文本。
但是对于一些实现相关的内容,如果还有通过自身的解释或者对标准的注解来做说明,那是非常费劲的。
《狂人C》中对++前后缀方式的说明就是如此,尽管键盘农夫试图把++解释的更为通俗易懂深入浅出,但是我看了还是一头雾水。
而当我看过++代码生成的“无优化VC++2010特别为了注释”版本以后,就恍然大悟,源码之下别无秘密。
而且,如果没有我用汇编展示的这个例子,OwnWaterloo大侠也不会出手解释,从这点来讲,也是抛砖引玉。

当然也可以说我这是一种误读,太细化,系统及平台依赖太强,
但是我感觉用汇编来解读部分C语言实现相关的细节,在现阶段对我而言是有帮助的,那就可以了。

int _tmain(int argc, _TCHAR* argv[])
{
        int a = 0;
        do
        {
                char buf[1024];
                buf[0] = 1;
        }while (0);

        a = 3;

        do
        {
                char buf[1024];
                buf[0] = 93;
        }while(0);

        return 0;
}

ASM输出结果为:

_TEXT        SEGMENT
_buf$4413 = -2076                                        ; size = 1024
_buf$4409 = -1044                                        ; size = 1024
_a$ = -12                                                ; size = 4
__$ArrayPad$ = -4                                        ; size = 4
_argc$ = 8                                                ; size = 4
_argv$ = 12                                                ; size = 4
_wmain        PROC                                                ; COMDAT

; 7    : {

        push        ebp
        mov        ebp, esp
        sub        esp, 2272                                ; 000008e0H
        push        ebx
        push        esi
        push        edi
        lea        edi, DWORD PTR [ebp-2272]
        mov        ecx, 568                                ; 00000238H
        mov        eax, -858993460                                ; ccccccccH
        rep stosd
        mov        eax, DWORD PTR ___security_cookie
        xor        eax, ebp
        mov        DWORD PTR __$ArrayPad$[ebp], eax

; 8    :         int a = 0;

        mov        DWORD PTR _a$[ebp], 0
$LN6@wmain:

; 9    :
; 10   :         do
; 11   :         {
; 12   :                 char buf[1024];
; 13   :                 buf[0] = 1;

        mov        BYTE PTR _buf$4409[ebp], 1

; 14   :         }while (0);

        xor        eax, eax
        jne        SHORT $LN6@wmain

; 15   :
; 16   :         a = 3;

        mov        DWORD PTR _a$[ebp], 3
$LN3@wmain:

; 17   :
; 18   :         do
; 19   :         {
; 20   :                 char buf[1024];
; 21   :                 buf[0] = 93;

        mov        BYTE PTR _buf$4413[ebp], 93                ; 0000005dH

; 22   :         }while(0);
作者: KBTiller    时间: 2010-12-21 10:00
回复  OwnWaterloo
《狂人C》中对++前后缀方式的说明就是如此,尽管键盘农夫试图把++解释的更为通俗易懂深入浅出,但是我看了还是一头雾水。
sagasw 发表于 2010-12-21 09:00


sagasw网友提到的++部分
http://bbs.chinaunix.net/thread-1830222-17-1.html
敬请各位方家指正
作者: sagasw    时间: 2010-12-21 10:23
在这里copy一下,原帖也做了回复:

    我对这段解释的看法是,为了解释++,键盘农夫不得不在一开始就引入更为难懂的概念“左值”“右值”(我对书中这个概念几乎都是跳过),
第一点,C语言中(至少是在K&R中)我没有看到右值的提法,RValue其实是从C++跑来的。
我在今年五月份还试图写博客来解释这个问题,http://sunxiunan.com/?p=1628 除了堆砌一些专家的提法,感觉解释的很不成功。

    用汇编语言解释是很讨巧的一种方式,但是作为教科书可能没法使用,因为不可移植以及太强的平台、软件依赖。

    但是我也不赞同使用用另一个更复杂的概念来解释,这只会让读者更浆糊。如果两者选择,我倾向于使用汇编。

    前缀型++和后缀型++相对在C语言中的应用并不广泛,如果不在表达式中使用,它们基本上是一样的。而我个人也不建议++混在在其他表达式中,比如函数调用作为参数传入也好,作为赋值表达式出现也罢,都有极强的破坏性和副作用,最好的规则就是,++只要单独运行,无论前后。
作者: KBTiller    时间: 2010-12-21 11:54
在这里copy一下,原帖也做了回复:

    我在今年五月份还试图写博客来解释这个问题,http://sunxiunan.com/?p=1628 除了堆砌一些专家的提法,感觉解释的很不成功。
sagasw 发表于 2010-12-21 10:23

那个帖子我仔细看过
(怯怯地表示)
许多专家的提法
我感到也很难苟同
作者: OwnWaterloo    时间: 2010-12-21 11:58
回复 8# sagasw

—— 关于gcc

其实我也是从vc开始学的…… 至今都不习惯at&t的汇编格式。
但我这里支持c99的编译器就只有它了……


—— 关于O1

其实, 对这种"无法证明, 但只需要一个反例即可证伪" 的命题, 明智的做法是避实就虚。
只要能做到不依赖这种命题的真伪也能编写出正确高效的代码, 那命题的证明可以先抛到一边, 编程才是最终目的。

只是呢, 在CU上, 如果仅纯理论的分析, 不拿出点实际的东西(比如, 让新手侧目、 让高手认可的汇编代码) 可能会让人认为仅仅是在"耍嘴皮子"而已。
所以我必须费点力气找几个反例出来。
既然是找反例, 自然也就不择手段了……



—— 关于汇编

仅从C语言的语法语意去理解确实有些抽象。
需要一些例子( C语言的小程序, 或者汇编代码)来帮助理解。

但需要注意的是, 小程序的结果、 产生的汇编代码, 是C语言的实现, 甚至可能是若干种可能实现的其中一种。
它们是用来协助理解C语言, 但不是C语言的唯一解释。
最好能摸清这些结果、这些汇编代码中哪些行为是C语言有保证的, 并尽可能利用这些部分来编程。

这可以避免很多类似: "我的程序在linux/ gcc/ 未优化/ gcc低版本/ 去掉free 下可以工作, 怎么在windows/ vc/ 高优化/ gcc高版本/ 添加free就崩了啊" 的问题。
编程时依赖的细节越少, 当这些细节发生变化时受到的影响也越少。
作者: OwnWaterloo    时间: 2010-12-21 12:03
回复 11# sagasw

这段代码给编译器的上下文太多了。

人肉就能分析出它等效于 int main(void) { return 0; }
而对一些死板的优化工作, 编译器会做得更好。
vc高优化下, 这代码主体应该就只有 xor eax eax。


7楼的c89的代码中, 让x和y作为参数是为了避免编译器得知它们的值, 让block是否进入是未知的。
而f函数只声明不定义, 编译器不知道它究竟会做什么, 就不能随意省略两个数组。
在 (vc6, vc8, vc9, vc10)×(O1, O2, Ox)的组合下都复用了空间。
在(vc6, vc8, vc9, vc10)×(Od)的组合下都没有复用空间。
作者: OwnWaterloo    时间: 2010-12-21 12:06
标题: 关于前缀、 后缀
为神马需要汇编啊……
为神马需要左值右值啊……
我记得学到这里的时候, 很容易理解啊……
作者: OwnWaterloo    时间: 2010-12-21 12:16
本帖最后由 OwnWaterloo 于 2010-12-21 12:17 编辑

回复 10# KBTiller

goto, 这么不和谐……

貌似解释很容易……  貌似, 在C语言里, 进入一个block的方式不仅仅是从第1个语句开始……

  1. void f(void* p);

  2. void g(int x, int y, int z)
  3. {
  4.       if (x)
  5.       {
  6.             char buf[1212] = {0};
  7.             enter:
  8.             f(buf);
  9.       }

  10.       if (y)
  11.       {
  12.             y = 0;
  13.             goto enter;
  14.       }

  15.       switch (z)
  16.       {
  17.             char buf[326] = {0};
  18.       case 0:
  19.             f(buf+19);
  20.             break;
  21.       case 1:
  22.             f(buf+86);
  23.             break;
  24.       }
  25. }
复制代码
从第2个if跳到第1个if中, 从switch进入两个case中, 貌似都算进入一个block。
只是它们只有空间, 没有机会得到初始化了 ( 若x为真, 执行第1个if时是会得到初始化的)。

而在C++中, 这种跳过初始化的局部跳转语句就非法了。


还有一个更邪恶的setjmp/ longjmp, 它们的使用比goto/ switch有更多限制, 应该也能解释。


平时用得不多, 拿不准, 期待专家考察。
作者: KBTiller    时间: 2010-12-21 12:23
本帖最后由 KBTiller 于 2010-12-21 12:33 编辑

回复 18# OwnWaterloo


    我懊悔的是用了“离开”这个词
    下面的东西无法自圆其说


{
  ……
  function();
  ……
}

作者: KBTiller    时间: 2010-12-21 12:34
回复  KBTiller
而在C++中, 这种跳过初始化的局部跳转语句就非法了。
OwnWaterloo 发表于 2010-12-21 12:16

C++有不少东西都比较好
函数原型我觉得也很好
作者: OwnWaterloo    时间: 2010-12-21 12:47
回复 19# KBTiller

同理, 这个也要看"离开"是怎么定义了。

{
      ...
      f(); // 进入f后, 程序的执行是挂在这里的
      ...
}

不过又要解释"挂"…………
不容易啊不容易……


回想一下, 我记得初学C的时候没仔细考虑过变量lifetime的问题。

一些玩具代码要无意间能写出这样的奇葩的代码:
T* p;
{
      T v;
      p = &v;
}
*p;

恐怕需要一点运气……


lifetime是到了C++的时候才开始重视的。
作者: KBTiller    时间: 2010-12-21 12:58
回复  KBTiller

同理, 这个也要看"离开"是怎么定义了。

{
      ...
      f(); // 进入f后, 程 ...
OwnWaterloo 发表于 2010-12-21 12:47


   是的。但容易让别人尤其是初学者误解总是不够好




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2