在C语言中,你可以和不能声明新的变量?

89

我听说(可能是从老师那里)应该在程序/函数的开头声明所有变量,而在语句中声明新变量可能会导致问题。

但是当我读 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 规范,这都是合法的。 - Dave
6
问题在于你一直在阅读已经过时的 K&R 书籍。 - Lundin
2
@Lundin 有没有适当的替代 K&R 的书籍?ANSI C 版本之后就没有了,而且这本书的读者可以清楚地看到它所参考的标准。 - Brandin
7个回答

144

我也经常听说将变量放在函数顶部是最好的做法,但我强烈反对。我更喜欢将变量限定在最小的范围内,这样它们就不容易被误用,而且在程序的每一行中我需要占用的脑力空间更少。

虽然所有版本的C都允许词法块级别的作用域,在哪里声明变量取决于您所针对的C标准的版本:

C99及以后或C++

现代C编译器(如gcc和clang)支持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语言中,你可以将带有大括号包围的块放置在任何语句可以放置的位置(不仅仅是在像 iffor 这样的语句之后),并且你可以使用这种方法来引入新的变量作用域。下面是先前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
5
请记住,想要节省内存的人通常处理嵌入式系统,在这种情况下,由于认证和/或工具链方面的原因,人们可能被迫坚持更低的优化级别和/或更旧的编译器版本。 - class stacker
2
我不知道你从哪里得到的这个想法,即在作用域中间声明变量只是一种“将声明有效地移动到顶部”的“hack”。事实并非如此,如果你尝试在一行中使用变量并在下一行中声明它,你将会得到一个“变量未声明”的编译错误。 - hugomg

2

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

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

