C 中的变量声明位置

Posted

技术标签:

【中文标题】C 中的变量声明位置【英文标题】:Variable declaration placement in C 【发布时间】:2010-09-22 06:04:17 【问题描述】:

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

以下代码使用gcc -std=c89gcc -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;

cs 的声明不应该导致 C89/ANSI 模式下的错误吗?

【问题讨论】:

请注意:ansi C 中的变量不必在函数的开头声明,而是在块的开头声明。因此,for 循环顶部的 char c = ... 在 ansi C 中是完全合法的。但是,char *s 不会。 【参考方案1】:

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

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

【讨论】:

可能值得注意的是,只有 s 的声明是扩展(从 C89 的角度来看)。 c 的声明在 C89 中是完全合法的,不需要扩展。 @AndreyT:是的,在 C 语言中,变量声明应该是 @ 一个 block 的开头,而不是函数本身;但是人们将块与功能混淆了,因为它是块的主要示例。 我将 +39 票的评论移到了答案中。【参考方案2】:

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

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

【讨论】:

非常正确。您可以在任何 ... 的开头声明变量。 @Artelius 不太正确。仅当花括号是块的一部分时(如果它们是结构或联合声明或大括号初始值设定项的一部分。) 只是为了迂腐,错误的声明至少应该按照C标准通知。所以应该是gcc中的错误或者警告。也就是说,不要相信一个程序可以被编译就意味着它是合规的。 @Jens 如何在结构、联合或大括号初始化程序中声明新变量? “A block”在这里显然代表“一段代码”。 @MarcH Artelius 不是这么说的。他说“在任何 ... 的开头”没有限定。【参考方案3】:

在块的顶部分组变量声明可能是由于旧的原始 C 编译器的限制而遗留下来的。所有现代语言都建议,有时甚至强制在最晚点声明局部变量:它们首先被初始化的地方。因为这消除了错误使用随机值的风险。将声明和初始化分开还可以防止您尽可能使用“const”(或“final”)。

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

C++ 引用的设计甚至不允许这样的块顶部分组。 如果您将 C++ 本地 object 的声明和初始化分开,那么您就无需支付额外构造函数的成本。如果无参数构造函数不存在,那么你甚至不能将两者分开!

C99 开始向同一方向移动 C。

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

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

【讨论】:

learncpp.com/cpp-tutorial/… 另请参阅在块顶部强制变量声明如何造成安全漏洞:lwn.net/Articles/443037 “不幸的是,C++ 一直在接受旧的***声明方式,以实现与 C 的向后兼容性”:恕我直言,这只是一种干净的方式。其他语言通过始终使用 0 初始化来“解决”这个问题。Bzzt,如果你问我,它只会掩盖逻辑错误。在很多情况下,您需要在没有初始化的情况下进行声明,因为有多个可能的初始化位置。这就是为什么 C++ 的 RAII 确实是一个巨大的痛苦 - 现在您需要在每个对象中包含一个“有效的”未初始化状态以允许这些情况。 @JoSo:我很困惑,为什么您认为读取未初始化的变量会产生任意影响,这比让它们产生一致的值或确定性错误更容易检测编程错误?请注意,无法保证读取未初始化存储的行为方式与变量可能持有的任何位模式一致,甚至不能保证此类程序的行为方式与通常的时间和因果律一致。给定类似int y; ... if (x) printf("X was true"); y=23; return y;... @JoSo:对于指针,特别是在捕获null 上的操作的实现上,全位零通常是一个有用的陷阱值。此外,在明确指定变量默认为所有位为零的语言中,依赖该值不是错误。编译器尚未倾向于对其“优化”过于古怪,但编译器编写者一直在努力变得越来越聪明。使用有意的伪随机变量初始化变量的编译器选项可能有助于识别故障,但仅保留存储的最后一个值有时会掩盖故障。【参考方案4】:

从可维护性而非句法的角度来看,至少有三种思路:

    在函数的开头声明所有变量,以便将它们放在一个位置,您将能够一目了然地看到完整的列表。

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

    在最内层作用域块的开头声明所有变量,这样它们就会尽快超出作用域,并允许编译器优化内存并告诉你是否不小心在没有使用它们的地方使用了它们打算。

我通常更喜欢第一个选项,因为我发现其他选项经常迫使我在代码中寻找声明。预先定义所有变量还可以更轻松地从调试器中初始化和观察它们。

我有时会在较小的范围块内声明变量,但这只是出于一个很好的理由,其中我很少。一个例子可能是在fork() 之后,声明只有子进程需要的变量。对我来说,这个视觉指示器有助于提醒他们的目的。

