在C语言中,局部变量的作用域和生命周期是什么?

7
我想了解以下两个C程序之间的区别。
第一个程序:
void main()
{
    int *a;
    {
        int b = 10;
        a=&b;
    }
    printf("%d\n", *a);
}

第二个程序:
void main()
{
    int *a;
    a = foo();
    printf("%d\n", *a);
}

int* foo()
{
    int b = 10;
    return &b;
}

在这两种情况下,本地变量(b)的地址被返回并赋值给a。我知道当b超出范围时,指针a所指向的内存不应该被访问。然而,在编译以上两个程序时,只有第二个程序会收到以下警告:
“warning C4172: returning address of local variable or temporary”
为什么第一个程序没有类似的警告?

1
首先,你没有返回值。编译器不知道指针是否超出了作用域(它没有测试这种情况)。 - Alexis Wilke
3
这两种情况都是未定义行为。当变量超出其作用域时,您不能拥有指向该变量的指针。发生这种情况的方式和时间并不重要。 - Some programmer dude
可能是这个SO问题的重复。 - Jabberwocky
我认为编译器应该在第一种情况下发出警告。编译器生成的代码在函数开头为所有本地变量(包括嵌套变量)分配堆栈空间,并在结束时释放它。然而,构造函数/析构函数按照定义精确调用:当执行进入/退出嵌套块时。因此,在您的情况下,b仍然存在,但如果它是类实例,则其析构函数已经被调用。 - Alex F
2
这些是C++程序,而不是C程序。你不能只使用“一点点C++”就得到一个C程序。 - unwind
显示剩余3条评论
6个回答

5

正如你所知,b在每个实例中都超出了作用域,并且访问该内存是非法的。我只是在阐述为什么只有一个情况会抛出警告而其他情况不会。

在第二种情况中,你正在返回存储在堆栈内存上的变量的地址。因此,编译器检测到了这个问题,并向你发出警告。

然而,在第一种情况下,编译器会跳过检查,因为它看到一个有效的初始化地址被分配给了a。编译器在很多情况下依赖程序员的智力。

类似的例子来说明你的第一种情况可以是,

char temp[3] ;
strcpy( temp, "abc" ) ;

编译器看到 temp 有一个内存空间,但这取决于编码人员将在该内存区域中复制多少个char字符。

2

你的foo()函数存在未定义行为,因为它返回指向不再使用且很快将被覆盖的堆栈内存的指针。

这被称为“b已经超出作用域”。虽然内存仍然存在,而且可能到目前为止还没有改变,但这并不保证。

对于你的第一个代码也是同样的情况,因为b的作用域在声明b的括号结束时结束。

编辑: 你没有在第一个代码中收到警告,因为你没有返回任何东西。警告明确指出了return。由于编译器可以一次性分配整个函数的堆栈空间,包括所有子块,因此它可能保证该值不会被覆盖。但尽管如此,这仍然是未定义的行为。

如果你使用更高的警告级别,可能会收到其他警告。


0
在第一个代码片段中,即使您明确添加括号,您使用的堆栈空间仍在同一区域; 代码中没有跳转或返回,因此代码仍然使用堆栈中连续的内存地址。发生了几件事情:
  1. 即使您删除代码块,编译器也不会将其他变量推送到堆栈上。
  2. 您只限制了变量b在该代码块中的可见性;这与在开头声明它并仅在完全相同的位置使用它一次的情况几乎相同,但没有{...}
  3. b的值很可能保存在寄存器中,因此稍后打印它不会有问题-但这是推测。
对于第二个代码片段,函数调用意味着跳转和返回,这意味着:
  1. 将当前堆栈指针和上下文推送到堆栈上
  2. 将函数调用的相关值推送到堆栈上
  3. 跳转到函数代码
  4. 执行函数代码
  5. 将堆栈指针恢复为函数调用之前的值
由于堆栈指针已经恢复,因此堆栈上的任何内容尚未丢失,但对堆栈的任何操作都可能覆盖这些值。

我认为很容易看出你为什么只在一个情况下收到警告以及期望的行为是什么...


0
也许这与编译器的实现有关。在第二个程序中,编译器可以识别返回调用是一个警告,因为程序返回了超出范围的变量。我认为使用关于ebp寄存器的信息很容易进行识别。但在第一个程序中,我们的编译器需要更多的工作才能实现它。

0

你们两个程序都调用了未定义的行为。花括号中组合在一起的语句称为块或复合语句。在块中定义的任何变量仅在该块中具有作用域。一旦超出块的作用域,该变量就不存在了,访问它是非法的。

int main(void) {
    int *a;
    {   // block scope starts

        int b = 10;  // b exists in this block only
        a = &b;

    }   // block scope ends

    // *a dereferences memory which is no longer in scope
    // this invokes undefined behaviour

    printf("%d\n", *a);  
}

同样地,函数中定义的自动变量具有函数作用域。一旦函数返回,分配在堆栈上的变量将不再可访问。这解释了您在第二个程序中收到的警告。如果您想从函数返回变量,则应该动态分配它。
int main(void) {
    int *a;
    a = foo();
    printf("%d\n", *a);
}

int *foo(void) {
    int b = 10;  // local variable

    // returning the address of b which no longer exists
    // after the function foo returns

    return &b;  
} 

此外,main 的签名应为以下之一 -

int main(void);
int main(int argc, char *argv[]);

-1
在你的第一个程序中-
变量b是一个块级变量,其可见性仅限于该块内部。 但是,b的生命周期是函数的生命周期,因此它一直存在到main函数退出。 由于b仍然分配了空间,*a打印存储在b中的值,因为a指向b。

错误。https://en.cppreference.com/w/c/language/storage_duration:每个对象都有一个称为*存储期限*的属性,它限制了对象的*生命周期*。在C语言中有四种存储期限: 自动存储期限。当声明对象的块被进入时分配存储空间,并在通过任何方式(goto、return、到达结尾)退出时释放存储空间。其中一个例外是可变长度数组... - imz -- Ivan Zakharyaschev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接