0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入理解C++ “static”关键字

CPP开发者 来源:CPP开发者 2023-08-14 12:25 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

大多数 C++ 关键字使用起来都是比较简单的,但也有少数相对复杂,static 便是其中的一个代表。

标准往往会避免为语言增加新的关键字,而是复用已有的。这使得 static 如今已存在十几种不同的意思,可以修饰全局,也可以修饰局部;可以修饰函数,也可以修饰变量;还可以和 inline、const、constexpr、constinit 等关键字组合起来使用。

许多 C++ devs 对其都只处于一个浅层次的理解,不全面也不深入,用来明所以。通过本文能够弥补此部分知识点。

1

内存布局

程序由指令和数据构成,指令和数据又存储在不同的段内,我们首先要了解执行程序的内存布局,才能从一个更底层的视角来理解 static。

常见的程序分段如表格所示:

Sections Meaning
.code/.text 代码段,存储编译后的机器指令
.data 数据段,通常存储全局变量、静态局部变量和常量
.bss Block Started by Symbol的缩写,用来存储未初始化的全局变量和静态局部变量,实际并不占据空间,仅是请求加载器在程序加载到内存时,预留一些空间
.rodata read-only data的缩写,通常存储静态常量

程序分段有利于区分指令和数据,指令通常是只读的,数据是可读写或只读的,分到不同的段内,设置不同的权限,可以防止程序指令被无意修改。

一个程序编译后的内存布局图可能如下图所示:

89815cb8-3a56-11ee-9e74-dac502259ad0.jpg

可以看到,程序被分成了六个主要的内存区域。因为代码指令、数据段等往往是固定大小的,所以处于低地址;堆栈可能会在程序执行时动态增长,所以处于高地址。

同时,不同段的数据还代表着不同的存储时期。

2

存储时期

C++中不同的数据存在四个存储时期,分别为 automatic, static, thread 和 dynamic。

automatic 主要指的就是栈上的数据,它能够在进入某个作用域时自动申请内存,并在离开时自动释放内存。


static 指的主要是 .data/.bss/.rodata 段的数据,这些数据在程序执行时就申请内存,等到程序结束时才释放。


而 thread 存储时期是 C++11才有的,只有 thread_local 修饰的数据才属此类,它们在线程开始时申请内存,线程结束时释放内存。


dynamic 则表示堆上的数据,也就是使用 new/malloc 申请的内存,这些内存必须手动释放。当然,通过智能指针这些数据的生命周期也能够自动管理。

不要把这些存储时期与编译期/运行期混淆,它们是编译原理的概念,按那种尺度来划分,则存在编译期-加载期-运行期三个不同的时期。

编译期指的是编译器处理代码的时期,该时期的数据符号地址被翻译成绝对地址,是最早就确定的数据。constexpr/constint 所修饰的数据,一般就是在这一时期分配的内存。

编译后的程序存储在硬盘上,准备执行时操作系统需要将它们读取到 RAM 中,这个时期就叫加载期。.data/.rodata 段的数据就是在这一时期分配内存的,一个常见的误区就是认为 static 数据是处于编译期。

运行期是程序已经运行,指令已经开始被CPU处理了。一些额外的内存需要分配给现在才存在的数据,比如 .bss 和堆栈数据就属于这一时期。

内存布局和存储时期搞清楚了,下面需要理解链接的基本概念。

3

链接

不同的数据作用范围也不尽相同,如何调整数据的作用范围?就是通过那些变量修饰符,它们能改变数据的链接方式。

每个 .cpp/.cc/.cxx... 文件称为一个 TU(Translation Units,翻译单元),有些数据只在当前 TU 使用,有些数据还需要在其他 TUs 使用,前者称为内部链接,后者称为外部链接。

每个 TU 就是一个模块,链接就是将这些模块组合起来,同时保证其中所引用的各种符号都在正确的位置上。

只有在当前 TU 中使用的数据才需要内部链接,局部的那些数据属于无链接。我们需要关注的是一个变量的内部链接和外部链接是如何指定的。

先说内部链接,这些名称能够在整个 TU 使用,以下是其规则:

  • 命名空间下以 static 修饰的变量、变量模板、函数和函数模板;

  • 命名空间下以 const 修饰的变量;

  • 匿名 union 的数据成员;

  • 匿名空间下的所有名称。

