C和C++中的'const static'是什么意思?

151
const static int foo = 42;

我在StackOverflow的一些代码中看到了这个,但我无法弄清它的作用。然后我在其他论坛上看到了一些困惑的回答。我最好的猜测是它用于C中将常量foo隐藏在其他模块中。如果是这样的话,在C++环境下为什么有人要使用它而不是直接将其设为private

12个回答

261
很多人给出了基本答案,但没有人指出在C++中const默认为namespace级别的static(有些人给出了错误信息)。请查看C++98标准第3.5.3节。首先介绍一些背景知识:翻译单元:经过预处理器(递归)包含所有include文件后的源文件。静态链接:符号仅在其翻译单元中可用。外部链接:符号可以从其他翻译单元中使用。在namespace级别:这包括全局命名空间和全局变量
static const int sci = 0; // sci is explicitly static
const int ci = 1;         // ci is implicitly static
extern const int eci = 2; // eci is explicitly extern
extern int ei = 3;        // ei is explicitly extern
int i = 4;                // i is implicitly extern
static int si = 5;        // si is explicitly static

在函数级别

static 意味着值在函数调用之间保持不变。
函数 static 变量的语义类似于全局变量,因为它们驻留在 程序的数据段(而不是堆栈或堆)中,有关 static 变量生命周期的更多详细信息,请参见 此问题

class 级别

static 意味着该值在类的所有实例之间共享,而 const 意味着它不会改变。


3
在函数级别上,static和const并不是多余的,它们可以表现出不同的行为。例如:const int *foo(int x) {const int b=x;return &b};const int *foo(int x) {static const int b=x;return &b}; - Hanczar
2
这个问题涉及到C和C++,因此您应该包含一个关于const仅在后者中意味着static的注释。 - Nikolai Ruhe
@Motti:非常好的答案。您能否解释一下在函数级别上是什么使某些内容变得冗余?您是否意味着const声明也意味着那里有static?换句话说,如果您取消const并修改该值,所有值都将被修改吗? - Cookie
1
@Motti const 在函数级别上并不意味着静态,否则这将是一个并发的噩梦(const != 常量表达式),在函数级别上的所有内容都隐式地是 auto。由于这个问题也被标记为 [c],我应该提到,在 C 中,全局级别的 const int 隐式地是 extern。你在这里描述的规则完美地描述了 C++。 - Ryan Haining
1
在 C++ 中,对于所有三种情况,static 表示变量具有静态持续时间(仅存在一个副本,从程序的开始到结束都将存在),并且如果未另行指定,则具有内部/静态链接性(对于局部静态变量,函数的链接性会覆盖此链接性,对于静态成员,则为类的链接性)。主要区别在于,在每个 static 有效的情况下,这意味着什么。 - Justin Time - Reinstate Monica
显示剩余7条评论

134

它在C和C++中都有用途。

正如你猜测的那样,static部分将其范围限制在编译单元内。它还提供了静态初始化。 const只是告诉编译器不允许任何人修改它。该变量可能被放置在数据或bss段中,具体取决于架构,并且可能在内存中标记为只读。

所有这些都是C如何处理这些变量(或者C++如何处理命名空间变量)。在C++中,标记为static的成员由给定类的所有实例共享。无论它是私有的还是公共的,都不影响一个变量被多个实例共享的事实。如果在这上面加上const,则会在任何代码尝试修改它时发出警告。

如果它严格是私有的,则每个类实例都将得到自己的版本(优化器除外)。


1
原始示例是在谈论“私有变量”。因此,这是一个成员变量,静态对链接没有影响。您应该删除“静态部分将其范围限制为该文件”的内容。 - Richard Corden
“特殊部分”被称为数据段,它与所有其他全局变量共享,例如显式的“字符串”和全局数组。这与代码段相反。 - spoulson
@Richard - 你为什么认为它是类的成员?问题中没有任何说明。如果它是类的成员,那么你是对的,但如果它只是在全局范围内声明的变量,那么Chris是正确的。 - Graeme Perrow
1
原帖提到私有作为可能更好的解决方案,但不是最初的问题。 - Chris Arguin
@Graeme,好的,所以它不是“绝对”的成员 - 然而,这个答案正在做出仅适用于命名空间成员的陈述,并且对于成员变量来说这些陈述是错误的。考虑到该错误获得的票数,可能会让不太熟悉该语言的人感到困惑 - 应该进行修正。 - Richard Corden
@Richard,我同意你的看法,并根据你的顾虑进行了更新。我是一名C程序员,没有考虑到对象范围的含义。 - Chris Arguin

45

那行代码实际上可以出现在几个不同的上下文中,虽然它的行为大致相同,但存在一些小差异。

命名空间作用域

