static、extern、inline 说明符和链接属性

这篇具有很好参考价值的文章主要介绍了static、extern、inline 说明符和链接属性。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

概述 - Overview

在我初学 C++ 时,staticinlineextern 可能是最令我迷惑的 C++ 说明符,原因是它们在不同的语境下会发挥不同的作用,而且某些说明符的含义已经和以前不同,这加剧了我在查询资料时的困扰。所以今天决定好好总结一下。

首先要介绍 C++ 的两个概念:存储期链接

存储期 - Storage duration

C++ 程序中,任何对象[1]都有一个存储期,它是下列四个之一:

  • 自动存储期:对象在代码块开始时分配,代码块结束时解分配。
  • 静态存储期:对象在整个程序开始时分配,程序结束时解分配。
  • 线程存储期:对象在某个线程开始时分配,线程结束时解分配。
  • 动态存储期:对象使用某些特定的表达式来进行分配和解分配。

存储期决定了一个对象在给定时刻是否有效。比如,具有静态存储期的对象,在程序开始和结束之间的任意时刻有效;具有动态存储期的对象,在分配和解分配之间的任意时刻有效。

链接 - Linkage

在本文中,术语“链接” \(≠\) 程序构建时所需要的名为“链接”的步骤,它只是 C++ 标准中定义的一种属性。如果程序中一个名字指代对象、引用、函数、类型、模板、命名空间或值,那么这个名字就可以具有链接属性(不是一定具有哦,只是可以具有)。

如果一个名字具有链接属性,那么它指代的实体,和另一个作用域中相同名字所指代的实体,是同一个实体。简而言之,就是允许一个名字在多个作用域中出现且它们都代表同一个实体。换句话说,我们可以在声明该名字的作用域以外的地方使用它。

链接属性还有两种不同的“等级”:

  • 内部链接:名字可在当前翻译单元中的所有作用域中使用。
  • 外部链接:名字可在其他翻译单元中的作用域中使用。

怎么让一个名字具有链接属性并指定它是内部或外部链接?简而言之,可以使用 staticextern 说明符来控制(好吧,这里很不准确,因为链接属性的详细规则比较复杂、琐碎,它不仅和 staticextern 有关,还和其他事情有关,在这里我只关注部分情形)。

声明说明符 - specifiers

回到本文的标题上来,staticexterninline 都是声明说明符,在声明时使用(当然不是任何声明都能用),并赋予某种性质。

如果硬要说它们有什么共同点,那就是它们以不同程度影响我们在翻译单元中使用一个名字的方式。

staticextern 说明符影响前面介绍的存储期和链接属性;inline 说明符不影响存储期,也几乎不影响链接,但它影响另一种重要的规则。下面就来依次说明:

static

static 说明符主要在三种地方使用:

  • 在命名空间作用域中,声明具有静态存储期内部链接的成员(当然,函数不是对象,所以没有存储期一说,这里只是为了书写上的方便,下面不再额外说明)。
// main.cpp
namespace A {
	static int a; // 在命名空间 A 中
	static void b() { } // 在命名空间 A 中
}
static int c; // 在全局命名空间中
int main() { /*...*/ }
  • 在块作用域中,定义具有静态存储期且只会初始化一次的变量。在块作用域中,有没有 static 说明符不影响链接属性。
// main.cpp
void foo() {
	static int a; // 在块作用域中
}
  • 在类作用域中,声明具有静态存储期的类静态成员。如果类自身具有外部链接,那么类的静态数据成员也有外部链接。
// main.cpp
struct A {
	static int a;
	static void b() { }
}
int main() { /*...*/ }

简单而言,用于声明类成员时,它声明一个静态成员。当用于声明对象时,它指定静态存储期。在命名空间作用域内声明时,它指定内部链接。

extern

extern 说明符的用途并不复杂:在命名空间作用域中,声明具有静态存储期外部链接的成员。它只能用于修饰(类成员或函数形参之外的)变量和函数声明。

// main.cpp
extern int i; // 变量 i 具有静态存储期和外部链接
extern void foo() { }// 函数 foo 具有外部链接

Tips: 在命名空间作用域中声明的对象,即使不带 staticextern 说明符,也自动拥有静态存储期。在命名空间作用域中声明的函数或非 const 变量(且没有被 static 修饰),即使不带 extern 说明符,也自动具有外部链接。

这使得我们可以在不同的翻译单元分享同一个变量或函数,而不必包含头文件:

// foo.cpp
int factor = 1; // 默认具有静态存储期和外部链接
int foo(int a, int b) { // 默认具有外部链接
	return (a + b) * factor;
}