再说外部链接,这些名称能够在不同 TUs 间使用,外部链接的名称甚至能够和其他语言生成的 TUs 链接。规则如下:

  • 命名空间下没有以 static 修饰的函数,没有额外修饰的变量和以 extern 修饰的变量;

  • 命名空间下的枚举名称;

  • 命名空间下的类名称,包含它们的成员函数、(const) static 数据成员、嵌套类/枚举、首次引入的友元函数;

  • 命名空间下以 static 修饰的非函数模板;

  • 首次在block scope 下的函数名、以 extern 修饰的变量名。

暂时先留个大概印象,后面还会再次以具体例子介绍有些规则。

4

以 static修饰变量

前置概念介绍完了,下面从不同方面来进行讨论 static 关键字。本节关注于修饰变量,这又有全局和局部之分。

4.1

以static修饰全局变量

全局变量处于 static存储时期,也对应于加载期,在 main()执行之前就已为这些变量分配了内存。

如果一个全局变量被 extern 修饰,则它具有外部链接,能够被其他 TUs 使用。相反,如果一个全局变量被 static 修饰,它具有内部链接,只能在当前 TU 使用。

一个例子:

//tu-one.cpp
externintvar_1=42;//externallinkage
staticintvar_2=24;//internallinkage

//tu-two.cpp
#include

//referstothevar_1definedinthetu-one.cpp
externintvar_1;

intmain(){
std::cout<< var_1 << "
";//prints42
}

若是再考虑组合 const 进行修饰,情况则又不相同。

如果一个全局变量没有使用const 修饰,那么它默认就有 extern 链接,无需多此一举再加上 extern 修饰。而对于这样一个变量,如何改变它的外部链接方式?只需使用 static 修饰,就可以将它变成内部链接。

如果一个全局变量使用了 const/constexpr 修饰,则它默认就有了 static 链接。此时如果再加上 static 修饰,也是多此一举。其他文件此时无法访问该全局变量,如何改变呢?前面加上 extern 修饰,就可以让它变成外部链接。

以上内容的一个例子:

//tu-one.cpp
intvar_1=42;//externallinkage by default
externintvar_2=42;//sameasvar_1,butit'sredundant.
staticintvar_3=42;//internallinkage
constintvar_4=42;//internallinkage by default
staticconstintvar_5=42;//sameasvar_4,butit'sredundant.
externconstintvar_6=42;//externallinkage
constexprintvar_7=42;//internallinkagebydefault
staticconstexprintvar_8=42;//sameasvar_7,butit'sredundant.

4.2

以static修饰局部变量

局部变量也要分情况讨论一下,先说函数中的局部变量。

函数中局部变量的存储时期为 automatic,此类变量无链接,使用时在栈上自动分配内存,离开作用域时自动释放,只能在当前作用域使用。

如果为这样的局部变量加上 static,就将其存储时期由 automatic 改变成了 static,生命周期遍及整个程序的生命周期。这种变量实际是先在 .bss 段预留了空间,等到首次进入该函数,才真正为其分配内存,此时初始化的时机就不是加载期,而且延迟到了运行期,所以这种方式也叫惰性初始化。

这种局部静态变量就相当于全局变量,不同之处在于它是无链接,可见性仅在当前函数,而且可以延迟初始化。

4.3

以static修饰成员变量

如果一个类成员变量以 static修饰,那么该变量只是一个声明,需要额外提供定义,类的所有对象共享此类变量。

classS{
staticintx;//declaration
};

intS::x=0;//definition,initialize outside the class

为什么需要在外部定义呢?

因为 static 对象必须满足 ODR(One Definition Rule),而类一般是在头文件中声明,该头文件可能会被多个 TUs 包含,每个对象必须具备唯一的定义,否则在编译链接时会出现问题。所以将它作为一个声明,定义在类外部单独指定。

但是在某些时候也可以不用定义,比如:

//Examplefromcppref

structS
{
staticconstintx=0;//staticdatamember
//adefinitionoutsideofclassisrequiredifitisodr-used
};

constint&f(constint&r);

