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一起搭配用来声明一个编译期常量。

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

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

  1. 禁止在声明时同时初始化非const的static类成员

  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

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变量。

static和inline的区别

以函数为例,一个static的函数具有internal linkage,当这个函数位于头文件中被多次包含时,编译器会生成对每个编译单元生成一个独立函数(也有可能进行内联优化),编译器不保证所有编译单元中生成的所有函数是一样的。容易发现,static会导致生成的binary变大。这时候使用C++提供了inline关键字是个很好的方案,inline并不会保证一定会被编译器内联(可以使用__forceinline__attribute__((always_inline))提高内联的几率),当编译器没有真正内联该函数时,就可能出现重复定义的情况(例如被多次包含)。C++要求单一定义规则,不允许同名的strong symbol。注意C++中同名的重载函数会被经过mangling变成一堆乱码一样的东西以保证唯一性,这导致同一个函数通过C或C++编译出来的符号是不一样的。为了解决内联失败的问题,编译器为这些inline函数创建了weak symbol。根据CSAPP,strong symbol包含了函数和已初始化的变量,而weak symbol包含未初始化的变量。C++在link时首先会选择strong symbol,只会link同名的weak symbol中的任一个,因此用户必须保证所有的声明都是一样的。
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的可访问性

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