将变量声明放置在C中


129

我一直以为在C语言中,所有变量都必须在函数开始时声明。我知道C99中的规则与C ++中的规则相同,但是C89 / ANSI C的变量声明放置规则是什么?

以下代码使用gcc -std=c89和成功编译gcc -ansi

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

不应该的声明c,并s导致C89 / ANSI模式错误?


54
请注意:ansi C中的变量不必在函数的开始处声明,而在块的开始处声明。因此,在for循环顶部的char c = ...在ansi C中是完全合法的。但是char * s则不是。
杰森·可可

Answers:


149

它可以成功编译,因为GCC允许将其声明s为GNU扩展,即使它不是C89或ANSI标准的一部分。如果要严格遵守这些标准,则必须通过-pedantic标志。

c开头的声明{ }是C89标准的一部分;该块不一定是一个函数。


41
可能值得注意的是,只有C的声明s才是扩展(从C89的角度来看)。的声明c在C89中完全合法,不需要扩展。
AnT 2010年

7
@AndreyT:是的,在C语言中,变量声明应该是@ 的开头,而不是函数本身;但是人们将块与功能混淆了,因为它是块的主要示例。
legends2k 2012年

1
我以+39票将评论转移到答案中。
MarcH

78

对于C89,必须在作用域块的开头声明所有变量。

因此,您的char c声明是有效的,因为它位于for循环作用域块的顶部。但是,char *s声明应该是一个错误。


2
完全正确。您可以在任何{...}的开头声明变量。
Artelius

5
@Artelius不太正确。只有在花括号是一个块的一部分(如果没有他们是结构或联合声明或初始支撑体系的一部分。)
延斯

仅仅出于学究,至少应根据C标准通知错误的声明。因此,应该是中的错误或警告gcc。也就是说,不要相信程序可以编译就意味着它是合规的。
jinawee

35

由于旧的原始C编译器的限制,将变量声明分组在块的顶部是一种传统。所有现代语言都建议甚至有时在最晚执行局部变量的声明:首先对它们进行初始化。因为这样可以避免错误使用随机值的风险。分开声明和初始化还可能使您无法使用“ const”(或“ final”)。

不幸的是,C ++一直接受旧的,顶部声明的方式来实现与C的向后兼容性(一个C兼容性拖累了许多其他方法...),但是C ++试图摆脱它:

  • C ++引用的设计甚至不允许这样的块分组。
  • 如果您将C ++本地对象的声明和初始化分开,则无需付出额外的构造函数费用。如果no-arg构造函数不存在,那么您甚至都不允许将两者分开!

C99开始沿相同方向移动C。

如果您担心找不到局部变量在哪里声明,那么这意味着您有一个更大的问题:封闭块太长,应该拆分。

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions



另请参见在块顶部强制强制变量声明如何创建安全漏洞:lwn.net/Articles/443037
MarcH 2011年

“不幸的是,C ++一直接受旧的,顶部声明的方式来实现与C的向后兼容性”:恕我直言,这只是干净的方式。其他语言通过始终以0初始化来“解决”此问题。Bzzt,仅在您问我时才掩盖逻辑错误。在很多情况下,无需初始化就需要声明,因为存在多个可能的初始化位置。这就是C ++的RAII确实给人带来极大痛苦的原因-现在,您需要在每个对象中包括一个“有效”未初始化状态,以允许出现这些情况。
乔所以

1
@JoSo:我很困惑,为什么您认为读取未初始化的变量会产生任意影响,而不是让编程错误产生一致的值或确定性的错误,则更容易发现编程错误?请注意,无法保证读取未初始化的存储的行为将与变量可能拥有的任何位模式一致,甚至无法保证此类程序的行为与时间和因果关系的惯常规律一致。给定类似int y; ... if (x) { printf("X was true"); y=23;} return y;...
超级猫

1
@JoSo:对于指针,尤其是在陷阱操作上的实现null,全位零通常是有用的陷阱值。此外,在明确指定变量默认为全零位的语言中,对该值的依赖不是error。编译器尚未对它们的“优化”变得过于古怪,但是编译器作者一直在努力变得越来越聪明。使用故意伪随机变量初始化变量的编译器选项可能对识别故障很有用,但仅保留其最后一个值的存储有时会掩盖故障。
超级猫

22

从可维护性而不是语法的角度来看,至少存在三种思路:

  1. 在函数的开头声明所有变量,以便将它们放在一个位置,您一眼就能看到完整的列表。

  2. 将所有变量声明为尽可能靠近它们首次使用的位置,因此您将知道为什么需要每个变量。

  3. 在最里面的作用域块的开始处声明所有变量,这样它们将尽快超出作用域,并允许编译器优化内存,并告诉您是否在您不希望使用它们的地方使用它们。