intn=b?(1,S::x)//S::xisnotodr-usedhere
:f(S::x);//S::xisodr-usedhere:adefinitionisrequired

只有在 ODR-used 时才必须要提供定义,在不需要 lvalue 的表达式中,它可以直接使用 S::x 的值,此时经历了 lvalue-to-rvalue 的隐式转换。相反,在需要 lvalue 的表达式中,则必须提供定义。

注:ODR-used 是标准用来指必须为实体提供定义的术语,因为它不是必须的,需要依赖情境讨论,所以不单独使用 used 来描述。比如一个虚函数是非 ODR-used,而一个纯虚函数是 ODR-used,使用时必须提供定义。模板只在使用时才实例化,这里的使用准确的描述也应该是 ODR-used。

如果嫌在外部定义麻烦,在 C++17 可以采用 inline 来光明正大地违背 ODR,它能够告诉链接器,我想在多个 TUs 之间拥有相同的定义。

classS{
//since C++17
inlinestaticintx=42;
};

在 C++20,由于 constexpr 会隐式 inline,所以还可以这么写:

classS{
//since C++20
staticconstexprintx=42;
};

另外,在 C++98,如果以 static const 修饰一个整型成员数据,那么也可以在类内直接初始化,并且可以保证初始化是在编译期完成的

//C++98
structS{
staticconstintx=42;//OK
constinty=42;//since C++11, default member initializer
};

对于非 static 数据成员,自 C++11 开始支持 default member initializer,于是也可以直接在类内直接初始化。

5

以static修饰函数

函数也分全局函数和成员函数,以 static 修饰时也要分别讨论。

5.1

以static修饰全局函数

情况下,全局函数默认是外部链接,可以通过前置声明在多个 TUs 使用。

如果以 static 修饰全局函数,则将其链接方式变为内部链接,只能在当前 TU 使用。

一个小例子:

//tu-one.cpp
#include

staticvoidfoo(){
std::cout<< "internallinkage
";
};

voidbar(){
std::cout<< "externallinkage
";
}

//tu-two.cpp
externvoidfoo();
externvoidbar();//referstothebar()definedinthetu-one.cpp

intmain(){
foo();//Error,undefinedreferenceto'foo()'
bar();//OK
}

5.2

以static修饰成员函数

之前在【洞悉C++函数重载决议】里已经讲解过本节内容。

以 static 修饰的成员函数增加了一个隐式对象参数,它并不是 this 指针,而是为了重载决议能够正常运行所定义的一个可以匹配任何参数的对象参数。这样的成员函数无法访问其他的非静态成员名称,因为那些名称都与对象绑定。

当时编写的一个示例:

structS{
voidf(long){
std::cout<< "memberversion
";
}

staticvoidf(int){
std::cout<< "staticmemberversion
";
}
};

intmain(){
Ss;
s.f(1);//staticmemberversion
}

6

static修饰变量对 Lambdas捕获参数的影响

如果是全局变量,那么 Lambdas 无需捕获便可以直接使用:

intx=42;

intmain(){
//youdon'tneed tocapturea globalvariable
[]{returnx;}();
}

但如果是局部变量,由于它的存储时期为 automatic,就必须捕获才能使用:

intmain(){
intx=42;
//youhavetocapturealocalvariable
[&x]{returnx;}();
}

但如果使用 static 修饰该局部变量,就无需再进行捕获

intmain(){
staticintx=42;

//OK
[]{returnx;}();
}

同理,const/constexpr/constinit 修饰的变量在某些时候也无需再进行捕获:

constinitintm=42;

intmain(){
constexprintx=42;
constintn=42;

//OK
[]{returnm+x+n;}();
}

7

static constexpr, static constinit

请大家注意我上节最后一句的用词,是在某些时候」也无需进行捕获,使用那些词修饰并非一定是可以无需捕获。

准确地说,非 ODR-used 的数据无需捕获,它可以直接把那个常量隐式捕获。

intmain(){
constexprstd::string_viewx="foo";
[]{x;}();  //OK,xisnotodr-used
[]{x.data();}();//error:x.data()isodr-used
}

此时就可以借助 static constexpr,就可以强保证 Lambdas 可以隐式捕获该数据:

