在哪里可以和不能在C中声明新变量?


76

我(可能是从老师那里)听说,应该在程序/函数之上声明所有变量,并且在声明中声明新变量可能会引起问题。

但是后来我在阅读K&R时碰到了这句话:“变量的声明(包括初始化)可能会在左括号后面加上任何复合语句,而不仅仅是开始一个函数的语句”。他举了一个例子:

if (n > 0){
    int i;
    for (i=0;i<n;i++)
    ...
}

我对这个概念玩了一点,它甚至适用于数组。例如:

int main(){
    int x = 0 ;

    while (x<10){
        if (x>5){
            int y[x];
            y[0] = 10;
            printf("%d %d\n",y[0],y[4]);
        }
        x++;
    }
}

那么,到底什么时候不允许我声明变量?例如,如果我的变量声明在右括号之后不正确怎么办?像这儿:

int main(){
    int x = 10;

    x++;
    printf("%d\n",x);

    int z = 6;
    printf("%d\n",z);
}

根据程序/机器,这会引起麻烦吗?


5
gcc非常宽松。您正在使用c99可变长度数组和声明。编译时gcc -std=c89 -pedantic,您会大吼大叫。但是,根据c99,所有这些都是洁净的。
戴夫

6
问题是您一直在阅读过时的K&R。
隆丁

1
@Lundin有适合K&R的替代品吗?ANSI C版本之后没有任何内容,并且本书的读者可以清楚地阅读其所指的标准
Brandin

Answers:


125

我也经常听到把变量放在函数顶部是最好的处理方法,但是我非常不同意。我更喜欢将变量限制在尽可能小的范围内,这样变量被滥用的机会就更少了,因此在程序的每一行中填满我的思维空间的东西也就更少了。

尽管所有版本的C都允许词法块作用域,但您可以在其中声明变量取决于目标C语言版本:

从C99开始或C ++

诸如gcc和clang之类的现代C编译器支持C99C11标准,这些标准允许您在可能进行语句的任何地方声明变量。变量的范围从声明的点开始到块的末尾(下一个大括号)。

if( x < 10 ){
   printf("%d", 17);  // z is not in scope in this line
   int z = 42;
   printf("%d", z);   // z is in scope in this line
}

您还可以在for循环初始化程序中声明变量。该变量仅在循环内部存在。

for(int i=0; i<10; i++){
    printf("%d", i);
}

ANSI C(C90)

如果您以较早的ANSI C标准为目标,则仅限于在括号1之后立即声明变量。

但这并不意味着您必须在函数顶部声明所有变量。在C语言中,您可以将大括号分隔的块放在语句可以到达的任何位置(不仅在诸如if或之后for),并且可以使用它引入新的变量作用域。以下是以前的C99示例的ANSI C版本:

if( x < 10 ){
   printf("%d", 17);  // z is not in scope in this line

   {
       int z = 42;
       printf("%d", z);   // z is in scope in this line
   }
}

{int i; for(i=0; i<10; i++){
    printf("%d", i);
}}

1请注意,如果您使用的是gcc,则需要传递该--pedantic标志以使其实际执行C90标准,并抱怨变量声明在错误的位置。如果只使用-std=c90它,它将使gcc接受C90的超集,该超集还允许更灵活的C99变量声明。


1
“变量的作用域从声明的位置开始到块的末尾”-如果有人怀疑,这并不意味着手动/创建一个较窄的块对于使编译器有效地使用堆栈空间是有用的/需要的。我已经看过几次了,这是从错误的说法中得出的错误推断,即C是“便携式汇编程序”。因为(A)变量可能分配在寄存器中,而不是堆栈中,&(B)如果变量在堆栈中,但是编译器可以看到您停止使用它,例如在整个块中使用了10%的代码,可以轻松地将该空间用于其他用途。
underscore_d

3
@underscore_d请记住,想要节省内存的人们经常会处理嵌入式系统,由于认证和/或工具链方面的原因,嵌入式系统不得不要么坚持较低的优化级别和/或使用较旧的编译器版本。
类堆叠器

