news 2026/9/27 22:47:41

static 的五种含义:同一个关键字,五种不同的事

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
static 的五种含义:同一个关键字,五种不同的事

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

constexpr保证它在编译期就是常量,可以取地址、可以当模板参数、零运行时开销。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 = 5

9. 延伸阅读

  • Storage duration — cppreference:五种存储期(自动/静态/动态/线程/空)的总纲,建议通读。
  • Static members — cppreference:类静态成员、静态函数的完整规则。
  • Core Guidelines · I.2 / NR.* :关于避免全局状态、限制可见性的设计建议,理解static全局为何要慎用。

10. 一句话总结

static在五个位置干五件不同的事:函数内改存储期(跨调用存活,且 C++11 起初始化线程安全)、全局改链接(收窄到本文件)、类内成员是共享数据/无 this 的接口、类内inline static constexpr是编译期常量——存储期全都一样是静态存储期,真正分化的是作用域与链接,记住这点就不再晕。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 22:46:42

VsCode 前端常用快捷键:用 TaoToken 统一 Key 打通 AI 补全配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 22:46:21

Open Computer Use 安装与使用方法全解:从零配置到跑通第一个任务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 22:46:20

IP协议必会知识

1. IP协议基本了解1.1 基本概念主机&#xff1a;有IP&#xff0c;但不能进行路由控制。路由器&#xff1a;既有IP又可以路由控制。节点&#xff1a;主机和路由器的统称。1.2 头格式4位版本号&#xff1a;ipv44位头部长度&#xff1a;代表有多少个32个比特位&#xff0c;即lengt…

作者头像 李华