intmain(){
staticconstexprstd::string_viewx="foo";
[]{x;}();//OK,xisnotodr-used
[]{x.data();};//OK,x.data()isnot odr-used
}

可以理解为此时捕获的不是 lvalue,而是经由 lvalue-to-rvalue 的那个值。

static constinit 也是同理,事实上 constinit 在局部使用必须添加 static,它只能修饰静态存储期或是线程存储期的数据。

在类中使用 static constexpr 修饰数据,可以保持数据既是编译期,又能够所有对象共享一份数据。

template<intN>
structS{
staticconstexprintx=N;
};

constinit 和 constexpr 大多时候都是同理,只是前者是可读可写,后者是只读,除非有不同的意义,否则讨论的 constexpr 用法也适用于 constinit。后文不再提及。

static constexpr 的另一个用处是强保证」发生于编译期。constexpr 本身只是「弱保证」,它并不一定发生于编译期。

它们的其他用处见第9节。

8

static const vsconstexpr

前面讲过,C++ 以 const 修饰全局变量会默认为内部链接,所以 static 可以省略不写。但是局部变量不可省,因为 static 修饰局部变量时的意义是改变局部变量的存储时期,此时的 static const必须完整写出

全局变量本身的存储时期就是 static,加上 const 表示只读,此时以 static 修饰的意义是指定其链接方式。

局部变量本身的存储时期是 automatic,无链接,加上 const 依旧表示只读,此时以 static 修饰的意义是指定其存储时期。

所以对于全局 (static) const 数据来说,是在加载期就分配内存了(如存储时期那节所强调,不要误以为它发生在编译期)。而对于局部 static const 数据来说,它实际分配内存是在首次使用时,实际可能发生于运行期。

constexpr 修饰的变量则不同,它们发生于编译期,这其实是要早于 static const 修饰的变量。

但是经过优化,它们展现的效果是差不多的,相对来说,更推荐使用 constexpr。

intmain(){
staticconstintm=42;//sinceC++98
constexprintn=42;//sinceC++11

returnm+n;
}

9

Solvingthe"Static Initialization Order Fiasco"

SIOF是存储时期为 static 的这类数据在跨 TUs 时相互依赖所导致的问题,因为不同 TUs 中的这些数据初始化顺序没有规定,引用的数据可能还没初始化。

因此全局变量、静态变量、静态成员变量都可能会引起这个问题。

看如下例子:

//tu-one.cpp
autoget_value(intval)->int{
returnval*2;
}

autotu_one_x=get_value(42);

//tu-two.cpp
#include

externinttu_one_x;
autotu_two_x=tu_one_x;

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

tu_one_x 跨 TUs 使用,tu_two_x 依赖于它。在编译时初始化 tu_two_x,如果此时 tu_one_x 还未初始化,那么结果就偏离预期。

这依赖于编译顺序,你只有50%的几率获得预期结果。

89a16a76-3a56-11ee-9e74-dac502259ad0.png

解决策略一是使用局部静态变量替代全局变量。前面讲过,局部静态变量相当于全局变量,而且可以延迟初始化。

//tu-one.cpp
autoget_value(intval)->int{
returnval*2;
}

//autotu_one_x=get_value(42);
autoget_x()->int&{
staticautox=get_value(42);
returnx;
}

//tu-two.cpp
#include

autoget_x()->int&;
autotu_two_x=get_x();

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

局部静态静态会在首次访问时初始化,因此在初始化 tu_two_x 之前,就先把 tu_one_x 初始化了。于是不会再有 SOIF:

89b8a81c-3a56-11ee-9e74-dac502259ad0.png

解决策略二是借助 constinit。前面依旧讲过,constinit 发生于编译期,而存储时期为 static 的数据实际发生于加载期,SOIF 只是加载期的问题,只要将初始化时期错开,体现一前一后,就能够指定顺序,从而解决该问题。

//tu-one.cpp
constexprautoget_value(intval)->int{
returnval*2;
}

constinitautotu_one_x=get_value(42);

//tu-two.cpp
#include

externconstinitinttu_one_x;
autotu_two_x=tu_one_x;

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

此时 tu_one_x 初始化于编译期,而 tu_two_x 初始化于加载期,所以也不会存在SOIF。

