首页 > 其他分享 >static、extern、inline 说明符和链接属性

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

时间:2023-11-29 21:37:35浏览次数:40  
标签:int 作用域 cpp 说明符 static extern 链接

概述 - Overview

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

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

存储期 - Storage duration

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

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

存储期决定了一个对象在给定时刻是否有效。比如,具有静态存储期的对象,在程序开始和结束之间的任意时刻有效;具有动态存储期的对象,在分配和解分配之间的任意时刻有效。(关于不同存储期的对象是如何进行初始化的,那又是另外的话题了)

链接 - 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 说明符实际上既不影响存储期,也(几乎)不影响链接属性。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 构建(未开启优化),并对构建出来的可执行文件进行反汇编,可以看到:

image

image

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

image

image

再来看看使用 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 构建并执行反汇编。我们可以看到:

image

image

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

image

image

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

标签:int,作用域,cpp,说明符,static,extern,链接
From: https://www.cnblogs.com/Code-For-What/p/17865791.html

相关文章

  • static_cast<>
    static_cast<>在C++中有以下几个优点:类型检查:static_cast<>在编译时期进行类型检查,如果类型转换不合法,编译器会报错。这是它相比C风格类型转换的一个重要优势,因为C风格类型转换不进行类型检查,可能会导致类型转换错误代码清晰:static_cast<>的出现,使得类型转换更容易在代码......
  • docker故障:driver failed programming external connectivity on endpoint
    故障现象Errorresponsefromdaemon:driverfailedprogrammingexternalconnectivityonendpointjenkins(ffdc7c9cda72c575d6b045574d1432b256603a3d986a05da319ab7f3af233755):(iptablesfailed:iptables--wait-tnat-ADOCKER-ptcp-d0/0--dport50000-jDN......
  • (二十九)C#编程基础复习——static静态成员
    在C#中,我们可以使用static关键字声明属于类型本身而不是属于特定对象的静态成员,因此不需要使用对象来访问静态成员。在类、接口和结构体中可以使用static关键字修饰变量、函数、构造函数、类、属性、运算符和事件。注意:索引器和析构函数不能时静态的。若要定义某个成员时使用sta......
  • extern "C":实现C++和C的混合编程
    原文:https://c.biancheng.net/view/8064.html通过《C语言和C++到底有什么关系?》一节的学习,读者已经了解了C++和C语言之间的关系。简单的理解,C++就是在C语言的基础上增加了一些新特性,从大的方面讲,C++不仅支持面向过程编程,还支持面向对象编程和泛型编程;从小的方面讲,C++还......
  • Java中static、final、static final的区别
    finalfinal可以修饰:属性,方法,类,局部变量(方法中的变量)final修饰的属性的初始化可以在编译期,也可以在运行期,初始化后不能被改变。final修饰的属性跟具体对象有关,在运行期初始化的final属性,不同对象可以有不同的值。final修饰的属性表明是一个常数(创建后不能被修改)。final修饰的方......
  • final 和 static
    //1.final常量,需要在定义的时候进行初始化;每个对象的初始化不一样;//2.staticfinal常量可以在定义的时候初始化;也可以在static块中初始化;该种定义该类的对象使用的值一致。//3.被static修饰的变量,叫静态变量//4:静态区:方法区中一个模块,用于存放静态变量和静态代码块,也就是st......
  • extern关键字的用法
    extern关键字的理解extern是C/C++语言中的一个关键字,用于声明一个变量或函数具有外部链接性(externallinkage),即这些变量或函数可以被其他文件访问。在C/C++中,如果我们需要在不同的源文件中共享变量或函数,可以使用extern关键字来声明变量或函数。当我们在一个源文件中声明一个......
  • Troubleshooting ‘Externally Managed Environment’ Error in Debian 12 Pip3 Insta
    https://medium.com/@kiena/troubleshooting-externally-managed-environment-error-in-debian-12-pip3-installation-439d62e5a970 WhenworkingwithPythononDebian12,youmayencounteranerrormessageregardingan‘ExternallyManagedEnvironment’.Thise......
  • static
     static:叫静态,可以修饰成员变量,成员方法。类变量:属于类,与类一起加载一次,在内存中只有一份,可以被类和类的所有对象共享。实例变量:属于对象,每个对象中都有一份,只能用对象访问。 ......
  • JAVA中static关键字的使用
    static是静态的意思,是一个修饰符,就像是一个形容词,是用来形容类,变量,方法的。static修饰变量,这个变量就变成了静态变量,修饰方法这个方法就成了静态方法,static关键字方便在没有创建对象的情况下来进行调用(方法/变量)。1.static修饰变量通过static修饰成员变量,我们可以不用创建对象......