static大概是 C++ 里最「身兼数职」的关键字:放在函数里的局部变量前、放在全局变量前、放在类成员前,意思全不一样,新手极易混淆。但万变不离其宗——它要么改变存储期(storage duration,变量活多久),要么改变链接(linkage,名字在多少个翻译单元里可见),要么只是声明一个编译期常量。本文把五种含义逐个拆开,每种配一个能真编译运行的最小示例。
1. 引子:为什么同一个词意思差这么多
先看三段代码,猜猜static各自在干嘛:
voidf(){staticintx=0;++x;}// ① 函数内的 staticstaticintg=0;// ② 全局的 staticstructS{staticintn;};// ③ 类里的 static① 让x跨调用存活;② 让g只在本文件可见;③ 让n被所有对象共享。同一个词,管的三件事风马牛不相及。根源在于 C 语言历史包袱:static最早只有「内部链接」一义,后来被不断复用。理解它的钥匙是分清作用域(scope,名字在哪能用)和存储期(变量活到什么时候)——下面每节都会点明它到底改了哪个。
2. 含义一:局部静态变量(改的是存储期)
函数内的static局部变量,作用域还是那个函数块,但存储期变成了「静态存储期」:第一次执行到时初始化一次,之后一直存活到程序结束,下次调用沿用上次的值。
#include<cstdio>intnextId(){staticintid=0;// 只初始化一次,之后一直存在到程序结束return++id;}intmain(){printf("id1 = %d\n",nextId());printf("id2 = %d\n",nextId());printf("id3 = %d\n",nextId());}id1 = 1 id2 = 2 id3 = 3如果你把static去掉,id每次都是新的局部变量,三次都会打印1。static在这里只改存储期,不改作用域——id仍然只能在nextId里访问,但它不再随函数返回而销毁。
官方文档:Storage duration — cppreference:静态存储期的权威定义,以及它与自动、动态、线程存储期的并列关系。
2.1 关键坑:C++11 起,局部静态的初始化是线程安全的
这是面试常考点,也是Meyers 单例(Meyers singleton)能成立的根基。C++11 标准规定:局部静态变量的初始化不会发生数据竞争(data race),多线程同时第一次进入时,只有一个线程执行初始化,其余阻塞等待。看这个单例:
#include<cstdio>classLogger{public:staticLogger&instance(){staticLogger inst;// C++11 起:初始化线程安全,且只发生一次returninst;}voidlog(constchar*msg){printf("log: %s\n",msg);}private:Logger(){printf("Logger 构造(只一次)\n");}// 私有构造,禁止外界 new};intmain(){Logger::instance().log("hello");Logger::instance().log("world");}Logger 构造(只一次) log: hello log: world注意Logger只构造了一次——第二次instance()复用第一次的对象。在 C++11 之前,这个写法要用「双检锁(double-checked locking)」手写,极易写错;现在靠局部静态的线程安全初始化,一行static Logger inst;就搞定。
3. 含义二:全局静态(改的是链接)
全局变量前加static,意思是内部链接(internal linkage):这个变量只在**当前翻译单元(translation unit,大致就是一个 .cpp 加上它 include 的头)**里可见,别的 .cpp 即使extern也找不到它。
#include<cstdio>staticintg_count=42;// 静态全局变量:内部链接,只在当前翻译单元可见intmain(){printf("static 全局变量 g_count = %d\n",g_count);}static 全局变量 g_count = 42它的存储期仍是静态存储期(活到程序结束),和普通的全局变量一样;static在这里纯粹是收窄了链接——把「全局可见」改成「文件内可见」,避免和其它 .cpp 里的同名全局变量在链接阶段撞符号。这也是匿名命名空间(anonymous namespace)流行之前,限制全局名字的主要手段。
官方文档:Program and linkage — cppreference:内部链接、外部链接、无链接三者的精确界定。
4. 含义三:静态成员变量(所有对象共享一份)
类里的static成员变量不属于任何一个对象,而是被该类的所有对象共享,只有一份。它同样活在静态存储区,sizeof一个对象时不会把它算进去。
#include<cstdio>structWidget{Widget(){++count;}// 每构造一个,计数 +1staticintcount;// 声明:所有对象共享这一份};intWidget::count=0;// 定义:必须在类外(C++17 之前必须这样写)intmain(){Widget a,b,c;printf("构造了 %d 个 Widget\n",Widget::count);printf("通过对象也能访问:%d\n",a.count);}构造了 3 个 Widget 通过对象也能访问:3重点在int Widget::count = 0;这行类外定义。C++17 之前,静态成员变量只是「声明」,必须在某个 .cpp 里单独「定义」一次,否则链接时报undefined reference。C++17 起可以用inline把定义直接写进类里(见第 6 节),少写一行还不容易漏。
5. 含义四:静态成员函数(没有 this,可作回调)
类里的static成员函数没有this指针,因此它不能访问非静态成员,但可以不依赖任何对象直接通过类名调用。这个特性让它很适合当回调函数(比如传给std::thread、C 风格 API 的函数指针)。
#include<cstdio>structMath{staticintadd(intx,inty){returnx+y;}// 没有 this,不能访问非静态成员};intmain(){printf("Math::add(3, 4) = %d\n",Math::add(3,4));// 不用对象也能调用}Math::add(3, 4) = 7非静态成员函数因为有this,类型其实是int (Math::*)(int,int),没法直接当普通函数指针用;静态成员函数类型就是int(*)(int,int),二者在「能不能当回调」上差别很大。
6. 含义五:类内 inline static constexpr 常量(C++17)
这是最「现代」的一种。在类里放一个编译期常量,C++17 之前要写成「类内static constexpr声明 + 类外定义」;C++17 起,给static成员加inline,定义直接写在类里,头文件里就能完成,不用再找地方放类外定义。
#include<cstdio>structConfig{staticconstexprintMaxRetry=3;// C++17:类内直接定义,无需类外staticconstexprdoubleTimeout=1.5;};intmain(){printf("MaxRetry = %d\n",Config::MaxRetry);printf("Timeout = %.1f\n",Config::Timeout);}MaxRetry = 3 Timeout = 1.5constexpr保证它在编译期就是常量,可以取地址、可以当模板参数、零运行时开销。inline解决了「头文件里定义导致多翻译单元重复定义」的老问题——这正是下一讲inline要展开的主题。
7. 作用域 vs 存储期:一张表收尾
五种含义最容易被搞混,是因为static在不同位置改的是不同的维度。把这张表存进知识库,随时对照:
| 含义 | 出现位置 | 主要改变 | 存储期 | 链接 |
|---|---|---|---|---|
| 局部静态变量 | 函数内 | 存储期(跨调用存活) | 静态 | 无链接 |
| 全局静态 | 全局 | 链接(收窄到本 TU) | 静态 | 内部链接 |
| 静态成员变量 | 类内 | 被所有对象共享一份 | 静态 | 外部链接(类作用域) |
| 静态成员函数 | 类内 | 去掉this,可作回调 | 静态 | 外部链接(类作用域) |
| 类内 inline static constexpr | 类内 | 编译期常量,类内定义 | 静态 | 外部链接(内联) |
它们的存储期出奇一致——全是静态存储期,活到程序结束;真正分化的是作用域和链接。进程内存布局里它们都落在静态区(data/BSS 段),和栈上的自动变量、堆上的动态变量划清界限:
进程内存布局(简化) ┌──────────────────┐ 高地址 │ 栈 stack │ ← 局部自动变量(自动存储期,函数返回即释放) ├──────────────────┤ │ 堆 heap │ ← new / make_unique(动态存储期) ├──────────────────┤ │ BSS(未初始化静态)│ │ data(已初始化静态)│ ← static / 全局(静态存储期,程序结束才释放) ├──────────────────┤ │ 代码 text │ └──────────────────┘ 低地址哪种写法更优:局部静态 vs 裸全局计数
顺带回答一个常见取舍:要做一个「跨函数调用累加的计数器」,该用局部静态还是裸全局?
| 写法 | 可见范围 | 风险 |
|---|---|---|
static int x放在函数内 | 仅该函数可见 | 封装好,其它代码碰不到 |
裸int x放全局 | 整个程序(extern 可见) | 名字易冲突,易被误改 |
结论:能用局部静态就别用裸全局——前者把状态封在函数内部,符合「最小可见性」原则,也省去全局命名污染。只有当多个函数都必须读写同一份状态时,才提升到(最好也加static的)全局或类静态成员。
8. 完整示例:把五种 static 串起来
下面这个程序整体编译运行,覆盖前面所有要点:
#include<cstdio>// ② 全局静态:本文件内可见staticintg_fileTag=99;// ⑤ 类内 inline static constexpr:编译期常量structLimits{staticconstexprintMaxItems=5;};structCounter{// ③ 静态成员变量:所有对象共享staticinttotal;// ④ 静态成员函数:无 this,可作回调式接口staticintgetTotal(){returntotal;}Counter(){++total;}// ① 局部静态也常和类静态配合计数};intCounter::total=0;// 类外定义(非 inline 的经典写法)intmain(){Counter a,b,c;printf("对象个数 = %d\n",Counter::getTotal());printf("文件标签 = %d\n",g_fileTag);printf("MaxItems = %d\n",Limits::MaxItems);}对象个数 = 3 文件标签 = 99 MaxItems = 59. 延伸阅读
- Storage duration — cppreference:五种存储期(自动/静态/动态/线程/空)的总纲,建议通读。
- Static members — cppreference:类静态成员、静态函数的完整规则。
- Core Guidelines · I.2 / NR.* :关于避免全局状态、限制可见性的设计建议,理解
static全局为何要慎用。
10. 一句话总结
static在五个位置干五件不同的事:函数内改存储期(跨调用存活,且 C++11 起初始化线程安全)、全局改链接(收窄到本文件)、类内成员是共享数据/无 this 的接口、类内inline static constexpr是编译期常量——存储期全都一样是静态存储期,真正分化的是作用域与链接,记住这点就不再晕。