但是你无法使用 extern constexpr,像如下这样写会编译失败:

//tu-two.cpp
externconstexprinttu_one_x;//error:notadefinition
autotu_two_x=tu_one_x;

因为 constinit 修饰的数据是可读可写的,而 constexpr 修饰的数据是只读的,定义时必须要给初值。这里这种写法被视为只是一个声明。

虽然无法使用 extern constexpr,但也是可以借助 constexpr 来解决 SOIF 的,只不过要把所有的实现全部放到头文件,然后在另一个实现文件中包含该头文件。本节最后有一个相关例子。

使用 static 修饰的变量与全局变量同理,也提供一个例子:

//tu-one.h
structS{
staticinttu_one_x;//declaration
};

//tu-one.cpp
#include"tu-one.h"

autoget_value(intval)->int{
returnval*2;
}

//definition
intS::tu_one_x=get_value(42);

//tu-two.cpp
#include"tu-one.h"
#include

staticautotu_two_x=S::tu_one_x;

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

它们的存储时期也是 static,所以也会产生 SOIF。tu_two_x 的初始化依赖于 S::tu_one_x,因此你也有 50% 的几率得到正确结果。

89dbcba8-3a56-11ee-9e74-dac502259ad0.png

通过使用 static constinit,也得以解决此问题。

//tu-one.h
structS{
staticconstinitinttu_one_x;//declaration
};

//tu-one.cpp
#include"tu-one.h"

constexprautoget_value(intval)->int{
returnval*2;
}

//definition
intS::tu_one_x=get_value(42);

//tu-two.cpp
#include"tu-one.h"
#include

staticautotu_two_x=S::tu_one_x;

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

使用 constinit,所有相关操作也都得是编译期完成,所以 get_value() 也加上了 constexpr 修饰。那么 static 在此时主要指的是所修饰数据在所有对象之间共享,constinit 将它的初始化时间提前到了编译期。

89f06126-3a56-11ee-9e74-dac502259ad0.png

但是你不能把 tu_two_x 也以 static constinit 修饰,因为编译期的值发生在链接之间,在编译期就得确定,而 tu_two_x 的值又来自于另一个文件,编译时根本就不知道所依赖的那个常量值。

同理这里也可以使用 static constexpr,但是 constexpr 没有 constinit 灵活,它是 const 的,所以定义时就必须跟着初始化。

//tu-one.h
constexprautoget_value(intval)->int{
returnval*2;
}

structS{
staticconstexprinttu_one_x=get_value(42);//definition
};

//tu-two.cpp
#include"tu-one.h"
#include

staticautotu_two_x=S::tu_one_x;

intmain(){
std::cout<< "tu_two_x:"<< tu_two_x << "
";
}

结果和使用 static constinit 完全相同。

10

static inline

static 能够指示编译器数据只在单个 TU 使用,即内部链接;与之相反,inline 能够指示编译器数据需要在多个 TUs 使用,此时即使违背了 ODR,也不应该报错,属于外部链接。

那如果它们组合使用,会有怎样的效果?

让我们写个例子来对比一下不同的情况编译之后到底产生了什么内容。

////Thisexampleisadaptedfromhttps://gist.github.com/htfy96/50308afc11678d2e3766a36aa60d5f75

//header.hpp
inlineintonly_inline(){return42;}
staticintonly_static(){return42;}
staticinlineintstatic_inline(){return42;}

//tu-one.cpp
#include"header.hpp"

autoget_value_one()->int{
returnstatic_inline()+only_inline()+only_static();
}

//tu-two.cpp
#include"header.hpp"

autoget_value_one()->int;
autoget_value_two()->int{
returnstatic_inline()+only_inline()+only_static();
}

automain()->int{
returnget_value_one()+get_value_two();
}

先编译 tu-one.cpp,并查看生成的目标文件的符号表:

lkimuk@cppmore:~/Desktop/demo$g++-ctu-one.cpp
lkimuk@cppmore:~/Desktop/demo$readelf-sWtu-one.o|c++filt-t

