C++中static关键字的用法

C++中static关键字具有很多迥然不同的意义与用途,常在不同的情景下出现。例如声明局部静态变量、声明静态函数、声明类的静态成员。这三种用法的背后分别对应着不同的linkage。本文还将staticinlineextern等存储类指定符进行简单的比较,以期了解C++编译阶段和连接阶段的行为。

声明局部静态变量

此时static作为五种存储类指定符storage duration specifiers(auto, register, static, extern, thread_local)中的一种,static声明的静态变量(称为local static)相对于auto(C++11标准后auto关键字另作他用)声明的自动变量,它的生存空间是从所属模块(编译单元)开始全局的,并且能够保证在函数调用之前被初始化构造完成。但是相对于直接使用全局变量,将static置于全局函数内部并返回引用可以保证在任何访问该静态对象的时候,该对象都已完成初始化(可参见Effective C++),由此可以实现单例模式,称为Meyers’ Singleton。从C++11标准开始,Meyers’ Singleton是线程安全的,这是因为新的标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它,而在之前的标准中可能会产生多次初始化的结果。其他的单例模式还包括使用atomtic,和std::call_once,可以访问http://www.cnblogs.com/liyuan989/p/4264889.html来了解。

静态变量和匿名名字空间

注意到在C++中声明局部变量还可以通过匿名名字空间来实现,例如

1
2
static int _func();
static int _var;

可以写为

1
2
3
4
namespace{
int _func();
int _var;
}

但是匿名名字空间还适用于修饰类型的情况如

1
2
3
4
namespace {
struct _struct {
};
}

但匿名名字空间和static变量还是不同的,匿名名字空间具有外部链接性(但是不知道具体名字),而static变量具有内部链接性

声明静态函数/变量

在这里,static表示该函数/变量名字只能在该编译单元内部使用,而不导出。例如在不同的编译单元中使用static关键字允许出现多个同名的变量/函数。因此可以利用static实现在头文件中直接给出函数定义,也就是使用static修饰。但这是非常不好的做法,正确的做法应该是分情况使用constexpr或者inline
static常和const一起搭配用来声明一个编译期常量。

声明类的静态成员/成员函数

在class声明中初始化的规定

C++标准只允许static const的integral/enumeration类型(integral types)在class声明中初始化。注意在《STL源码分析一书中》指出在CB4对在类外部初始化如static int成员的支持有限,见有关组态__STL_STATIC_TEMPLATE_MEMBER_BUG的部分。
在C++中禁止以下两种初始化,原因详见Stackoverflow

  1. 禁止在声明时同时初始化非const的static类成员
    我们注意事实上不能static member作为类的一部分,典型的就是我们需要在类外进行定义

    1
    2
    3
    4
    struct Foo{
    static int i;
    };
    int Foo::i;
  2. 禁止在声明时同时初始化const的非literal的static类成员
    这是由于C++ requires that every object has a unique definition. That rule would be broken if C++ allowed in-class definition of entities that needed to be stored in memory as objects

C++11之后的标准似乎允许(待考证)在class声明中用默认值初始化non-static non-const的integral字段,但当定义了构造函数之后就应当用构造函数初始化

linkage

链接性(linkage)指的是一个名字在整个程序或某个编译单元中是否会绑定到同一个实体(entity)上。在上面的用法中,static都限定了一个名字的链接性,然而这三种情况下的链接性都不一样。
函数中的static限定了名字只在某个函数内可见,是没有链接性的,因为它既不是全局可见,也不对当前的编译单元可见。没有链接性的实体包括局部变量和函数的形参。
一个static的函数具有内部链接性,即对当前编译单元可见,而在全局不可见,即对链接器而言不可见。根据维基百科,具有内部链接性的实体包括声明、名字空间中static的自由函数(不带上下文的函数,相对于成员函数)、友元函数、变量和const常量(特别地在C++中单独的const也是内部链接性的,除非是extern const,这和C语言不一样)、enum、inline自由函数和非自由函数、class/struct、union。
一个static的类成员/成员函数具有外部链接性。具有外部链接性的实体包括非inline的函数(包括非static自由函数、类非static成员函数和类static成员函数)、类的static成员和名字空间(不包括无名命名空间)中的非static变量。
特别地,SoF上有一篇回答说具有外部链接性的inline函数可以出现多个定义,但我们不能认为inline函数具有外部链接性,因为他们可能实际上被内联了。

static和inline的区别

单一定义原则(ODR)

我们首先查看C++中重要的单一定义规则(ODR),该规则不允许同名的强符号(strong symbol),但允许同名的弱符号(weak symbol)。根据CSAPP,strong symbol包含了函数和已初始化的全局变量,而weak symbol包含未初始化的全局变量*。当weak symbol和strong symbol出现冲突时,链接器选择strong symbol。当没有strong symbol,而各weak symbol间产生冲突时,链接器选择占用空间最大的一个。选择占空间最大的原因在于在编译期时并不能决定这些未初始化的全局变量实际的占用大小,因为作为一个弱符号,它有可能在某个编译单元内被定义为某个4字节的长度然后在另一个编译单元内被定义为8个字节的长度。如果出现这样的情况,那么程序有大概率是出错的,但是在未开启fno-common选项时GCC并不会输出警告。但是链接期的时候链接期需要为这个弱符号在.bss注册大小,为了能够适应所有编译单元中的空间需求,所以只能取最大的一个。
作为用户,为了保证weak symbol间不出现冲突,必须保证所有的声明都是一样的。特别地对于同名的重载函数,我们需要理解对于编译器和链接器,其处理的函数名字并不是其本名,而是经过mangling变成一堆乱码一样的东西以保证唯一性,这导致同一个函数通过C或C++编译出来的符号是不一样的。

inline function

相对于C89标准,C++引入了inline函数。为了了解其作用,我们首先考虑一个static的函数具有internal linkage,当这个函数位于头文件中被多次包含时,编译器会生成对每个编译单元生成一个独立函数(也有可能进行内联优化),编译器不保证所有编译单元中生成的所有函数是一样的。容易发现,static会导致生成的binary变大。C++的inline关键字提供了另一个更好的方案,inline并不会保证一定会被编译器内联(可以使用__forceinline __attribute__((always_inline))提高内联的几率),当编译器没有真正内联该函数时,就可能出现重复定义的情况(例如当这个inline定义被多次包含时),而这违背了C++重要的单一定义规则。为了解决未进行内联下的符号问题,编译器为这些inline函数创建了weak symbol。

inline variable

C++17引入了inline variable,因此inline现在也可以用在变量定义上了。由于C++禁止in-class initialization of static data member of non-literal type,又禁止重复定义,需要借助下面的Workaround,通过模板的实例化阶段来获得一次“重新定义的机会”。

1
2
3
4
5
6
7
8
9
10
template<class Dummy>
struct Kath_
{
static std::string const hi;
};

template<class Dummy>
std::string const Kath_<Dummy>::hi = "Zzzzz...";

using Kath = Kath_<void>; // Allows you to write `Kath::hi`.

在C++17后可以写作

1
2
3
4
5
6
struct Kath
{
static std::string const hi;
};

inline std::string const Kath::hi = "Zzzzz..."; // Simpler!

当然如果在一个编译单元内我们需要使用另一个编译单元中定义的变量时,我们还可以用extern int x;来声明,但注意extern int x = 1;是个定义。

static inline

static的可访问性

staticthread_local是可以从闭包里直接访问而不需要捕捉的,使用[&]形式的捕捉可能造成问题