【讨论】:

我使用选项 2 或 3,这样更容易找到变量——因为函数不应该太大以至于看不到变量声明。 选项 3 不是问题,除非您使用 70 年代的编译器。 如果你使用了一个不错的 IDE,你就不需要去寻找代码,因为应该有一个 IDE 命令来为你找到声明。 (在 Eclipse 中按 F3) 我不明白您如何确保在选项 1 中进行初始化,可能有时您只能在块中稍后通过调用另一个函数或执行计算来获取初始值。 @Plumenator:选项 1 不能确保初始化;我选择在声明时将它们初始化为它们的“正确”值,或者如果它们没有正确设置,则可以保证后续代码会中断。我说“选择”是因为自从我写这篇文章后我的偏好已经变成了 #2,也许是因为我现在使用 Java 比 C 更多,而且因为我有更好的开发工具。【参考方案5】:

正如其他人所指出的,GCC 在这方面是允许的(可能还有其他编译器,取决于它们被调用的参数),即使在“C89”模式下,除非您使用“pedantic”检查。老实说,不学究的理由并不多。高质量的现代代码应该总是在没有警告的情况下编译(或者很少有人知道你正在做一些对编译器来说可能是可疑的特定错误),所以如果你不能用迂腐的设置来编译你的代码,它可能需要一些注意。

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

【讨论】:

【参考方案6】:

如前所述,对此有两种观点。

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”)。如果用途不相关,请为这些不相关的用途中的每一个创建新变量。

【讨论】:

【参考方案7】:

我将引用 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”这个词来区分编译器时,你是对的。但是当用“conform”这个词来描述一些用法时,你可以说“A用法不符合标准”。并且所有初学者都认为不符合标准的用法会导致错误。 @Marc Lehmann,顺便说一句,当 gcc 看到不符合 C89 标准的用法时,没有诊断。 您的回答仍然是错误的,因为声称“gcc 不符合”与“某些用户程序不符合”不同。您对conform 的使用完全不正确。此外,当我还是初学者时,我不同意您所说的观点,所以这也是错误的。最后,不要求符合标准的编译器来诊断不符合标准的代码,事实上,这是不可能实现的。【参考方案8】:

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

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

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

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

2/ web、PC等其他系统(内存空间大):

您应该在“本地”声明变量以优化内存使用。如果这些系统运行了很长时间并且发生堆栈溢出(因为有人忘记删除动态内存)。只需做简单的事情来重置电脑:P 它对生活没有影响

【讨论】:

我不确定这是否正确。我猜你是说如果你在一个地方声明所有的局部变量更容易审计内存泄漏? 可能是真的,但我不太确定我会买它。至于第(2)点,您说在本地声明变量会“优化内存使用”吗?这在理论上是可能的。编译器可以选择在函数过程中调整堆栈帧的大小以最小化内存使用,但我不知道有任何这样做的。实际上,编译器只会将所有“本地”声明转换为“幕后函数启动”。 1/ 嵌入式系统有时不允许动态内存,所以如果你在函数顶部声明所有变量。构建源代码时,它可以计算它们在堆栈中运行程序所需的字节数。但是对于动态内存,编译器无法做到这一点。 2/ 如果您在本地声明一个变量,该变量只存在于“”开/关括号内。因此,如果该变量“超出范围”,编译器可以释放该变量的空间。这可能比在函数顶部声明所有内容要好。 我认为您对静态内存和动态内存感到困惑。静态内存是在栈上分配的。在函数中声明的所有变量,无论它们在哪里声明,都是静态分配的。动态内存是在堆上分配的,类似于malloc()。虽然我从未见过没有这种能力的设备,但最好的做法是避免在嵌入式系统上进行动态分配 (see here)。但这与你在函数中声明变量的位置无关。 虽然我同意这是一种合理的操作方式,但实际情况并非如此。这是与您的示例非常相似的实际程序集:godbolt.org/z/mLhE9a。如您所见,在第 11 行,sub rsp, 1008 正在为 if 语句的整个数组 outside 分配空间。在我尝试的每个版本和优化级别上,clanggcc 都是如此。

以上是关于C 中的变量声明位置的主要内容,如果未能解决你的问题,请参考以下文章

C#中在哪里声明全局变量啊,具体位置在哪儿,我是初学者。。。

现代c++实践:变量声明强化,竟然可以在if/switch中定义变量!

c语言怎样声明和定义全局变量

C 中的变量声明

C语言中的32个关键字

C语言中的32个关键字