我通常更喜欢第一种选择,因为我发现其他选择经常迫使我在代码中寻找声明。预先定义所有变量还使初始化和从调试器监视它们变得更加容易。

有时我会在较小的作用域块中声明变量,但这只是出于一个很好的理由,而我却很少。一个示例可能在后面fork(),以声明仅子进程所需的变量。对我来说,这个视觉指示器可以帮助提醒他们其目的。


27
我使用选项2或3,因此更容易找到变量-因为函数的大小不应该太大,以至于看不到变量声明。
乔纳森·勒夫勒

8
选项3是非发行的,除非您使用70年代的编译器。
edgar.holleis,2010年

15
如果您使用了不错的IDE,则无需进行代码搜寻,因为应该有一个IDE命令来为您查找声明。(Eclipse中的F3)
edgar.holleis,2010年

4
我不明白如何才能确保在选项1中进行初始化,可能有时只能通过调用另一个函数或执行计算来在块的稍后部分获取初始值。
Plumenator

4
@Plumenator:选项1不能确保初始化;我选择在声明时将它们初始化为它们的“正确”值,或者将其设置为可以确保如果它们的设置不正确,则后续代码将中断的东西。我之所以说“选择”,是因为自从我写这篇文章以来,我的喜好已更改为#2,这也许是因为我现在使用Java的人数超过了C的人数,并且因为我拥有更好的开发工具。
亚当·利斯

6

正如其他人所指出的那样,即使在“ C89”模式下,除非您使用“ pedantic”检查,否则GCC在这方面是允许的(可能还有其他编译器,具体取决于调用它们的参数)。老实说,没有太多的理由不学究。高质量的现代代码应该始终在没有警告的情况下进行编译(或者很少有您知道自己在做的特定事情可能会导致编译器怀疑为错误),因此,如果您无法使代码使用笨拙的设置进行编译,则可能需要引起注意。

C89要求在每个范围内的任何其他语句之前声明变量,以后的标准允许声明更接近使用(可以更直观,更有效),尤其是在“ for”循环中同时声明和初始化循环控制变量。


0

如前所述,对此有两种思想流派。

1)因为年份是1987,所以在功能顶部声明所有内容。

2)声明最接近首次使用且范围尽可能小。

我的回答是“都做!” 让我解释:

对于长函数,1)使重构非常困难。如果您在开发人员反对子例程的代码库中工作,则函数开头将有50个变量声明,其中一些可能只是for循环的“ i”函数的底部。

因此,我据此制定了《最高PTSD声明》,并尝试认真地做选择2)。

由于一件事,我回到了选项一:短函数。如果您的函数足够短,那么您将只有很少的局部变量,并且由于该函数很短,因此如果将它们放在函数的顶部,它们仍将接近首次使用。

另外,当您想在顶部声明但尚未进行一些初始化所需的计算时,“声明并设置为NULL”的反模式已解决,因为您需要初始化的内容很可能会作为参数接收。

因此,现在我的想法是,您应该在函数顶部声明并尽可能接近首次使用。所以都!做到这一点的方法是使用划分良好的子例程。

但是,如果您正在使用长函数,则应将最接近第一次使用的东西放到最前面,因为那样提取方法会更容易。

我的食谱是这个。对于所有局部变量,采用变量并将其声明移至底部,进行编译,然后将声明移至编译错误之前。那是第一个用途。对所有局部变量执行此操作。

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

现在,定义一个作用域块,该作用域块在声明之前开始,并移动结束直到程序编译为止

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

这不会编译,因为还有更多使用foo的代码。我们可以注意到编译器能够遍历使用bar的代码,因为它不使用foo。在这一点上,有两种选择。机械的方法是向下移动“}”直到编译,另一种选择是检查代码并确定顺序是否可以更改为:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

如果可以切换顺序,那可能就是您想要的,因为它会缩短临时值的寿命。

还要注意的另一件事是,是否需要在使用它的代码块之间保留foo的值,或者两者是否只是一个不同的foo。例如

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

这些情况比我的程序更需要。开发人员将必须分析代码以确定该做什么。

但是第一步是找到第一个用途。您可以直观地执行此操作,但有时,删除声明,尝试编译并将其放回第一次使用的位置更容易。如果该第一次使用是在if语句内,则将其放在此处并检查其是否可以编译。然后,编译器将确定其他用途。尝试制作一个包含两种用途的作用域块。

机械部分完成后,将更易于分析数据的位置。如果在大型作用域块中使用了变量,请分析情况并查看您是否只是在两个不同的事物上使用相同的变量(例如用于两个for循环的“ i”)。如果用途不相关,则为这些不相关的用途中的每一个创建新的变量。


0

