%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 1. C/C++中**在不同代码位置、以不同形式定义的变量、函数的存储持续性**分别是什么? 2. **关键字对存储持续性的影响** #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% # 存储类说明符 存储类说明符包括:`auto`, `static`, `extern`, `register`, `thread_local`,`mutable` 「**存储类说明符**」与 「**名称的作用域**」一起控制名称的两个独立属性[^1]: - **==存储持续性==**(storage duration) - **==链接性==**(linkage)。 > [!NOTE] 各个 "存储类说明符" 可出现的位置 > > ![[_attachment/02-开发笔记/01-cpp/cpp 基本概念/cpp-存储类说明符.assets/IMG-cpp-存储类说明符-3EC8D1204079CCF1D736D15451751855.png|805]] > <br> ### `auto` 说明符 > 此关键字自 C++11 起,作用及含义已改变,不再关于存储持续性,而是**用于自动类型推断**。 在 C++11 之前,用于在块作用域内或函数参数列表中显式声明对象为**自动存储持续性**,即自动变量。 (块作用域和函数参数中的变量默认就是自动变量,C++11 之前的 auto 通常都是省略的) <br> ### `resiter` 说明符 > 此关键字**自 C++17 起已弃用**。 只用于在**块作用域**和**函数参数列表**中声明的对象,其 "**指示**" (而非强制) 优化器使用 CPU 寄存器存储变量值(现代编译器通常会自动做出最优决策)。 <br> ### `static` 说明符⭐ 只允许在**对象声明 (函数参数除外)**、**函数声明(块作用域除外)**、**匿名 unions 声明**中使用。 其作用有两方面: - 声明 "**==静态存储持续性==**": - 用于**局部变量**,声明静态局部变量(除非同时带有 `thread_local` 声明为 "线程局部存储持续性") - 用于**类成员声明**中,声明**静态成员变量**、**静态成员函数** - 声明 "**==内部链接性==**"(针对原本具有 "外部链接性" 的实体名称) - 用于 "**命名空间作用域**" 下的==**全局变量**==、==**普通函数**==时,修改为具有 "**内部链接性**"。 <br> ### `extern` 说明符⭐ `extern` 的作用有三个: 1. 声明引入一个 "**在其它地方定义的、本身具有"==外部链接性=="的变量或函数**",让编译器去**当前编译单元之外**查找定义。 2. **修饰全局 `const` 或 `constexpr` 常量**时,为其赋予 "**==外部链接性==**"(全局常量默认具有内部链接性) 3. 通过 `extern` 关键字来 **指定语言链接性** 以及 **显式模版实例化声明**,但在这些情况下**不作为存储类说明符**。 ###### (1)用于引入外部变量或函数 `extern` 只允许在**变量和函数的声明中**使用(类成员或函数参数除外),可用在**命名空间作用域下声明引入**,也可用于在**块作用域(例如函数)中声明引入**。 > [!faq] "全局变量" 和 "非静态函数" 都具有外部链接性,为什么引入外部变量时必须声明 `extern`,但引入外部函数时却可省略 `extern` ? > > - 对于变量,使用 `extern` 旨在标识语句为 "**==变量声明==**" 而非定义,**如果不带有 `extern`,则编译器会视为 "变量定义"**(**无显式初始化项的变量定义**),故尝试**为该变量分配内存**,从而触发 "**重复定义**" 的错误。 > >> > > - 对于函数,**函数声明**本身就只表示 "**引入一个函数名称**",链接器会根据函数声明在 "程序范围内" 查找函数定义,因此,`extern` 关键字对于引入外部函数而言是可有可无的。 ###### (2)用于赋予全局常量 "外部链接性" ```cpp // 下面四个全局常量都可在其它文件中通过`extern const`声明引入 // 将默认具有内部链接性的全局常量声明为具有外部链接性. extern const int extCstVar = 15; extern constexpr int extCstexprVar = 99; // 也可以先通过`extern`声明为具有外部链接性, 然后再初始化. extern const int extCstVar2; const int extCstVar2 = 66; extern const int extCstexprVar2; // 这里声明时只能先用`const`, 而不能用`constexpr` constexpr int extCstexprVar2 = 55; // 因为`constexpr`声明的变量必须被显式初始化, 给定常量表达式 // 参考: https://stackoverflow.com/questions/45987571/difference-between-constexpr-and-static-constexpr-global-variable ``` > [!summary] > > - `static` 用于**默认具有外部链接性的全局变量、函数**时,修改为内部链接性 > - `extern` 用于**默认具有内部链接性的对象** (例如`const`全局常量) 时,修改为外部链接性。 ###### (3)指定语言链接性、显式模版实例化声明 略。 <br> ### `thead_local` 说明符 > since C++11 声明具有 **==线程存储持续性==**(Thread-Local Storage,**TLS**),**各线程持有一份独立实例**,只允许对**在命名空间作用域声明的对象**、**在块作用域声明的对象**、**类的静态数据成员**使用。 > [!NOTE] **`thread_local` 可以与 `static` 或 `extern` 组合,指定内部或外部链接性**。 > (类的静态数据成员除外,因为其始终具有外部链接性 ) > [!example] C/C++ 中的 TLS 变量声明 > > > ```cpp > // GCC风格的 TLS > __thread int x = 0; > // C++11标准的 TLS > thread_local int y = 42; > ``` > > [!NOTE] 对于 "线程存储持续性" 变量,由 **系统库** 负责确定其内存区域并进行初始化 > > 以 Linux 下 C++程序为例,当调用 `pthread_create()` 系统调用时,其背后会: > > 1. 为新线程分配 "**==栈区==**" + "**==TLS 区域==**"(根据 **ELF 文件中的 `PT_TLS` 段配置**); > 2. 在线程入口处,**将新线程的 TLS 区域地址写入相应寄存器**(`x86-64` 中的 `%fs.base` 段寄存器)以及**线程控制块 TCB** > 3. 跳转至执行**线程函数** --- > [!caution] 除 `thread_local` 以外的存储类说明符都不允许用于" [explicit specializations](https://en.cppreference.com/w/cpp/language/template_specialization) and [explicit instantiations](https://en.cppreference.com/w/cpp/language/class_template#Explicit_instantiation)",如下所示: ```cpp template<class T> struct S { thread_local static int tlm; }; template<> thread_local int S<float>::tlm = 0; // "static" does not appear here ``` <br> ### `mutable` 说明符 **不影响存储持续性和链接性**,仅用于修饰类成员变量,标识 **==放宽对类成员变量的常量性限制==**: 当一个成员变量被声明为`mutable`时,**即使在一个`const`成员函数中,该成员变量仍然可以被修改**。 <br><br> # 存储持续性 ⭐ 存储持续性描述了 "**==实体对象的生命周期==**",即它们 "**被分配的内存空间**" 存在的时间持续性[^5]。 (此处的 "对象" 是指 C++ 内存模型层面的实体,包括**变量、函数** 等) C++中存储持续性分为四种: - **自动存储持续性**(automatic storage) - **静态存储持续性**(static storage duration) - **动态存储持续性**(dynamic storage duration) - **线程存储持续性**(thread storage duration) > [!NOTE] 子对象(subobjects)和引用成员(reference member)的存储持续性是它们的完整对象的存储持续性。 <br> ### 自动存储持续性 对象的内存在**封闭代码块的起始处被分配**,**在结束处被释放**。 即**在声明它们的代码块被执行时创建,在退出该代码块时被销毁**。例如函数、`{}` 代码块。 **除了被声明为`static`、`extern`、`thread_loacal` 以外** 的 **所有"==局部对象=="都具有自动存储持续性**,也称为 "**==自动变量==**"。 即 C++中下列两种对象为**自动存储持续性**: 1. 无 `static`、`extern`、`thread_local` 修饰的**局部变量** 2. **函数参数** <br> ### 静态存储持续性 对象的内存在程序开始时分配,在程序结束时释放,**整个程序运行期间都存在**。 C++中下列对象具有静态存储持续性: - **命名空间作用域(包括全局命名空间) 声明的所有实体**(`thread_local` 声明的除外) - **带有 `static` 声明的对象**:静态全局变量、静态局部变量、**静态成员变量**、**静态成员函数** - **带有 `extern` 声明的对象** > [!NOTE] **同一内联函数**(具有**外部链接性** )的**所有定义**中的函数内==**静态局部变量**==都指向 **==单个翻译单元==** 中的 **==同一对象==** <br> ### 动态存储持续性 通过 **==动态内存分配==(如使用`new`或`malloc`)创建的对象** 具有动态存储持续性。 对象的内存根据请求**通过动态内存分配函数进行分配和释放**, 即使用 `new`/`malloc` 分配的内存,**直到使用 `delete`/`free`释放** 或 **程序结束为止**。 <br> ### 线程存储持续性 只有**声明为 `thread_local` 的对象**才具有此存储持续性。 每个线程对 `thread_local` 对象都 **==持有一份自己的对象实例==**。 这类对象的内存**在线程开始时分配,在线程结束时释放**,生命周期与所属线程一样长。 > [!info] 局部静态变量,局部线程变量的初始化 > > 块作用域中的`static` 或 `thread_local` 变量的分别具有静态、线程存储持续性,但这些变量的 **==初始化==** 只在程序 **==首次执行到他们被声明的位置时==** 才唯一执行(零初始化或常量初始化除外,这两种情况可在首次进入到块时就执行),**此后再次执行到块中的初始化语句时将跳过**。 > > 块作用域静态变量的**析构函数**在程序退出时调用,但只有在初始化成功的情况下才会调用。 > > 如果多个线程试图并发地初始化一个静态局部变量,则初始化只会实际执行一次。(C++11 标准保证对局部静态变量初始化的线程安全性) > > ![image-20240110175850718|917](_attachment/02-开发笔记/01-cpp/cpp基本概念.assets/IMG-cpp基本概念-43583573E5C80C5257F68A928AF942EC.png) > <br><br> # 链接性 ⭐ C++中的链接性(Linkage)标识的是一个**实体名称**(对象、引用、函数、类型、模版、命名空间等)**在程序中不同位置的可见性** [^2]。 C++中一个 "**名称**" 的链接性包括下列几种: - **无链接性**(No linkage):只在定义其的 **==作用域内==** 可见; - **内部链接性**(Internal linkage):只在 "**==定义其的编译单元==**" 内可见; - **外部链接性**(External linkage):**整个程序**中可见,即对 **==所有编译单元==** 可见。 - **模块链接性**(Module linkage) since C++20 > [!NOTE] 在一个编译上下文中,当对一个实体名称**多次声明**时, **其"链接性" 仅取决于首次声明**。 > [!quote] [What is external linkage and internal linkage?](https://stackoverflow.com/questions/1358400/what-is-external-linkage-and-internal-linkage) > > ![image-20240112201234390|702](_attachment/02-开发笔记/01-cpp/cpp%20基本概念/cpp-存储类说明符.assets/IMG-cpp-存储类说明符-12FBB50EF2904DDD2B569D7B83F8B8B9.png) > > <br> ## 无链接性 无链接性的实体 **只在定义它们的==作用域内部==** 可见。 在「**==块作用域中==**」**定义**的下列实体名称都没有链接性: - **局部变量**(包括 `static`修饰的 **==静态局部变量==**、`thread_local` 修饰的**局部线程变量**)) - **局部类**及其**成员函数** - **局部定义的枚举类以及枚举成员**。 - 块作用域内**通过`typedef`、`using` 声明的其他名称** <br> ## 内部链接性 具有内部链接性的实体**只在定义它们的编译单元内可见**。 在「**==命名空间作用域内==**」**定义**的下列实体名称都具有内部链接性: - **声明为`static`的==静态全局变量==、变量模板 (自 c++ 14 起) - **声明为`static` 的函数、函数模板**。 - **未被 "`extern`" 修饰的==全局常量==**( "**const-qualified"且"non-volatile"的 "non-templte" 类型的变量**) - 在 "**匿名命名空间**"或 "**匿名命名空间内的命名空间中**" 声明的**所有名称**(即使显式声明为`extern`) > [!NOTE] 全局变量和函数本身具有 "**外部链接性**",而声明为 "`staic`" 会将其修改为 "**具有==内部链接性==**"。 > [!NOTE] 通过 `const` 或 `constexpr` 定义的"**==全局常量==**" 本身具有 "**==内部链接性==**",加上 `extern` 修饰后会改为 "**具有==外部链接性==**"。 > [!caution] `static` 的优先级高于 `inline`,使变量具有内部链接性而不会跨翻译单元共享,后者失效,两个关键字组合使用没有实际意义。 <br> ## 外部链接性 具有外部链接性的实体 **在整个程序中可见,即在所有翻译单元中可见**。 在「**==命名空间作用域内(除未命名的命名空间)==**」定义的下列实体均具有外部链接性: - `non-static` 且 `non-const` 的 **==全局变量==** - `extern` 修饰的**全局==常量==** - **==内联变量==**、**==内联常量==**( `inline const`)(since C++17) - `non-static` 的 **==函数==、==函数模板==**(包括 **==内联函数==**) - **==类类型 & 其所有成员函数(包括静态) & 其静态数据成员==** (const or not) & 其中定义的**嵌套类与嵌套枚举类** - **枚举类** enumerations > [!NOTE] 具有外部链接性的变量和函数同样具有语言链接性,因此可以链接使用不同编程语言编写的 TUs。 <br> ## 链接指示(语言链接性) 由于 C、C++在编译时对符号名的修饰规则不同[^3] [^4],当在 C++中使用 **预编译 C 库中的函数** 时, 需要 **使用`extern "C"` 对函数原型声明进行 "==链接指示=="**(linkage directive), 指示编译器对该函数**按照 "C 语言的名称修饰规则" 生成符号**,从而确保能够 "**正确链接**"[^6]。 用法说明: ```cpp // use C protocol for name look-up extern "C" void spiff(int); // use C++ protocol for name look-up (C++中默认如此, 因此"C++"可省略) extern "C++" void spaff(int); extern void spbff(int); // 可使用花括号, 对其中的所有适用于该链接指示的函数声明进行声明. extern "C" { size_t strlen(const char *); int strcmp(const char*, const char*); char *strcat(char*, const char*); } // 可对"头文件"进行链接指示:该头文件中所有普通函数声明, 将被认为是由链接指示的语言所编写的. extern "C" { #include <string.h> } ``` > [!info] C 和 C++ 程序在编译时==**对符号名(例如函数名、变量名)的修饰规则**==不同 > > 例如函数 `void spiff (int)` 在 C 编译器下生成的符号名可能为 `_spiff`, > 而在 C++编译器下生成的符号名可能为 `_Z5spiffi`。 > > 当**在 C++ 代码中引用了==预编译好的 C 库(动/静态库)中的外部函数==** 时,编译器在**编译 C++代码**时会对该函数按 **==C++名称修饰规则==** 进行生成 "**==外部符号==**",于是使得 **该函数 "在 C++文件中的符号名"** 与 "**预编译库中的符号名**" 不同,**导致链接器无法正确与 "C 库中的符号进行连接"**。 > > [!tip] 令 "库文件" 兼容 C/C++符号修饰规则的技巧 > > 对于 "**库源文件**" **提供者** (提供**库的头文件和源代码文件**,而非预编译好的动静态库)而言,为**保证库对 C、C++程序的兼容性**, > 通常会在**其库的头文件或源文件的声明**中采用如下 **==条件编译==** 技巧:(该技巧几乎在所有系统头文件中都有被使用。) > > ```cpp > #ifdef __cplusplus > extern "C" { > #endif > > void *memset(void*, int, size_t); > > #ifdef __cplusplus > } > #endif > ``` > > - 若编译的是 C++代码,C++编译器会默认定义宏 `__cplusplus`,因此上述条件编译在**预处理阶段会引入 `extern "C"` 的声明**。 > - 若编译的是 C 代码,则条件编译不会生效,直接声明。 > > > > [!NOTE] C/C++函数在 C/C++程序中的使用 > > ```cpp > // 在 C++ 中, 引入一个 C 库中的函数; > extern "C" void spiff(int); > > // 在 C++中定义一个可被 C 程序调用的函数. 编译器将为该函数生成适用于指定语言的代码. > extern "C" double calc(double dpram) { > // ... > } > ``` > > > [!caution] "链接指示" 对函数声明中作为 "返回类型或形参" 的函数指针也有效 > > ```cpp > // C函数f1, 其接收一个`void(*)(int)`类型的 "C 函数指针". > extern "C" void f1(void(*)(int)); > extern "C" void spiff(int); > > extern "C" typedef void(*FC)(int); // 为 C 函数指针声明别名 > FC = spiff; > > // 调用 f1 时, 必须传入一个 C 函数名字或者 C 函数指针, 而不能是C++函数或C++函数指针 > f1(spiff); > f1(FC); > ``` > > <br> ## 模块链接性 略。 <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: [Storage class specifiers - cppreference.com](https://en.cppreference.com/w/cpp/language/storage_duration) [^2]: [Storage class specifiers - cppreference.com](https://en.cppreference.com/w/cpp/language/storage_duration#Linkage) [^3]: 《程序员的自我修养:链接、装载与库》 [^4]: [cpp/language/language linkage) - cppreference.com](https://en.cppreference.com/w/cpp/language/language_linkage)) [^5]: [Storage class specifiers - cppreference.com](https://en.cppreference.com/w/cpp/language/storage_duration#Explanation) [^6]: 《C++ Primer》P758