// foo.h
static const int i = 0;
'i'将出现在包含该头文件的每个翻译单元中。但是,除非您实际使用对象的地址(例如'&i'),否则编译器会将'i'视为类型安全的0。当两个或更多翻译单元使用'&i'时,每个翻译单元的地址将不同。
// foo.cc
static const int i = 0;
'i'具有内部链接,因此无法从该翻译单元外部引用。但是,除非使用其地址,否则它很可能被视为类型安全的0
值得指出的一件事是,以下声明:
const int i1 = 0;

在命名空间中用const声明的变量,且没有显式使用extern关键字,其实和static const int i = 0是完全相同的。这种情况下,const变量会被隐式声明为静态变量。如果你仔细想一下,C++委员会旨在允许在头文件中声明const变量而不总是需要static关键字来避免破坏ODR。

类作用域

class A {
public:
  static const int i = 0;
};
在上面的示例中,标准明确指定如果不需要其地址,则'i'无需定义。换句话说,如果您仅将'i'用作类型安全的0,则编译器不会定义它。类和命名空间版本之间的一个区别是,对于类成员,如果在两个或多个翻译单元中使用,则'i'的地址将相同。在使用地址的地方,必须为其定义一个定义。
// a.h
class A {
public:
  static const int i = 0;
};

// a.cc
#include "a.h"
const int A::i;            // Definition so that we can take the address

5
对于指出在命名空间范围内,static const与const是相同的,给予点赞。 - Plumenator
实际上,在“foo.h”或“foo.cc”中放置没有区别,因为在编译翻译单元时会简单地包含.h。 - Mikhail
3
@Mikhail:你说得对。有一个假设,即头文件可以包含在多个TUs中,因此单独讨论这一点是有用的。 - Richard Corden

30

这是一种小型空间优化。

当你说

const int foo = 42;

你没有定义一个常量,而是创建了一个只读变量。编译器足够聪明,在看到foo时使用42,但它也会为其分配初始化数据区的空间。这样做是因为,如定义所示,foo具有外部链接性。另一个编译单元可以说:

extern const int foo;

以获得对其值的访问。这不是一个好习惯,因为那个编译单元不知道foo的值。它只知道它是一个const int,并且每次使用时必须从内存重新加载值。

现在,通过声明它是静态的:

static const int foo = 42;

编译器可以进行常规的优化,但它也可以说:“嘿,这个编译单元之外的人看不到foo,我知道它总是42,所以没有必要为它分配任何空间。”

另外需要注意的是,在C++中,防止名称从当前编译单元逃逸的首选方法是使用匿名命名空间:

namespace {
    const int foo = 42; // same as static definition above
}

1
你提到了不使用static时,“它也会在初始化数据区域分配空间”,而使用static时,“不需要为其分配任何空间”(那么编译器从哪里获取值呢?)。您能否解释一下变量存储在堆栈中的术语?如果我理解有误,请纠正我。 - Nihar
@N.Nihar - 静态数据区是一个固定大小的内存块,其中包含所有具有静态链接的数据。它是通过将程序加载到内存中的过程来“分配”的。它不是堆栈或堆的一部分。 - Ferruccio
如果我让一个函数返回指向foo的指针会发生什么?这会破坏优化吗? - nw.
@nw:是的,它必须这样。 - Ferruccio
在C++中,const值也具有内部链接,并且可以在头文件中使用。https://learn.microsoft.com/en-us/cpp/cpp/const-cpp?view=msvc-170。经过一些实验,似乎没有太大的区别,无论是否使用static,但是const对于让编译器使用直接值至关重要。 - gast128

10

缺少了一个 'int'。应该是:

const static int foo = 42;

在C和C++中,它声明了一个整数常量,具有局部文件作用域,值为42。
为什么是42?如果您还不知道(很难相信您不知道),这是对生命、宇宙和万物的答案的引用。

谢谢...现在每次...在我的余生中...当我看到42时,我总是会想起这个。哈哈 - Inisheer
这证明宇宙是由有13个手指的生物创造的(实际上问题和答案都基于13进制)。 - paxdiablo
这是关于鼠的内容。每只脚有3个脚趾,再加上一条尾巴可以形成13进制数。 - KeithB
在声明中实际上不需要使用“int”,尽管将其写出来肯定是好习惯。C语言默认假设类型为“int”。试试看吧! - ephemient
“值为42的本地文件作用域”?还是整个编译单元? - aniliitb10

8

C++17 inline 变量

如果你在谷歌上搜索的是"C++ const static",那么你很可能真正想要使用的是C++17 inline 变量

这个厉害的 C++17 特性允许我们:

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

编译和运行:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub上游

另请参阅:内联变量如何工作?

C++标准中的内联变量

C++标准保证地址相同。C++17 N4659标准草案 10.1.6“内联说明符”:

6 具有外部链接的内联函数或变量在所有翻译单元中都应具有相同的地址。