您应该在函数的顶部或“本地”声明所有变量。答案是:

这取决于您使用的系统类型:

1 /嵌入式系统(尤其与飞机或汽车等生活有关):它确实允许您使用动态内存(例如:calloc,malloc,new ...)。想象一下,您正在一个拥有1000名工程师的大型项目中工作。如果他们分配了新的动态内存却忘记了将其删除(当它不再使用时)该怎么办?如果嵌入式系统长时间运行,将导致堆栈溢出,并且软件将损坏。不容易确保质量(最好的方法是禁止动态内存)。

如果飞机在30天内运行并且没有关闭,那么如果软件损坏(飞机仍在空中)会发生什么情况?

2 /其他系统,例如Web,PC(具有较大的存储空间):

您应该声明“本地”变量以优化内存使用。如果这些系统长时间运行并且发生堆栈溢出(因为有人忘记删除动态内存)。只需执行简单的操作即可重置PC:P对生命没有影响


我不确定这是正确的。我想您是说,如果在一个位置声明所有局部变量,则更容易审核内存泄漏?这可能是真的,但我不敢肯定我买它。至于第(2)点,您说在本地声明变量将“优化内存使用率”吗?理论上这是可能的。编译器可以选择在函数过程中调整堆栈框架的大小,以最大程度地减少内存使用量,但是我不知道这样做的原因。实际上,编译器只是将所有“本地”声明转换为“幕后函数启动”。
QuinnFreedman

1 /嵌入式系统有时不允许动态存储,因此如果在函数顶部声明所有变量。构建源代码后,它可以计算堆栈中运行程序所需的字节数。但是对于动态内存,编译器无法做到这一点。
Dang_Ho

2 /如果您在本地声明变量,则该变量仅存在于“ {}”打开/关闭括号内。因此,如果该变量“超出范围”,则编译器可以释放该变量的空间。这可能比在函数顶部声明所有内容更好。
Dang_Ho

我认为您对静态内存与动态内存感到困惑。静态内存分配在堆栈上。在函数中声明的所有变量,无论它们在何处声明,都是静态分配的。动态内存通过分配在堆上malloc()。尽管我从未见过不具备此功能的设备,但是最佳实践是避免在嵌入式系统上进行动态分配(请参见此处)。但这与在函数中声明变量的位置无关。
QuinnFreedman

1
尽管我同意这是一种合理的操作方式,但实际情况并非如此。这是非常类似于您的示例的实际程序集:godbolt.org/z/mLhE9a。如您所见,在第11行,在if语句之外sub rsp, 1008为整个数组分配空间。本作是真实的,并在每一个版本和优化水平我试过了。clanggcc
QuinnFreedman

-1

我将引用gcc版本4.7.0的手册中的一些语句,以提供清晰的解释。

“编译器可以接受几种基本标准,例如'c90'或'c ++ 98',以及这些标准的GNU方言,例如'gnu90'或'gnu ++ 98'。通过指定基本标准,编译器可以将接受该标准之后的所有程序以及使用与该标准不矛盾的GNU扩展的程序,例如,“-std = c90”将关闭与ISO C90不兼容的GCC某些功能,例如asm和typeof关键字,但不其他在ISO C90中没有含义的GNU扩展,例如省略了?:表达式的中间术语。”

我认为您的问题的关键是,即使使用选项“ -std = c89”,gcc为何也不符合C89。我不知道您的gcc版本,但我认为不会有太大的不同。gcc的开发人员告诉我们,选项“ -std = c89”只是意味着与C89相反的扩展名已关闭。因此,它与C89中没有意义的某些扩展无关。不限制变量声明位置的扩展名属于与C89不矛盾的扩展名。

坦白地说,每个人都会以为它首先符合选项“ -std = c89”的C89。但事实并非如此。至于在开始时声明所有变量是好是坏的问题只是一个习惯问题。


符合并不意味着不接受扩展:只要编译器编译有效程序并为其他程序生成任何必需的诊断程序,它便符合。
记得莫妮卡

@Marc Lehmann,是的,当使用“ conform”一词来区分编译器时,您是对的。但是,当使用“符合”一词来描述某些用法时,您可以说“用法不符合标准”。并且所有初学者都认为不符合标准的用法会导致错误。
junwanghe 2012年

@Marc Lehmann,顺便说一句,当gcc看到不符合C89标准的用法时,没有诊断。
junwanghe 2012年

您的答案仍然是错误的,因为声称“ gcc不符合”与“某些用户程序不符合”是不同的。您对conform的使用完全是错误的。此外,当我还是一个初学者时,我并不认为你会这么说,所以这也是错误的。最后,不需要合格的编译器来诊断不合格的代码,实际上,这是不可能实现的。
请记住莫妮卡
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.