C 和 C++ 中的"常量静态"是什么意思?

What does 'const static' mean in C and C++?

本文关键字:是什么 意思 静态 常量 C++ 中的      更新时间:2023-10-16
const static int foo = 42;

我在StackOverflow上的一些代码中看到了这一点,但我无法弄清楚它的作用。然后我在其他论坛上看到了一些令人困惑的答案。我最好的猜测是它在 C 中用于隐藏其他模块的常量foo。这是对的吗?如果是这样,为什么有人会在C++环境中使用它,你可以让它private

很多人给出了基本答案,但没有人指出C++ const默认static namespace级别(有些人给出了错误的信息(。请参阅 C++98 标准第 3.5.3 节。

首先介绍一些背景:

翻译单元:预处理器(递归(之后的源文件包含其所有包含文件。

静态链接:符号仅在其翻译单元中可用。

外部链接:符号可从其他翻译单元获得。

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表示它不会更改。

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

如您所猜,static部分将其范围限制在该编译单元上。它还提供静态初始化。 const只是告诉编译器不要让任何人修改它。此变量根据体系结构放在数据段或 bss 段中,并且可能位于标记为只读的内存中。

这就是 C 如何处理这些变量(或C++如何处理命名空间变量(。在 C++ 中,标记为 static 的成员由给定类的所有实例共享。无论它是否私有都不会影响一个变量由多个实例共享的事实。如果有任何代码试图修改它,const会警告您。

如果它是严格私有的,那么类的每个实例都将获得自己的版本(尽管有优化器(。

这行代码实际上可以出现在几个不同的上下文中,并且它的行为大致相同,存在细微差异。

命名空间范围

// 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;

static const int i = 0一模一样。 命名空间中使用 const 声明但未使用 extern 显式声明的变量是隐式静态的。 如果您考虑一下,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

这是一个小空间优化。

当你说

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
}

它缺少一个"int"。 它应该是:

const static int foo = 42;

在 C 和 C++ 中,它声明一个本地文件范围值为 42 的整数常量。

为什么是42? 如果你还不知道(而且很难相信你不知道(,这是对生命、宇宙和一切的答案的参考。

C++17 inline变量

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

这个很棒的 C++17 功能使我们能够:

  • 方便地为每个常量仅使用一个内存地址
  • 将其存储为constexpr:如何声明 constexpr extern?
  • 从一个
  • 标题在一行中完成

主.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

不是主.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 具有外部链接的内联函数或变量在所有翻译单元中应具有相同的地址。

CPP偏好 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 nmu

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

所以我们看到有一个专用的ELF扩展。

C++前17:extern const

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

inline的缺点是:

    不可能
  • 用这种技术使变量constexpr,只有inline允许: 如何声明 constexpr extern?
  • 它不太优雅,因为您必须在标头和 cpp 文件中分别声明和定义变量

主.cpp

#include <cassert>
#include "notmain.hpp"
int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

不是主.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 上游。

仅 C++17 之前的标头替代方案

这些不如extern解决方案,但它们有效并且只占用一个内存位置:

constexpr函数,因为constexpr意味着inline,并且inline允许(强制(定义出现在每个翻译单元上:

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 使用,另请参阅:定义 constexpr 静态数据成员

C

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

唯一的区别是,在C++中,const 意味着全局变量的static,但在 C 中则不然:C++"静态常量"与"常量"的语义

有什么方法可以完全内联它吗?

TODO:有没有办法完全内联变量,根本不使用任何内存?

很像预处理器所做的。

这将需要以某种方式:

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

相关:

  • 具有类成员和 constexpr 链接时间优化的 C++11 枚举

在 Ubuntu 18.10、GCC 8.2.0 中测试。

根据 C99/GNU99 规范:

  • static

    • 是存储类说明符

    • 默认情况下,文件级范围的对象具有外部链接

    • 具有
    • 静态说明符的文件级范围的对象具有内部链接
  • const

      是类型
    • 限定符(是类型的一部分(

    • 关键字应用于紧邻左侧实例 - 即

      • MyObj const * myVar; - 指向 const 限定对象类型的非限定指针

      • MyObj * const myVar; - 指向非限定对象类型的常量限定指针

    • 最左侧的用法 - 应用于对象类型,而不是变量

      • const MyObj * myVar; - 指向 const 限定对象类型的非限定指针

因此:

static NSString * const myVar; - 指向具有内部链接的不可变字符串的常量指针。

缺少 static 关键字将使变量名称成为全局名称,并可能导致应用程序中的名称冲突。

在C++,

static const int foo = 42;

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

#define foo 42

因为它不会颠覆类型安全系统。

对于所有伟大的答案,我想添加一个小细节:

如果您编写插件(例如 DLL 或 .so 库由 CAD 系统加载(,那么 static 是避免像这样名称冲突的救星:

  1. CAD系统加载一个插件A,其中有一个"const int foo = 42;"。
  2. 系统加载一个插件 B,其中包含"const int foo = 23;"。
  3. 因此,插件 B 将使用值 42 表示 foo,因为插件加载器将意识到,已经存在具有外部链接的"foo"。

更糟糕的是:步骤 3 的行为可能会有所不同,具体取决于编译器优化、插件加载机制等。

我曾经在两个插件中使用两个辅助函数(相同的名称,不同的行为(遇到过这个问题。声明它们是静态的解决了问题。

是的,它从其他模块中隐藏了模块中的变量。在C++中,当我不想/不需要更改将触发其他文件不必要的重建的 .h 文件时,我会使用它。另外,我把静态放在第一位:

static const int foo = 42;

此外,根据其用途,编译器甚至不会为其分配存储,而只是在使用它的位置"内联"值。如果没有静态,编译器就不能假设它没有在其他地方使用,也不能内联。

这个 ia 的全局常量仅在编译模块(.cpp 文件中可见/可访问(。顺便说一句,不推荐为此目的使用静态。最好使用匿名命名空间和枚举:

namespace
{
  enum
  {
     foo = 42
  };
}

将其设为私有仍然意味着它出现在标题中。我倾向于使用"最弱"的方式。请参阅Scott Meyers的这篇经典文章:http://www.ddj.com/cpp/184401197(它是关于函数的,但也可以在这里应用(。