// main.cpp
int factor; // 错误! 违反单一定义原则,因为这样做是定义而非单纯声明
extern int factor; // 正确! 应使用 extern 声明
int foo(int a, int b); // 正确!具有外部链接,且未违反单一定义原则

int main() {
	factor = 2;
	foo(1, 2);
}

除此之外,extern 说明符还有其他作用(控制语言链接,显式实例化模板),但与本文的关注点关系不大,所以不加讨论。(话说回来,我感觉似乎没有在块作用域中使用 extern 修饰变量的需求?绝大多数时间都在命名空间作用域中使用它)

inline

inline 说明符实际上既不影响存储期,也(几乎)不影响链接属性[2]inline 说明符的用处相当直接,就是将函数或变量声明为内联。至于内联的具体作用将在下面解释。用法简单粗暴,直接在声明处加上 inline 说明符即可。有一点需要注意:具有静态存储期的变量(静态类成员或命名空间作用域变量)才能声明为内联变量。

Tips: 下列情形会隐式将函数或变量内联:

  • 如果一个函数的定义在 class/struct/union 内部,那么它是内联函数。
  • 如果一个函数声明有 constexpr,那么它是内联函数。
  • 如果一个类的静态成员变量声明有 constexpr,那么它是内联变量。

内联函数和内联变量有一个必须满足的条件:它们的定义必须在访问它的翻译单元中可达。

这个条件看起来微不足道。不过若是能进一步满足"具有外部链接"这个看起来同样微不足道(但实际上隐藏了诸多细节)的条件,我们将会获得重量级的好处!

那就是,内联函数和变量可以在程序中多次定义!只要它们每个定义都出现在不同的翻译单元,且它们均等同。这对喜欢只用头文件来分发库代码的人来说是莫大的福音:

// lib.h
inline int add(int a, int b) {
	return a + b;
}

// source1.cpp
#include "lib.h"
int foo1() {
	return add(1, 2);
}

// source2.cpp
#include "lib.h"
int foo2() {
	return add(3, 4);
}

不需要额外的步骤,只需要包含头文件,就可以方便地使用其他人编写的功能函数或变量。

有的人可能会说,即使不用 inline 说明符,使用 static 也能达到类似的效果:

// lib.h
static int add(int a, int b) {
	return a + b;
}

// source1.cpp
#include "lib.h"
int foo1() {
	return add(1, 2); 
}

// source2.cpp
#include "lib.h"
int foo2() {
	return add(3, 4); 
}

某种程度上的确如此。然而,现在应该清楚地认识到,两者使用的是不同的语言机制:

对于 static 说明符:通过包含头文件,source1.cppsource2.cpp 在各自的翻译单元内都能访问到名字 add。在这里,我们并没有多次定义一个 add 函数,相反,我们在 source1.cppsource2.cpp 中各自定义了不同add 函数,尽管它们看起来一模一样。换言之,代码中的 add(1, 2)add(3, 4),它们实际引用了不同的函数。而正是多亏了 static 说明符赋予的内部链接属性,它们各自在外部不可见,因此不会造成重定义。

对于 inline 说明符:通过包含头文件,source1.cppsource2.cpp 在各自的翻译单元中也能访问到名字 add,而且该名字具有外部链接。因此在这里,我们确实多次定义了同一个实体—— add 函数。而多亏了 inline 说明符,这种行为被允许,所以也不会造成重定义。

这两种情况的微妙差别,还影响了实现。这可以从执行编译、链接后的二进制文件中看出:

假设我们有以下文件,这是使用 static 说明符的情形:

// Lib.h
static int foo() { return 114514; }

// Src1.cpp
#include "Lib.h"
int main() { return foo(); }

// Src2.cpp
#include "Lib.h"
int fn() { return foo(); }

使用 Visual Studio 构建(未开启优化),并对构建出来的可执行文件进行反汇编,可以看到:

static、extern、inline 说明符和链接属性

static、extern、inline 说明符和链接属性

它们调用的 foo 函数,其地址不同。而二进制文件里确实存在两个长得“一样”的 foo 函数:

static、extern、inline 说明符和链接属性

static、extern、inline 说明符和链接属性

再来看看使用 inline 说明符的情形:

// Lib.h
inline int foo() { return 114514; }

// Src1.cpp
#include "Lib.h"
int main() { return foo(); }

// Src2.cpp
#include "Lib.h"
int fn() { return foo(); }

除了改用 inline,和之前没什么区别。让我们再用 Visual Studio 构建并执行反汇编。我们可以看到:

static、extern、inline 说明符和链接属性

static、extern、inline 说明符和链接属性