仅仅因为您在范围的中间声明了一个变量并不会使其范围更短。这样只会更难查看哪些变量在范围内,哪些不在范围内。使作用域更短的原因是使匿名作用域成为一个匿名作用域,而不是在作用域中间进行声明(这只是一种黑客,可以有效地将声明移到顶部,并将分配保持在适当的位置,只会使推理环境变得更加困难)作用域,实际上与在每个作用域中声明了所有变量的乘积的匿名结构同构。
德米特里(Dmitry)

2
我不知道您在哪里知道在作用域中间声明变量只是“有效地将声明移到顶部的黑客”的想法。事实并非如此,如果您尝试在一行中使用变量并在下一行中对其进行声明,则会出现“未声明变量”的编译错误。
hugomg

3

missingno介绍了ANSI C所允许的内容,但是他没有说明为什么您的老师告诉您在函数顶部声明变量。在奇怪的地方声明变量会使您的代码难以阅读,并可能导致错误。

以下面的代码为例。

#include <stdio.h>

int main() {
    int i, j;
    i = 20;
    j = 30;

    printf("(1) i: %d, j: %d\n", i, j);

    {
        int i;
        i = 88;
        j = 99;
        printf("(2) i: %d, j: %d\n", i, j);
    }

    printf("(3) i: %d, j: %d\n", i, j);

    return 0;
}

如您所见,我已经声明了i两次。好吧,更准确地说,我已经声明了两个变量,两个变量都带有name i。您可能会认为这会导致错误,但不会导致错误,因为两个i变量的作用域不同。当您查看此函数的输出时,可以更清楚地看到这一点。

(1) i: 20, j: 30
(2) i: 88, j: 99
(3) i: 20, j: 99

首先,我们分别为i和分配20和30 j。然后,在花括号内,我们指定88和99。那么,为什么要j保留其值,但是i又回到20?这是由于两个不同的i变量。

在花括号的内部集合之间,i值20的变量是隐藏的且不可访问,但是由于我们尚未声明new j,因此我们仍在使用j外部作用域。当我们离开内部一组花括号时,i保持值88消失,我们再次可以访问i具有值20的。

有时候,这种行为是件好事,而有时却不是。但是应该清楚的是,如果您不加选择地使用C的此功能,则确实会使您的代码混乱且难以理解。


29
您使代码难以阅读,因为您对两个变量使用了完全相同的名称,而不是因为您不在函数的开头声明了变量。那是两个不同的问题。我强烈不同意在其他地方声明变量会使您的代码难以阅读的说法,我认为相反的说法是正确的。在编写代码时,如果按照时间和空间局部性原则在变量即将使用时声明它,那么在阅读时,您将能够很容易地识别出它的作用,原因以及使用方式。
2014年

3
根据经验,我在该块的开头声明了该块中多次使用的所有变量。一些临时变量仅用于某个地方的局部计算,我倾向于声明使用该变量的位置,因为在该代码片段之外没有意义。
隆丁

5
通常,您可以在需要的地方(不一定在块的顶部)声明变量,以便对其进行初始化。而不是{ int n; /* computations ... */ n = some_value; }你可以写{ /* computations ... */ const int n = some_value; }
基思·汤普森

@Havok“您为两个变量使用了相同的名称”,也称为“阴影变量”(man gcc然后搜索-Wshadow)。所以我同意阴影变量在这里得到了展示。
Trevor Boyd Smith,

1

如果编译器允许,则可以在任何位置声明。实际上,当您在使用位置而不是函数顶部声明变量时,代码更具可读性(IMHO),因为这样可以更轻松地发现错误,例如忘记初始化变量或意外隐藏变量。


0

帖子显示以下代码:

//C99
printf("%d", 17);
int z=42;
printf("%d", z);

//ANSI C
printf("%d", 17);
{
    int z=42;
    printf("%d", z);
}

我认为这意味着它们是等效的。他们不是。如果将int z放在此代码段的底部,则会对第一个z定义(而不对第二个z定义)引起重新定义错误。

但是,以下几行:

//C99
for(int i=0; i<10; i++){}

确实有效。显示了此C99规则的微妙之处。

就个人而言,我热情地避免使用此C99功能。

如这些示例所示,它缩小变量范围的论点为false。根据新规则,在扫描完整个块之前,您不能安全地声明变量,而以前,您只需要了解每个块开头的内容即可。


