头文件中的变量声明-静态还是非静态?


91

重构时,#defines我在C ++头文件中遇到了类似于以下内容的声明:

static const unsigned int VAL = 42;
const unsigned int ANOTHER_VAL = 37;

问题是,静电会产生什么区别(如果有)?请注意,由于经典#ifndef HEADER #define HEADER #endif技巧(如果很重要),不可能多次包含标头。

VAL如果标头被多个源文件包含,则静态值是否仅创建一个副本?


Answers:


107

static意味着将为其中VAL包含的每个源文件创建一个副本。但是,这也意味着多个包含不会导致该对象的多个定义VAL在链接时发生冲突。在C中,如果没有,则static需要确保仅定义一个源文件,VAL而其他源文件声明它extern。通常,这是通过在源文件中定义它(可能使用初始化程序)并将extern声明放入头文件中来实现的。

static 全局变量仅在自己的源文件中可见,无论它们是通过include到达还是在主文件中。


编者注:在C ++中,声明中const既没有staticnor也没有extern关键字的对象是隐式的static


我是最后一句话的粉丝,非常有帮助。我没有投票赞成答案,因为42更好。编辑:语法
RealDeal_EE'13年

“静态意味着将为其中包含的每个源文件创建一个VAL副本。” 这似乎意味着如果两个源文件包含头文件,将有VAL的两个副本。我希望这是不对的,并且无论是否有多少文件包含标头,总是会有一个VAL实例。
布伦特212年

4
@ Brent212编译器不知道声明/定义是来自头文件还是主文件。所以你白白希望。如果某人很傻并且将静态定义放在头文件中,它将有两个VAL副本,并且它包含在两个源中。
Justsalt 2014年

1
const值在C ++中具有内部链接
adrianN '16

112

文件作用域变量上的staticextern标记确定它们是否可以在其他翻译单元(即其他.c.cpp文件)中访问。

  • static给出变量内部链接,将其对其他翻译单元隐藏。但是,具有内部链接的变量可以在多个翻译单元中定义。

  • extern给出可变的外部链接,使其对其他翻译单元可见。通常,这意味着只能在一个翻译单元中定义变量。

默认值(不指定static或时extern)是C和C ++不同的区域之一。

  • 在C中,extern默认情况下,文件作用域变量是(外部链接)。如果您使用的是C,VAL则is staticANOTHER_VALis extern

  • 在C ++中,文件范围的变量是static(内部连接)默认如果它们const,并extern通过缺省,如果他们不。如果您使用的是C ++,则VALANOTHER_VAL均为static

C规范的草稿中:

6.2.2标识符的链接... -5-如果一个函数的标识符声明没有存储类说明符,则其链接的确定与使用存储类说明符extern声明的完全相同。如果对象的标识符声明具有文件范围且没有存储类说明符,则其链接是外部的。

根据C ++规范的草案:

7.1.1-存储类说明符[dcl.stc] ... -6-在命名空间范围中声明的没有存储类说明符的名称具有外部链接,除非由于先前的声明而具有内部链接,并且没有声明的常量。声明为const且未明确声明为extern的对象具有内部链接。


47

静态表示每个文件一个副本,但是与其他文件不同,这样做是完全合法的。您可以使用一个小的代码示例轻松地对此进行测试:

test.h:

static int TEST = 0;
void test();

test1.cpp:

#include <iostream>
#include "test.h"

int main(void) {
    std::cout << &TEST << std::endl;
    test();
}

test2.cpp:

#include <iostream>
#include "test.h"

void test() {
    std::cout << &TEST << std::endl;
}

运行此命令可以得到以下输出:

0x446020
0x446040


5
谢谢你的例子!
Kyrol

我想知道是否TESTconstLTO是否能够将其优化到单个内存位置。但是-O3 -fltoGCC 8.1没有。
西罗Santilli郝海东冠状病六四事件法轮功

这样做是非法的-即使它是恒定的,静态的也保证每个实例对于编译单元都是本地的。如果将其用作常量,则可能会内联常量值本身,但是由于我们要获取其地址,因此它必须返回唯一的指针。
slicedlime

6

constC ++中的变量具有内部链接。因此,使用static无效。

const int i = 10;

一个cpp

#include "a.h"

func()
{
   cout << i;
}

二.cpp

#include "a.h"

func1()
{
   cout << i;
}

如果这是C程序,您将i由于(由于外部链接)而出现“多个定义”错误。


2
嗯,使用static的效果是它可以整洁地传达意图和对编码内容的意识,这绝不是一件坏事。在我看来,这包括virtual覆盖时:我们不必这样做,但是当我们这样做时,事情看起来更加直观-并且与其他声明一致。
underscore_d

可能会在C中得到多个定义错误。这是未定义的行为,不需要诊断
MM

5

此代码级别的静态声明意味着variabel仅在当前编译单元中可见。这意味着只有该模块中的代码才能看到该变量。

如果您有一个头文件声明了一个静态变量,并且该头文件包含在多个C / CPP文件中,则该变量对于这些模块将是“本地”的。对于包含标头的N个位置,该变量将有N个副本。它们根本不相关。这些源文件中任何一个中的任何代码都只会引用该模块中声明的变量。

在这种情况下,“静态”关键字似乎没有提供任何好处。我可能会丢失一些东西,但这似乎无关紧要-以前我从未见过这样的事情。

至于内联,在这种情况下,该变量很可能是内联的,但这仅是因为它已声明为const。编译器可能更容易内联模块静态变量,但这取决于情况和正在编译的代码。不能保证编译器会内联“ statics”。


