在C语言中哪些地方可以声明新变量,哪些地方不能?

svujldwt  于 2023-03-01  发布在  其他
关注(0)|答案(7)|浏览(159)

我听说(可能是从老师那里)应该在程序/函数的顶部声明所有变量,并且在语句中声明新变量可能会导致问题。
但后来我在阅读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);
}

根据程序/机器,这是否会导致问题?

e37o9pze

e37o9pze1#

我也经常听说把变量放在函数的顶部是最好的方法,但我坚决不同意,我更喜欢把变量限制在尽可能小的范围内,这样它们被误用的机会就小了,这样我在程序的每一行都有更少的东西填满我的思维空间。
虽然所有版本的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变量声明.

k5hmc34c

k5hmc34c2#

missingno涵盖了ANSI C允许的内容,但他没有解释为什么老师告诉你要在函数的顶部声明变量,在奇怪的位置声明变量会使代码更难阅读,从而导致bug。
以下面的代码为例。

#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。你可能认为这会导致错误,但事实并非如此,因为这两个i变量在不同的作用域中。当你查看这个函数的输出时,你会更清楚地看到这一点。

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

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

sulc1iza

sulc1iza3#

如果你的编译器允许的话,你可以在任何你想声明的地方声明。事实上,当你在你使用的地方声明变量而不是在函数的顶部声明变量时,代码更可读(恕我直言),因为它更容易发现错误,例如忘记初始化变量或意外隐藏变量。

f4t66c6m

f4t66c6m4#

在内部,函数的所有局部变量都被分配到堆栈或CPU寄存器中,然后如果编译器不好或CPU没有足够的寄存器来保持所有的球在空中杂耍,生成的机器码会在寄存器和堆栈之间交换(称为寄存器溢出)。
CPU有两个特殊的寄存器来分配栈中的东西,一个叫做栈指针(SP)和另一个--基指针(BP)或帧指针(表示当前函数作用域的堆栈帧)。SP指向堆栈上当前位置的内部,而BP指向工作数据集(在它上面)和函数参数当函数被调用时,它将调用者/父函数的BP压入堆栈(由SP指向),并将当前SP设置为新的BP,然后将SP增加从寄存器溢出到堆栈上的字节数,执行计算,并在返回时,通过从堆栈弹出它来恢复其父BP。
一般来说,将变量保留在它们自己的{}-作用域内可以加快编译速度,并通过减少编译器为确定哪些变量在何处以及如何使用而必须遍历的图的大小来改进生成的代码。(特别是当涉及到后藤的时候)编译器可能会忽略变量不再被使用的事实,除非你明确地告诉编译器它的使用范围。编译器可以有时间/深度限制来搜索程序图。
编译器可以将声明的变量放在同一个堆栈区域,这意味着加载一个变量会将所有其他变量预加载到缓存中。同样,声明变量register可以给予编译器一个提示,提示您要不惜一切代价避免所述变量溢出到堆栈上。
严格的C99标准要求在声明前显式声明{,而C和GCC引入的扩展允许在主体中进一步声明变量,这使得gotocase语句复杂化。C还允许在循环初始化中声明填充项,这限制在循环的范围内。
最后但并非最不重要的一点是,对于另一个正在阅读你的代码的人来说,当他看到一个函数的顶部散落着500个变量声明,而不是将它们本地化在它们的使用位置时,他会感到不知所措,这也使得注解它们的使用变得更加容易。
TLDR:使用{}显式声明变量范围对编译器和读者都有帮助。

2exbekwf

2exbekwf5#

下面的代码:

//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特性。
它缩小了变量的作用域,这一论点是假的,正如这些例子所示,在新规则下,只有扫描完整个代码块,你才能安全地声明变量,而以前你只需要了解每个代码块的头部发生了什么。

a8jjtwal

a8jjtwal6#

使用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重新定位到函数定义区。编译器不介意块中的整数定义。

a2mppw5e

a2mppw5e7#

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

相关问题