1
愿意承担起跟踪代码责任的其他大多数人都欢迎他们张开双臂“在任何地方声明”,因为它为可读性带来了许多好处。而且for是一个不相干的比较
underscore_d

它没有您说的那么复杂。变量的作用域始于其声明,然后终止于next }。而已!在第一个示例中,如果要添加更多z在printf之后使用的行,则可以在代码块内而不是在代码块外执行。您绝对不需要“扫描整个块”以查看是否可以定义新变量。我确实不得不承认,第一个代码片段只是一个虚构的示例,由于它会产生额外的缩进,我倾向于避免使用它。但是,这种{int i; for(..){ ... }}模式是我一直都在做的事情。
hugomg

您的声明是不准确的,因为在第二个代码片段(ANSI C)中,您甚至不能在ANSI C块的底部放置第二个int z声明,因为ANSI C仅允许您将变量声明放在顶部。因此错误是不同的,但是结果是相同的。您不能将int z放在这两个代码段的底部。
RTHarston

另外,for循环有多行是什么问题?int i仅存在于for循环的块内,因此没有泄漏,也没有重复的int i定义。
RTHarston

0

根据K&R的C编程语言-

在C语言中,必须在使用所有变量之前先声明它们,通常在函数的开头声明任何可执行语句之前。

在这里,您通常可以看到单词不是必须的。


如今,并不是所有的C都是K&R-用古老的K&R编译器编译的当前代码很少,那么为什么要使用它作为参考?
Toby Speight

清晰度及其解释能力非常棒。我认为向原始开发人员学习是一件好事,虽然它很古老,但对初学者来说却很好。
Gagandeep kaur

0

使用clang和gcc时,我遇到了以下主要问题。gcc版本8.2.1 20181011 clang版本6.0.1

  {
    char f1[]="This_is_part1 This_is_part2";
    char f2[64]; char f3[64];
    sscanf(f1,"%s %s",f2,f3);      //split part1 to f2, part2 to f3 
  }

编译器都不喜欢f1,f2或f3放在该块内。我必须将f1,f2,f3定位到函数定义区域。编译器不介意使用该块定义整数。


0

在内部,所有函数局部变量都分配在堆栈上或内部CPU寄存器中,然后,如果编译器损坏或CPU没有足够的寄存器来将生成的机器代码在寄存器和堆栈之间交换(称为寄存器溢出)。保持所有球在空中飞舞。

为了在堆栈上分配内容,CPU有两个特殊的寄存器,一个称为堆栈指针(SP),另一个称为基本指针(BP)或帧指针(表示当前函数作用域本地的堆栈帧)。SP指向堆栈上当前位置的内部,而BP指向工作数据集(在其上方)和函数自变量(在其下方)。调用函数时,它将调用者/父函数的BP推入堆栈(由SP指向),并将当前SP设置为新的BP,然后将SP从寄存器溢出到堆栈的字节数增加,进行计算,并在返回时通过从堆栈中弹出来恢复其父级的BP。

通常,将变量保留在自己的{}-scope内可以通过减小编译器确定用于何处以及如何使用哪些变量的图形的大小来加快编译速度并改善生成的代码。在某些情况下(尤其是在涉及goto的情况下),编译器可能会遗漏不再使用变量的事实,除非您明确告知编译器其使用范围。编译器可能有时间/深度限制来搜索程序图。

编译器可以将声明的变量彼此靠近放置在同一堆栈区域,这意味着加载一个变量会将所有其他变量预加载到缓存中。同样的方式,声明变量register,可以为编译器提供一个提示,您希望避免不惜一切代价将所述变量溢出到堆栈中。

严格C99标准要求显式{声明之前,而延伸部通过C ++和GCC引入允许声明进一步瓦尔到体内,其复杂化gotocase声明声明。C ++还允许在循环初始化中声明内部内容,这仅限于循环范围。

最后但并非最不重要的一点是,对于另一个正在阅读您的代码的人来说,当他看到一个函数的顶部充满了半百个变量声明,而不是将它们本地化在使用位置时,这将是不知所措。这也使注释它们的使用更加容易。

TLDR:{}用于显式声明变量作用域可以同时帮助编译器和读者。


“严格的C99标准要求显式{”是不正确的。我猜你的意思是那里的C89。C99允许在语句后声明。
梅基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.