cppreference https://en.cppreference.com/w/cpp/language/inline 解释了如果没有使用 static,则具有外部链接。

GCC实现内联变量

我们可以通过以下方式观察其实现:

nm main.o notmain.o

其中包含:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

man nm介绍了关于u的信息:

"u"这个符号是一个独特的全局符号。这是标准ELF符号绑定集合的GNU扩展。对于这样的符号,动态链接器将确保在整个进程中只使用一个具有此名称和类型的符号。

因此,我们可以看到有一个专门的ELF扩展。

C++ 17之前: extern const

在C++ 17之前以及在C语言中,我们可以通过extern const实现非常相似的效果,这将导致只使用一个内存位置。

inline相比的缺点是:

  • 无法使用此技术使变量成为constexpr,只有inline允许这样做:如何声明constexpr extern?
  • 它不够优雅,因为您必须在头文件和cpp文件中分别声明和定义变量

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.cpp

#include "notmain.hpp"

const int notmain_i = 42;

const int* notmain_func() {
    return &notmain_i;
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

extern const int notmain_i;

const int* notmain_func();

#endif

GitHub上游

Pre-C++17仅头文件替代方案

这些方案不如extern方案好,但它们可行并且只占用单个内存位置:

使用constexpr函数,因为constexpr意味着inlineinline允许(强制)定义出现在每个翻译单元上

constexpr int shared_inline_constexpr() { return 42; }

我敢打赌,任何一个好的编译器都会内联这个调用。

你也可以使用一个 constconstexpr 静态变量,例如:

#include <iostream>

struct MyClass {
    static constexpr int i = 42;
};

int main() {
    std::cout << MyClass::i << std::endl;
    // undefined reference to `MyClass::i'
    //std::cout << &MyClass::i << std::endl;
}

但你不能像获取它的地址这样做,否则它就成为了odr-used,参见:定义constexpr静态数据成员

C语言

在C语言中,情况与C++ 17之前的情况相同,我上传了一个示例:C中"static"是什么意思?

唯一的区别是,在C++中,对于全局变量,const意味着static,但在C中不是这样的:C++中`static const`与`const`的语义

有没有完全内联它的方法?

待办事项:有没有完全内联变量的方法,而不使用任何内存?

就像预处理器所做的那样。

这需要以某种方式:

  • 禁止或检测是否获取了变量的地址
  • 将该信息添加到ELF对象文件中,并让LTO进行优化

相关内容:

在Ubuntu 18.10,GCC 8.2.0中测试。


6
根据C99/GNU99规范:
- `static`是存储类别说明符 - 默认情况下,文件级别作用域的对象具有外部链接性 - 具有静态说明符的文件级别作用域对象具有内部链接性
- `const`是类型限定符(是类型的一部分) - 关键字应用于左侧实例,即: - `MyObj const * myVar;` - 非限定指向常量限定对象类型的指针 - `MyObj * const myVar;` - 常量限定的指向非限定对象类型的指针 - 最左边的用法 - 应用于对象类型,而不是变量 - `const MyObj * myVar;` - 非限定指向常量限定对象类型的指针
因此:
`static NSString * const myVar;` - 带有内部链接性的不可变字符串的常量指针。
如果没有`static`关键字,变量名将成为全局变量,并可能导致应用程序中的名称冲突。

4
对于所有优秀的答案,我想补充一个小细节:
如果您编写插件(例如DLL或.so库,供CAD系统加载),那么“static”可以避免名称冲突,如下所示:
1. CAD系统加载插件A,其中有“const int foo = 42;”。 2. 系统加载插件B,其中有“const int foo = 23;”。 3. 结果是,插件B将使用值42作为foo,因为插件加载器将意识到已经存在具有外部链接的“foo”。
更糟糕的是:第3步可能会根据编译器优化、插件加载机制等而表现出不同的行为。
我曾经在两个插件中遇到过这个问题,它们有相同的名称但不同的行为。声明它们为静态解决了问题。

似乎两个插件之间的名称冲突有些奇怪,这促使我检查了其中一个定义m_hDfltHeap为具有外部链接的句柄的许多DLL之一的链接映射。果然,在链接映射中,所有人都可以看到并使用_m_hDfltHeap。我已经忘记了这个事实。 - David A. Gray

4

在C++中,

static const int foo = 42;

使用常量的首选方式是定义和使用常量。即使用此方法而不是

#define foo 42

因为它不会破坏类型安全系统。


2

是的,它可以隐藏模块中的变量,使其对其他模块不可见。在C++中,当我不想/不需要更改.h文件以触发其他文件的不必要重建时,我会使用它。此外,我首先将static放在前面:

static const int foo = 42;

此外,根据其用途,编译器甚至不会为其分配存储空间,而是在使用它的地方直接“内联”该值。没有static关键字,编译器无法假设它不会在其他地方使用,并且无法内联。


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