Symboltable'.symtab'contains8entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABStu-one.cpp
2:00000000000000000SECTIONLOCALDEFAULT2.text
3:00000000000000000SECTIONLOCALDEFAULT6.text._Z11only_inlinev
4:000000000000000011FUNCLOCALDEFAULT2only_static()
5:000000000000000b11FUNCLOCALDEFAULT2static_inline()
6:000000000000000011FUNCWEAKDEFAULT6only_inline()
7:000000000000001636FUNCGLOBALDEFAULT2get_value_one()

readelf 用来查看 Linux 上可执行文件的结构,-s 表示显示符号表,-W 表示以宽格式显示。c++filt 是 gnu 提供的反 Name Demangling 工具,可以显示未经修饰的函数名称。

可以看到,only_static() 和 static_inline() 的绑定方式都是 LOCAL,表示仅在当前文件可见;only_inline() 的绑定方式为 WEAK,表示该符号可被覆盖,所以在其他文件中也是可见的;get_value_one() 的绑定方式是 GLOBAL,也表示在所有文件中可见。

再来编译 tu-two.cpp,也查看其目标文件的符号表:

lkimuk@cppmore:~/Desktop/demo$g++-ctu-two.cpp
lkimuk@cppmore:~/Desktop/demo$readelf-sWtu-two.o|c++filt-t

Symboltable'.symtab'contains10entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABStu-two.cpp
2:00000000000000000SECTIONLOCALDEFAULT2.text
3:00000000000000000SECTIONLOCALDEFAULT6.text._Z11only_inlinev
4:000000000000000011FUNCLOCALDEFAULT2only_static()
5:000000000000000b11FUNCLOCALDEFAULT2static_inline()
6:000000000000000011FUNCWEAKDEFAULT6only_inline()
7:000000000000001636FUNCGLOBALDEFAULT2get_value_two()
8:000000000000003a29FUNCGLOBALDEFAULT2main
9:00000000000000000NOTYPEGLOBALDEFAULTUNDget_value_one()

和 tu-two.o 的情况相似,这里不再赘述。

具体来看链接之后的情况:

lkimuk@cppmore:~/Desktop/demo$g++tu-one.otu-two.o-omain
lkimuk@cppmore:~/Desktop/demo$readelf-sWmain|c++filt-t

Symboltable'.dynsym'contains6entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FUNCGLOBALDEFAULTUND__libc_start_main@GLIBC_2.34(3)
2:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_deregisterTMCloneTable
3:00000000000000000NOTYPEWEAKDEFAULTUND__gmon_start__
4:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_registerTMCloneTable
5:00000000000000000FUNCWEAKDEFAULTUND__cxa_finalize@GLIBC_2.2.5(2)