这里“静态”的好处是,否则,您将声明多个具有相同名称的全局变量,每个包含标题的模块一个。如果链接程序不抱怨,那仅仅是因为它在咬人,而且很礼貌。

在这种情况下,由于存在conststatic因此暗含了,因此是可选的。推论是,正如迈克·F所声称的那样,不存在多重定义错误的敏感性。
underscore_d


2

要回答这个问题,“如果一个以上的源文件包含头,则静态是否仅创建一个VAL副本?” ...

没有。VAL将始终在每个包含标头的文件中单独定义。

在这种情况下,C和C ++的标准确实会有所不同。

在C语言中,默认情况下,文件作用域变量为extern。如果您使用的是C,则VAL为静态,而ANOTHER_VAL为extern。

请注意,如果标头包含在不同的文件中(相同的全局名称定义了两次),现代链接器可能会抱怨ANOTHER_VAL,并且如果另一个文件中的ANOTHER_VAL初始化为不同的值,则肯定会抱怨。

在C ++中,如果文件范围变量是const,则默认情况下为静态;如果不是,则默认为extern。如果您使用的是C ++,则VAL和ANOTHER_VAL都是静态的。

您还需要考虑两个变量都指定为const的事实。理想情况下,编译器将始终选择内联这些变量,而不为它们包括任何存储。可以分配存储的原因很多。我能想到的...

  • 调试选项
  • 文件中的地址
  • 编译器总是分配存储空间(复杂的const类型不容易被内联,因此成为基本类型的特例)

注意:在抽象机中,每个包含标题的单独翻译单元中都有VAL的一个副本。在实践中,链接器可能会决定仍然组合它们,而编译器可能会首先优化它们中的某些或全部。
MM

1

假设这些声明在全局范围内(即不是成员变量),则:

静态是指“内部联系”。在这种情况下,由于已声明为const,因此编译器可以对其进行优化/内联。如果省略const,则编译器必须在每个编译单元中分配存储。

通过省略静态,链接默认为外部。同样,您已经被常量保存了-编译器可以优化/内联用法。如果删除const,则在链接时会出现乘法定义的符号错误。


我相信编译器必须在所有情况下都为const int分配空间,因为另一个模块总是可以说“ extern const int what; something(&whatever);”。

1

您也必须在未定义静态变量的情况下对其进行声明(这是因为存储类修饰符static和extern是互斥的)。可以在头文件中定义静态变量,但是这将导致每个包含头文件的源文件都有其自己的变量私有副本,这可能不是预期的。


“ ...但是,这将导致每个包含头文件的源文件都有其自己的变量专用副本,这可能不是预期的。” -由于静态初始化顺序失败,可能需要在每个翻译单元中都有一个副本。
jww

1

const变量在C ++中默认是静态的,但在外部C中是静态的。因此,如果您使用C ++,则没有什么意义。

(7.11.6 C ++ 2003和Apexndix C都有示例)

比较编译/链接源为C和C ++程序的示例:

bruziuz:~/test$ cat a.c
const int b = 22;
int main(){return 0;}
bruziuz:~/test$ cat b.c
const int b=2;
bruziuz:~/test$ gcc -x c -std=c89 a.c b.c
/tmp/ccSKKIRZ.o:(.rodata+0x0): multiple definition of `b'
/tmp/ccDSd0V3.o:(.rodata+0x0): first defined here
collect2: error: ld returned 1 exit status
bruziuz:~/test$ gcc -x c++ -std=c++03 a.c b.c 
bruziuz:~/test$ 
bruziuz:~/test$ gcc --version | head -n1
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609

还有就是感仍然包括static。它表示对程序员正在做什么的意图/意识,并与缺少隐式的其他类型的声明(以及fwiw,C)保持相等static。就像virtual最近包含override在覆盖函数的声明中一样,这不是必需的,而是更多的自文档说明,对于后者,则有助于进行静态分析。
underscore_d

我完全同意。例如,在现实生活中,我总是明确地写它。
bruziuz

“所以,如果您使用C ++,那么使用什么结构都没有意义...” -嗯...我刚刚编译了一个const仅在标头中的变量上使用的项目g++ (GCC) 7.2.1 20170915 (Red Hat 7.2.1-2)。结果产生了约150个多重定义的符号(每个包含标题的翻译单元一个)。我认为我们需要一个staticinline或者一个匿名/未命名的名称空间来避免外部链接。
jww

我用gcc-5.4尝试了baby-example,const int并在名称空间范围和全局名称空间中进行了声明。而且它在某种原因编译和遵循规则“的对象声明为常量,而不是明确声明为extern有内部链接。”” ......也许在这个项目包括头为C编译的源代码,其中的规则完全不同。
bruziuz

@jww我上传了C链接问题而C ++没有问题的示例
bruziuz

0

静态的阻止另一个编译单元改变该变量,以便编译器可以仅在使用该变量的位置“内联”该变量的值,而不为其创建内存存储。

在第二个示例中,编译器无法假定其他源文件不会终止它,因此它实际上必须将该值存储在内存中的某个位置。


-2

静态阻止编译器添加多个实例。使用#ifndef保护,这变得不那么重要了,但是假设标头包含在两个单独的库中,并且应用程序已链接,则将包含两个实例。


假设通过“库”你的意思是翻译单元,则没有,包括得分后卫什么都不做,以避免多重定义,看到它们只谨防重复夹杂物的内同一翻译单元。因此,他们什么也做不了,使static“重要性降低”。甚至同时使用两者,您最终可能会获得多个内部链接的定义,这可能不是故意的。
underscore_d
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.