它们指向相同的地址。而二进制文件中,也只有一处 foo 函数的实现。

static、extern、inline 说明符和链接属性

static、extern、inline 说明符和链接属性

好了,这就是这篇文章的全部内容,如果出现任何错误,请务必让我知道!


  1. 以下实体不是对象:值,引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。 ↩︎

  2. 命名空间作用域的内联 const 变量默认具有外部链接。 ↩︎文章来源地址https://www.toymoban.com/news/detail-747491.html

到了这里,关于static、extern、inline 说明符和链接属性的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C6064:缺少“scanf_s”的整型参数(对应于转换说明符“2”

    学习过程遇见的问题,出现两个警告,一个是关于 C6054 :可能没有为字符串“name”添加字符串零终警告,另一个是关于 C6064 :缺少“scanf_s”的整型参数(对应于转换说明符“2”,因为篇幅太长,本片只介绍C6064:缺少“scanf_s”的整型参数(对应于转换说明符“2”,警告C605

    2024年02月11日
    浏览(47)
  • error C4430 缺少类型说明符 - 假定为 int。注意 C++ 不支持默认 int

    出现原因:两个类头文件相互包含 使用声明类代替头文件包含

    2024年02月14日
    浏览(33)
  • 解决C++遇到的未定义标识符 “string“、未定义标识符 “cout“、“name”: 未知重写说明符错误

    目录 解决C++遇到的未定义标识符 \\\"string\\\"、未定义标识符 \\\"cout\\\"、“name”: 未知重写说明符错误 1. 未定义标识符 \\\"string\\\" 2. 未定义标识符 \\\"cout\\\" 3. “name”: 未知重写说明符错误 总结 1. 未定义标识符 \\\"string\\\" 2. 未定义标识符 \\\"cout\\\" 3. “name”: 未知重写说明符错误 在C++编程中,我们可

    2024年02月06日
    浏览(117)
  • 《C和指针》笔记11: external和internal链接属性

    当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数 链接在一起,形成可执行程序 。然而, 如果相同的标识符出现在几个不同的源文件中时,它们是像Pascal那样表示同一个实体?还是表示不同的实体? 标识符的链接属性

    2024年02月10日
    浏览(29)
  • extern static 在linux 和 qt下差别

     从五个点来说 1.p3.h 中 静态定义一个const的int 变量并且赋值 2.p5.h 声明函数test2的定义 3. 直接extern 引用声明 test1() 函数 而不是像p5.h一样 把函数声明写到头文件 在别的.c文件直接包含头文件  第二点和第三点 是引用声明函数的两种用法 4.main函数 中static静态定义一个const变量

    2024年01月20日
    浏览(40)
  • extern\const\static的使用详解

    1.extern 利用extern,可以在一个文件中引用另一个文件中定义的变量或者函数, extern  可以应用于全局变量、函数或模板声明。   extern  具有四种含义,具体取决于上下文: 在非  const  全局变量声明中, extern  指定变量或函数在另一个转换单元中定义。 必须在

    2024年02月01日
    浏览(33)
  • 变量的四大存储类型static extern auto register

    在C语言中变量和函数有数据类型和存储类型两个属性,因此变量定义的一般形式为: 存储类型 数据类型 变量名表; C语言提供了一下几种不同的存储类型: (1) 自动变量(auto) (2) 静态变量(static) (3) 外部变量(extern) (4) 寄存器变量(register) 下面一个一个介

    2024年02月04日
    浏览(43)
  • static,const,volatile,extern,register关键字深入解析

    ✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!! 📃个人主页:@rivencode的个人主页 🔥系列专栏:玩转C语言 💬保持学习、保持热爱、认真分享、一起进步!! 我们都知道一个源文件要生成我们计算机课执行的文件要经过: 源文件(test.c)—预编

    2023年04月08日
    浏览(35)
  • C语言中volatile/register/const/static/extern/auto关键字的作用

    目录 一、volatile 二、register详解 三、const详解 四、static详解 五、extern详解 语法 作用 六、auto详解 突然想总结一下这些的作用,灵活使用这些对程序的可靠性和速率都有提高 volatile是防止编译器优化,如果是高频繁的变量编译器会自动将变量放到寄存器中,但是有的变

    2024年02月07日
    浏览(67)
  • css中新型的边框设置属性border-inline

    border-inline 是 CSS Logical Properties and Values 模块中的一个属性,用于控制元素在流内(inline)方向上的边框。该模块旨在提供与书写模式(writing mode)无关的布局和样式描述方式,使得元素在不同书写模式(如ltr、rtl、ttb等)下能够统一、适当地处理边框,它和 border-block 的区别

    2024年04月28日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包