Symboltable'.symtab'contains43entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABSScrt1.o
2:000000000000035832OBJECTLOCALDEFAULT3__abi_tag
3:00000000000000000FILELOCALDEFAULTABScrtstuff.c
4:00000000000010700FUNCLOCALDEFAULT14deregister_tm_clones
5:00000000000010a00FUNCLOCALDEFAULT14register_tm_clones
6:00000000000010e00FUNCLOCALDEFAULT14__do_global_dtors_aux
7:00000000000040281OBJECTLOCALDEFAULT25completed.0
8:de00OBJECTLOCALDEFAULT20__do_global_dtors_aux_fini_array_entry
9:00000000000011200FUNCLOCALDEFAULT14frame_dummy
10:dd80OBJECTLOCALDEFAULT19__frame_dummy_init_array_entry
11:00000000000000000FILELOCALDEFAULTABStu-one.cpp
12:000000000000112911FUNCLOCALDEFAULT14only_static()
13:000000000000113411FUNCLOCALDEFAULT14static_inline()
14:00000000000000000FILELOCALDEFAULTABStu-two.cpp
15:000000000000116e11FUNCLOCALDEFAULT14only_static()
16:000000000000117911FUNCLOCALDEFAULT14static_inline()
17:00000000000000000FILELOCALDEFAULTABScrtstuff.c
18:00000000000021d80OBJECTLOCALDEFAULT18__FRAME_END__
19:00000000000000000FILELOCALDEFAULTABS
20:00000000000020040NOTYPELOCALDEFAULT17__GNU_EH_FRAME_HDR
21:de80OBJECTLOCALDEFAULT21_DYNAMIC
22:00000000000040000OBJECTLOCALDEFAULT23_GLOBAL_OFFSET_TABLE_
23:00000000000040280NOTYPEGLOBALDEFAULT24_edata
24:00000000000040180NOTYPEWEAKDEFAULT24data_start
25:00000000000020004OBJECTGLOBALDEFAULT16_IO_stdin_used
26:00000000000000000FUNCWEAKDEFAULTUND__cxa_finalize@GLIBC_2.2.5
27:00000000000011a829FUNCGLOBALDEFAULT14main
28:00000000000040200OBJECTGLOBALHIDDEN24__dso_handle
29:00000000000011c80FUNCGLOBALHIDDEN15_fini
30:00000000000000000FUNCGLOBALDEFAULTUND__libc_start_main@GLIBC_2.34
31:000000000000116311FUNCWEAKDEFAULT14only_inline()
32:000000000000104038FUNCGLOBALDEFAULT14_start
33:00000000000010000FUNCGLOBALHIDDEN11_init
34:00000000000040280OBJECTGLOBALHIDDEN24__TMC_END__
35:000000000000113f36FUNCGLOBALDEFAULT14get_value_one()
36:000000000000118436FUNCGLOBALDEFAULT14get_value_two()
37:00000000000040180NOTYPEGLOBALDEFAULT24__data_start
38:00000000000040300NOTYPEGLOBALDEFAULT25_end
39:00000000000040280NOTYPEGLOBALDEFAULT25__bss_start
40:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_deregisterTMCloneTable
41:00000000000000000NOTYPEWEAKDEFAULTUND__gmon_start__
42:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_registerTMCloneTable

请注意观察,链接之后 only_static() 和 static_inline() 存在两份拷贝,说明它在每个 TU 都存在一份拷贝。而 only_inline() 只存在一份拷贝,说明 inline 的名称的确能够跨 TUs。

全局变量也只存在一份,说明外部链接是起作用的。

于是能够得出结论,static 是内部链接,inline 是外部链接,static inline 和 static 效果一样,此时加上 inline,仅仅是告诉编译器,可以尝试内联一下代码。

11

总结

本文深入全面系统地介绍了 static 关键字的方方面面,涉及内容又多又杂,又广又深。

static 是 C++ 中最复杂的关键字之一,有多达十几种不同的意思,而且涉及编译知识,许多使用形式意思非常细微。

所有相关内容几乎都包含在本文当中,具体总结大家就得自己归纳一下了。


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11216

    浏览量

    222948
  • 存储
    +关注

    关注

    13

    文章

    4696

    浏览量

    89570
  • C++
    C++
    +关注

    关注

    22

    文章

    2122

    浏览量

    76713
  • 编译器
    +关注

    关注

    1

    文章

    1669

    浏览量

    51082