一般来说,将变量保留在自己的{}作用域中可以加快编译速度,并通过减少编译器必须遍历的图形大小来改善生成的代码,以确定哪些变量在哪里以及如何使用。在某些情况下(特别是涉及goto时),编译器可能会错过变量不再使用的事实,除非您明确告诉编译器其使用范围。编译器可以在搜索程序图形时设置时间/深度限制。
编译器可以将声明在附近的变量放置到同一个堆栈区域中,这意味着加载一个变量将预加载所有其他变量到缓存中。同样,声明变量register可以给编译器一个提示,即您希望尽一切可能避免该变量被溢出到堆栈上。
严格遵循ANSI标准符合C89要求,在声明之前需要显式添加{

变量的声明(包括初始化)可以跟随引入任何复合语句的左括号,而不仅仅是开始函数的左括号。

(K&R (2e),第4章(“函数和程序结构”),p84;原文强调),C++和GCC引入的扩展允许在函数体内进一步声明变量,这使得gotocase语句更加复杂。C++还允许在for循环初始化中声明内容,这仅限于循环范围内。最后,对于另一个阅读您的代码的人来说,当他看到函数顶部布满了半百个变量声明时,而不是将它们局部化到使用位置,这会让人感到压抑。这也使得注释掉它们的使用更容易。简而言之:使用{}明确地说明变量的作用域可以帮助编译器和人类读者。

2

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。可能会认为这会导致错误,但实际上不会,因为这两个i变量位于不同的作用域内。当您查看此函数的输出时,可以更清楚地看到这一点。

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

首先,我们将20和30分别赋值给变量ij。然后,在花括号内,我们将88和99赋值给变量。那么,为什么j保留了它的值,而i又回到了20呢?这是因为有两个不同的i变量。
在内部花括号之间,值为20的i变量被隐藏且无法访问,但由于我们没有声明一个新的j,我们仍在使用外部范围的j。当我们离开内部花括号时,持有值为88的i消失了,我们再次可以访问持有值为20的i
有时这种行为是好的,有时可能不是,但应该清楚的是,如果您不加区分地使用C的这个特性,就会使您的代码变得混乱和难以理解。

33
由于你在两个变量中使用了完全相同的名称,而不是因为你在函数开始处声明变量,导致你的代码难以阅读。这是两个不同的问题。我强烈反对声明变量在其他位置会使你的代码难以阅读的说法,我认为相反的情况才是真实的。当编写代码时,如果你在将要使用的地方附近声明变量,遵循时间和空间局部性原则,在阅读时,你将能够非常容易地确定变量的作用、存在原因以及如何使用它。 - Havok
3
通常情况下,我会在代码块的开头声明所有被多次使用的变量。对于仅用于某个本地计算的临时变量,我会倾向于在使用它的地方进行声明,因为它在该代码片段之外没有任何意义。 - Lundin
7
在需要的地方声明变量,不一定要在块的顶部,通常可以让你初始化它。你可以写成{ /* 计算... */ const int n = some_value; },而不是{ int n; /* 计算... */ n = some_value; } - Keith Thompson
@Havok,“你为两个变量使用了完全相同的名称”,也被称为“shadowed variables”(man gcc然后搜索-Wshadow)。所以我同意这里演示了Shadowed variables。 - Trevor Boyd Smith

1
如果你的编译器允许,那么你可以在任何地方声明变量。事实上,在函数中使用变量时声明变量比在函数顶部更易读(在我看来),因为这样更容易发现错误,例如忘记初始化变量或意外隐藏变量。

1
一篇文章展示了以下代码:
//C99
printf("%d", 17);
int z=42;
printf("%d", z);

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

我认为这里的含义是它们是等价的。但实际上并不是这样。如果将 int z 放置在此代码片段底部,它会导致与第一个 z 定义的重定义错误,但不会影响第二个。

然而,多行连续的

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

尽管它们都声明了一个 int i 变量,编译完全没有问题。这显示了 C99 规则的微妙之处。

就个人而言,我非常反对这个 C99 特性。

这个特性缩小变量的作用域的论点是错误的,正如这些示例所示。根据新规则,在扫描整个块之前,您不能安全地声明变量,而在以前,您只需要理解每个块开头正在发生什么。


1
大多数愿意负责跟踪他们的代码的人都对“随处声明”非常欢迎,因为它带来了很多提高可读性的好处。而且,“for”是一个无关紧要的比较。 - underscore_d
这并不像你说的那么复杂。变量的作用域从声明开始,到下一个 } 结束。就是这样!在第一个示例中,如果您想在 printf 之后添加更多使用 z 的行,则应在代码块内部执行,而不是在外部执行。您绝对不需要“扫描整个块”以查看是否可以定义新变量。我必须承认,第一个片段有点人为的例子,因为它会产生额外的缩进,所以我倾向于避免使用它。但是,{int i; for(..){ ... }} 模式是我经常使用的东西。 - hugomg
你的陈述是不准确的,因为在第二段代码片段(ANSI C)中,你甚至不能在 ANSI C 块的底部放置 int z 的第二个声明,因为 ANSI C 只允许你在顶部放置变量声明。因此错误是不同的,但结果是相同的。在这两个代码片段的底部都无法放置 int z。 - RTHarston
另外,使用多行for循环有什么问题吗?int i只存在于该for循环的块中,因此不会泄漏和重复定义int i。 - RTHarston

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重新定位到函数定义区域。 编译器对块内整数的定义没有意见。


你能添加一个基本的函数定义吗?当我阅读你的解决方案时,我认为你的代码是整个程序(即:main())。我理解其他答案中的代码片段并不都这样做,但在你的情况下,缺少上下文(即:块周围的其他代码)会妨碍理解。 - Lover of Structure

-1
根据K&R的《C程序设计语言》 -
在C语言中,所有变量在使用之前必须声明,通常是在函数的开头,在任何可执行语句之前。
这里你可以看到通常这个词,它并不是必须的。

1
现在的C语言不再是K&R风格的了 - 目前很少有代码能够使用古老的K&R编译器进行编译,那么为什么要将其作为你的参考呢? - Toby Speight
清晰度和解释能力非常棒。我觉得从原始开发者那里学习是很好的选择。它虽然古老,但对于初学者来说非常不错。 - Gagandeep kaur
1
你引用的语句来自于K&R(第二版)第1章(“教程介绍”),第9页。它已被K&R(第二版)第4章(“函数和程序结构”)第84页的其他文本所取代:“变量的声明(包括初始化)可以跟随引入任何复合语句的左花括号,而不仅仅是开始一个函数的那个花括号。” - Lover of Structure

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