原文标题:深入理解 C++ “static” 关键字

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    C++程序异常的处理机制

    语法 C++异常处理涉及到3个关键字: throw---抛出一个异常,带参数 try---用于标识可能会出现异常的代码段 catch--是用于标识异常处理的代码段 下面以一个例子来说明它们
    发表于 12-02 07:12

    Temu跨境电商按关键字搜索Temu商品API的应用及接口请求示例

    Temu跨境电商按关键字搜索Temu商品API的应用及接口请求示例 Temu跨境电商按关键字搜索Temu商品API的应用场景 Temu跨境电商平台按关键字搜索Temu商品API的主要应用场景包括但不
    的头像 发表于 11-29 15:08 92次阅读

    C语言的编程技巧

    、_Alignas关键字‌:C11标准引入了_Alignas关键字,用于显式指定类型的对齐方式,优化内存访问效率。 ‌5、_Generic关键字‌:
    发表于 11-27 06:46

    哪些场合会用到volatile关键字?

    当你理解 volatile关键字的含义之后,你就能明白,其实很多场合都能用到 volatile关键字。 1.全局变量单片机开发,难免会用到全局变量。一些初级工程师,更是全局变量满天飞。这种情况下
    发表于 11-25 08:19

    天眼查平台关键字搜索企业数据API接口技术指南

    ​ 一、接口概述 天眼查提供的企业搜索API接口,支持通过关键字查询企业工商信息、经营状态等核心数据。该接口采用RESTful架构,返回JSON格式数据。 二、接口认证 调用需使用API密钥认证
    的头像 发表于 11-21 11:48 387次阅读
    天眼查平台<b class='flag-5'>关键字</b>搜索企业数据API接口技术指南

    1688平台关键字搜索商品API接口技术实践指南

    /com.alibaba.product/alibaba.product.search/ 必备参数: { "keyword": "手机", // 搜索关键字 "pageNo": 1, // 页码 "pageSize": 20, // 每页数量 "access_token": "xxx" // 认证令牌 } 二、响应
    的头像 发表于 11-11 14:22 256次阅读
    1688平台<b class='flag-5'>关键字</b>搜索商品API接口技术实践指南

    亚马逊平台根据关键字搜索商品API接口

    ​  在电商开发中,集成亚马逊平台的商品搜索功能是常见需求。通过亚马逊的Product Advertising API(PAAPI),开发者可以根据关键字高效检索商品信息。本技术帖将逐步介绍如何调用
    的头像 发表于 11-05 15:00 119次阅读
    亚马逊平台根据<b class='flag-5'>关键字</b>搜索商品API接口

    简单的内联汇编介绍

    // 非必需 : 可能影响的寄存器或存储器 // 非必需 ); “关键字 asm”为GCC 的关键字,表示进行内联汇编操作。 “关键字 volatile”或“ __volatile__
    发表于 10-30 08:04

    亚马逊 SP-API 深度开发:关键字搜索接口的购物意图挖掘与合规竞品分析

    本文深度解析亚马逊SP-API关键字搜索接口的合规调用与商业应用,涵盖意图识别、竞品分析、性能优化全链路。通过COSMO算法解析用户购物意图,结合合规技术方案提升关键词转化率,助力卖家实现数据驱动决策,安全高效优化运营。
    的头像 发表于 10-17 14:59 344次阅读

    阿里巴巴国际站关键字搜索 API 实战:从多条件筛选到商品列表高效获客

    ​​在跨境电商数据采集场景中,通过关键字精准搜索商品列表是基础且核心的需求。本文将聚焦阿里巴巴国际站的关键字搜索接口,详细介绍如何构建多条件搜索请求、处理分页数据、解析商品列表信息,并提供可直接复用的 Python 实现方案,帮助开发者快速搭建稳定高效的商品搜索功能。
    的头像 发表于 08-20 09:22 682次阅读
    阿里巴巴国际站<b class='flag-5'>关键字</b>搜索 API 实战:从多条件筛选到商品列表高效获客

    micro 关键字搜索全覆盖商品,并通过 API 接口提供实时数据

    micro 关键字搜索全覆盖商品”并通过 API 接口提供实时数据
    的头像 发表于 07-13 10:13 683次阅读

    深入理解C语言:C语言循环控制

    C语言编程中,循环结构是至关重要的,它可以让程序重复执行特定的代码块,从而提高编程效率。然而,为了避免程序进入无限循环,C语言提供了多种循环控制语句,如break、continue和goto,用于
    的头像 发表于 04-29 18:49 1735次阅读
    <b class='flag-5'>深入理解</b><b class='flag-5'>C</b>语言:<b class='flag-5'>C</b>语言循环控制

    static在单片机中的妙用

    不要从字面意思误以为关键字 static 很安静,其实它一点都不安静。这个关键字C 语言里主要有两个作用,下面我们就来介绍一下 C 语言
    发表于 04-02 13:50 1次下载

    单片机编程关键字之volatile

    volatile 修饰的变量是说这变量可能会被意想不到地改变。通常对于程序员而言,单片机 中用的就算常见了volatile 是易变的,不稳定的意思。其实对于很多人来说,根本没见过这个关键字,不 知道
    发表于 04-02 13:47 2次下载

    C++学到什么程度可以找工作?

    C++学到什么程度可以找工作?要使用C++找到工作,特别是作为软件开发人员或相关职位,通常需要掌握以下几个方面: 1. **语言基础**:你需要对C++的核心概念有扎实的理解,包括但不
    发表于 03-13 10:19