添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

c++的一个优点是它支持只使用头文件库的开发。然而,c++ 17之前,头文件中不需要或不提供全局变量或对象时才有可能成为一个库。c++ 17可以在头文件中定义一个内联的变量/对象,如果这个定义被多个编译单元使用,它们都指向同一个惟一的对象:

//hpp
class MyClass
	static inline std::string name = ""; // OK since C++17
	//...
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

1. Inline变量的动机

在c++中,类结构中不允许初始化非const静态成员:
 

class MyClass
	static std::string name = ""; // Compile-Time ERROR
	//...

包含在多个CPP文件中的头文件中定义类结构之外的变量也是一个错误,:

//hpp
class MyClass
	static std::string name; // OK
	//...
MyClass::name = ""; // Link ERROR if included by multiple CPP files

问题在于头文件可能被包含多次,多个包含该头文件的CPP文件中都定义了一份MyClass.name。

同样的原因,如果你在头文件中定义类的对象,你会得到一个链接错误:

//hpp
class MyClass
	//...
MyClass myGlobalObject; // Link ERROR if included by multiple CPP files

为了解决是上述问题,有一些变通办法:

a. 可以在类/结构中初始化静态const整数数据成员:

class MyClass 
    static const bool trace = false;
	//...

b. 可以定义一个内联函数返回一个静态局部变量:

inline std::string getName() 
    static std::string name = "initial value";
    return name;

c. 可以定义一个静态成员函数返回值:

std::string getMyGlobalObject()
    static std::string myGlobalObject = "initial value";
    return myGlobalObject;

d. 可以使用变量模板(因为c++ 14):

template<typename T = std::string>
T myGlobalObject = "initial value";

e. 可以从静态成员的基类模板派生:

template<typename Dummy>
class MyClassStatics
    static std::string name;
template<typename Dummy>
std::string MyClassStatics<Dummy>::name = "initial value";
class MyClass : public MyClassStatics<void>

但是,所有这些方法都会导致显著的开销、较低的可读性和/或使用全局变量的不同方式。此外,全局变量的初始化可能被推迟到第一次使用时,这将禁用我们希望在程序启动时初始化对象的应用程序(例如在使用对象监视进程时)。

2. 使用Inline变量

现在,使用内联,可以通过只在头文件中定义一个全局可用的对象,它可能被多个CPP文件包含:

class MyClass
    static inline std::string name = ""; // OK since C++17
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

执行包含头文件或包含这个定义的第一个编译单元时,将执行初始化。
这里使用line与内联函数具有相同的语义,如果不使用inline则[具体请看下面PS那部分],

a. 可以在多个翻译单元中定义,前提是所有定义都是相同的。

b. 必须在使用它的每个翻译单元中定义。

两者都是通过包含来自相同头文件的定义来给出的。程序的结果行为就好像只有一个变量一样。

PS:[内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)]。

你甚至可以应用这个定义原子类型在头文件中:

inline std::atomic<bool> ready{false};

注意,对于std::atomic,通常在定义值时必须初始化它们。

注意,在初始化类型之前,仍然必须确保类型已经完成。例如,如果结构体或类有自己类型的静态成员,则只能在类型声明之后内联定义该成员:

struct MyValue
    int value;
    MyValue(int i) : value{i} 
    // one static object to hold the maximum value of this type:
    static const MyValue max; // can only be declared here
inline const MyValue MyValue::max = 1000;

        C++程序通常都有多个C++源文件组成(其以 .cpp 或 .cc 结尾)。这些文件会单独编
译成模块/二进制文件(通常以 .o 结尾)。链接所有模块/二进制文件形成一个单独的
可执行文件,或是动态库/静态库则是编译的最后一步。
当链接器发现一个特定的符号,被定义了多次时就会报错。举个栗子,现在我们有
一个函数声明 int foo(); ,当我们在两个模块中定义了同一个函数,那么哪一个才
是正确的呢?链接器自己不能做主。这样没错,但是这也可能不是开发者想看到的。
       为了能提供全局可以使用的方法,通常会在头文件中定义函数,这可以让C++的所
有模块都调用头文件中函数的实现(C++中,头文件中实现的函数,编译器会隐式的
使用inline来进行修饰,从而避免符号重复定义的问题)。这样就可以将函数的定义
单独的放入模块中。之后,就可以安全的将这些模块文件链接在一起了。这种方式
也被称为定义与单一定义规则(ODR,One Definition Rule)。看了下图或许能更好
的理解这个规则: 

    如果这是唯一的方法,就不需要只有头文件的库了。只有头文件的库非常方便,因为只需要使用 #include 语句将对应的头文件包含入C++源文件/头文件中后,就可以使用这个库了。当提供普库时,开发者需要编写相应的编译脚本,以便连接器将库模块链接在一起,形成对应的可执行文件。这种方式对于很小的库来说是不必要的。
        对于这样例子, inline 关键字就能解决不同的模块中使用同一符号采用不同实现的方式。当连接器找到多个具有相同签名的符号时,这些函数定义使用 inline 进行声明,链接器就会选择首先找到的那个实现,然后认为其他符号使用的是相同的定义。所有使用 inline 定义的符号都是完全相同的,对于开发者来说这应该是常识。我们的例子中,连接器将会在每个模块中找到 process_monitor::standard_string 符号,因为这些模块包含了 foo_lib.hpp 。如果没有了 inline 关键字,连接器将不知道选择哪个实现,所以其会将编译过程中断并报错。同样的原理也适用于 global_process_monitor 符号。使用 inline 声明所有符号之后,连接器只会接受其找到的第一个符号,而将后续该符号的不同实现丢弃。
        C++17之前,解决的方法是通过额外的C++模块文件提供相应的符号,这将迫使我
们的库用户强制在链接阶段包含该文件。 

       传统的 inline 关键字还有另外一种功能。其会告诉编译器,可以通过实现直接放在
调用它的地方来消除函数调用的过程。这样的话,代码中的函数调用会减少,这样
我们会认为程序会运行的更快。如果函数非常短,那么生成的程序段也会很短(假
设函数调用也需要若干个指令,保护现场等操作,其耗时会高于实际工作的代
码)。当内联函数非常长,那么二进制文件的大小就会变得很大,有时并无法让代
码运行的更快。因此,编译器会将 inline 关键字作为一个提示,可能会对内联函数
消除函数调用。当然,编译器也会将一些函数进行内联,尽管开发者没有使
用 inline 进行提示。 

3. constexpr意味着inline

对于静态数据成员,自从C++17起constexpr意味着内联,因此下面的静态成员变量n为定义了静态数据成员n:

struct D
    static constexpr int n = 5; // C++11/C++14: //声明但未定义
                                // since C++17: 定义

也就是说,它等于:

struct D
    inline static constexpr int n = 5;

注意,在c++ 17之前,在没有相应定义的情况下声明静态数据成员时加上const就可以在类内初始化:

struct D
    static constexpr int n = 5;

但是,只有在不需要获取static成员变量D::n地址的情况下才可以不用定义D::n,

例如,当D::n通过值传递时:

std::cout << D::n; // OK (ostream::operator<<(int) gets D::n by value)

如果D::n是通过引用或者指针传递给一个函数,编译会报错。

int inc(const int& i);
std::cout << inc(D::n); // ld: error: undefined symbol: D::n

因此,在c++ 17之前,您必须在一个翻译单元中定义D::n:

constexpr int D:: n;  //在C++17之前表达式是定义,C++17中是冗余声明,已被弃用。

4. inline变量和thread_local

通过使用thread_local,可以为每个线程创建一个惟一的内联变量:

struct ThreadData
    inline static thread_local std::string name; // unique name per thread
inline thread_local std::vector<std::string> cache; // one cache per thread

作为一个完整的例子,考虑以下头文件:

inlinethreadlocal.hpp

#include <string>
#include <iostream>
struct MyData
    inline static std::string gName = "global"; // unique in program
    inline static thread_local std::string tName = "tls"; // unique per thread
    std::string lName = "local"; // for each object
    void print(const std::string& msg) const {
    std::cout << msg << '\n';
    std::cout << "- gName: " << gName << '\n';
    std::cout << "- tName: " << tName << '\n';
    std::cout << "- lName: " << lName << '\n';
inline thread_local MyData myThreadData; // one object per thread

在有main()的编译单元中使用:

inlinethreadlocal1.cpp

#include "inlinethreadlocal.hpp"
#include <thread>
void foo();
int main()
    myThreadData.print("main() begin:");
    myThreadData.gName = "thread1 name";
    myThreadData.tName = "thread1 name";
    myThreadData.lName = "thread1 name";
    myThreadData.print("main() later:");
    std::thread t(foo);
    t.join();
    myThreadData.print("main() end:");

在另一个定义foo()的编译单元中使用inlinethreadlocal.hpp头文件,foo在主线程中被调用:

inlinethreadlocal2.cpp

#include "inlinethreadlocal.hpp"
void foo()
    myThreadData.print("foo() begin:");
    myThreadData.gName = "thread2 name";
    myThreadData.tName = "thread2 name";
    myThreadData.lName = "thread2 name";
    myThreadData.print("foo() end:");

程序输出如下:

5. 使用内联变量来跟踪::new

下面的程序演示了通过只包含该头文件如何使用内联变量跟踪调用::new:

tracknew.hpp

#ifndef TRACKNEW_HPP
#define TRACKNEW_HPP
#include <new>
#include <cstdlib> // for malloc()
#include <iostream>
class TrackNew
    private:
    static inline int numMalloc = 0; // num malloc calls
    static inline long sumSize = 0; // bytes allocated so far
    static inline bool doTrace = false; // tracing enabled
    static inline bool inNew = false; // don’t track output inside new overloads
    public:
    // reset new/memory counters
    static void reset()
        numMalloc = 0;
        sumSize = 0;
    // enable print output for each new:
    static void trace(bool b)
        doTrace = b;
    // print current state:
    static void status()
     std::cerr << numMalloc << " mallocs for " << sumSize << " Bytes" << '\n';
    // implementation of tracked allocation:
    static void* allocate(std::size_t size, const char* call)
        // trace output might again allocate memory, so handle this the usual way:
        if (inNew)
            return std::malloc(size);
        inNew = true;
        // track and trace the allocation:
        ++numMalloc;
        sumSize += size;
        void* p = std::malloc(size);
        if (doTrace)
             std::cerr << "#" << numMalloc << " "
             << call << " (" << size << " Bytes) => "
            << p << " (total: " << sumSize << " Bytes)" << '\n';
        inNew = false;
        return p;
inline void* operator new (std::size_t size)
    return TrackNew::allocate(size, "::new");
inline void* operator new[] (std::size_t size)
    return TrackNew::allocate(size, "::new[]");
#endif // TRACKNEW_HPP

考虑在下面的头文件中使用这个头文件:

racknewtest.hpp

include "tracknew.hpp"
#include <string>
class MyClass
    static inline std::string name = "initial name with 26 chars";
MyClass myGlobalObj; // OK since C++17 even if included by multiple CPP files#

cpp文件tracknewtest.cpp如下:

#include "tracknew.hpp"
#include "tracknewtest.hpp"
#include <iostream>
#include <string>
int main()
    TrackNew::status();
    TrackNew::trace(true);
    std::string s = "an string value with 29 chars";
    TrackNew::status();

输出取决于何时初始化跟踪以及初始化执行了多少分配。但结尾应该是这样的:

.......

#33 ::new (27 Bytes) => 0x89dda0 (total: 2977 Bytes)
33 mallocs for 2977 Bytes
#34 ::new (30 Bytes) => 0x89db00 (total: 3007 Bytes)
34 mallocs for 3007 Bytes

初始化MyClass::name需要27个字节,初始化main()中的s需要30个字节。(注意,字符串是由大于15个字符的值初始化的,以避免使用实现小/短字符串优化的库在堆上不分配内存(SSO),它在数据成员中存储最多15个字符的字符串,而不是分配堆内存。)

第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple Object Model) 表格驱动对象模型(A Table-driven Object Model) C++对象模型(Th e C++ Object Model) 对象模型如何影响程序(How the Object Model Effects Programs) 1.2 关键词所带来的差异(A Keyword Distinction) 关键词的困扰 策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virual Function”的Class “带有一个virual Base class”的Class 2.2 Copy Constructor的建构操作 Default Memberwise Initialization Bitwise Copy Semantics(位逐次拷贝) 不要Bitwise Copy Semantics! 重新设定的指针Virtual Table 处理Virtual Base Class Subobject 2.3 程序转换语意学(Program Transformation Semantics) 明确的初始化操作(Explicit Initialization) 参数的初始化(Argument Initialization) 返回值的初始化(Return Value Initialization) 在使用者层面做优化(Optimization at the user Level) 在编译器层面做优化(Optimization at the Compiler Level) Copy Constructor:要还是不要? 2.4 成员们的初始化队伍(Member Initialization List) 第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data Member 3.4 “继承”与Data Member 只要继承不要多态(Inheritance without Polymorphism) 加上多态(Adding Polymorphism) 多重继承(Multiple Inheritance) 虚拟继承(Virtual Inheritance) 3.5 对象成员的效率(Object Member Efficiency) 3.6 指向Data Members的指针(Pointer to Data Members) “指向Members的指针”的效率问题 第4章 Function语意学(The Semantics of Function) 4.1 Member的各种调用方式 Nonstatic Member Functions(非静态成员函数) Virtual Member Functions(虚拟成员函数) Static Member Functions(静态成员函数) 4.2 Virtual Member Functions(虚拟成员函数) 多重继承下的Virtual Functions 虚拟继承下的Virtual Functions 4.3 函数的效能 4.4 指向Member Functions的指针(Pointer-to-Member Functions) 支持“指向Virtual Member Functions”之指针 在多重继承之下,指向Member Functions的指针 “指向Member Functions之指针”的效率 4.5 Inline Functions 形式对数(Formal Arguments) 局部变量(Local Variables) 第5章 构造、解构、拷贝 语意学(Semantics of Construction,Destruction,and Copy) 纯虚拟函数的存在(Presence of a Pure Virtual Function) 虚拟规格的存在(Presence of a Virtual Specification) 虚拟规格中const的存在 重新考虑class的声明 5.1 无继承情况下的对象构造 抽象数据类型(Abstract Data Type) 为继承做准备 5.2 继承体系下的对象构造 虚拟继承(Virtual Inheritance) 初始化语意学(The Semantics of the vptr Initialization) 5.3 对象复制语意学(Object Copy Semantics) 5.4 对象的功能(Object Efficiency) 5.5 解构语意学(Semantics of Destruction) 第6章 执行期语意学(Runting Semantics) 6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete运算符 针对数组的new语意 Placement Operator new的语意 6.3 临时性对象(Temporary Objects) 临时性对象的迷思(神话、传说) 第7章 站在对象模型的类端(On the Cusp of the Object Model) 7.1 Template Template的“具现”行为(Template Instantiation) Template的错误报告(Error Reporting within a Template) Template中的名称决议方式(Name Resolution within a Template) Member Function的具现行为(Member Function Instantiation) 7.2 异常处理(Exception Handling) Exception Handling快速检阅 对Exception Handling的支持 7.3 执行期类型识别(Runtime Type Identification,RTTI) Type-Safe Downcast(保证安全的向下转型操作) Type-Safe Dynamic Cast(保证安全的动态转型) References并不是Pointers Typeid运算符 7.4 效率有了,弹性呢? 动态共享函数库(Dynamic Shared Libraries) 共享内存(Shared Memory) if init表达式 C++17语言引入了一个新版本的if/switch语句形式,if (init; condition)和switch (init; condition),即可以在if和switch语句中直接对声明变量并初始化,如下: if(const auto it = myString.find(hello); it != string::npos) { cout << it << - Hello\n; if(const auto it = 因为头文件在每个包含它的.cpp文件中都会被编译一次,如果头文件中有变量或函数的定义,那么就会在每个.cpp文件中都生成该变量或函数的定义,导致链接时出现多重定义错误。但如果A和B定义在不同的文件中,那么可能出现A在B之后初始化的情况,而B在初始化时又会试图使用A,那么就会引发运行时错误。静态变量:函数内部和函数外部,本身静态变量就是为了隐藏全局变量,并且函数内部定义的静态变量,作用域在所在函数,第一次调用时初始化,结束时销毁,没定义时默认为0,函数外部的类似于全局变量,但是对于其他文件不可见。 可能不太熟悉,也有可能没有去关心过。我们只关心程序能否正确运行,或者程序怎么实现等等一些问题。 这里笔者就为介绍下我们熟悉又不太熟悉的“#include”,首先我们了解下C/C++头文件。 头文件为相关声明提供了一个集中存在的位置。头文件一般包含类的定义,extern变量声明与函数声明。注意这里声明与定义的区别:它们最本质的区别是定义只可以出现一次,声明可以出现多次。声明不分配空间,而定义是要分配空间的。头文件正确使用可以保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。 头文件还可以定义:在编译的时候就已知道其值的cosnt对象和inline 函数。在头文件中定义上述 一、关于staticstatic 是C++中很常用的修饰符,它被用来控制变量的存储方式和可见性,下面我将从 static 修饰符的产生原因、作用谈起,全面分析static 修饰符的实质。 static 的两大作用: 一、控制存储方式 static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间。 引出原因:函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点 第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple Object Model) 表格驱动对象模型(A Table-driven Object Model) C++对象模型(Th e C++ Object Model) 对象模型如何影响程序(How the Object Model Effects Programs) 1.2 关键词所带来的差异(A Keyword Distinction) 关键词的困扰 策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virual Function”的Class “带有一个virual Base class”的Class 2.2 Copy Constructor的建构操作 Default Memberwise Initialization Bitwise Copy Semantics(位逐次拷贝) 不要Bitwise Copy Semantics! 重新设定的指针Virtual Table 处理Virtual Base Class Subobject 2.3 程序转换语意学(Program Transformation Semantics) 明确的初始化操作(Explicit Initialization) 参数的初始化(Argument Initialization) 返回值的初始化(Return Value Initialization) 在使用者层面做优化(Optimization at the user Level) 在编译器层面做优化(Optimization at the Compiler Level) Copy Constructor:要还是不要? 2.4 成员们的初始化队伍(Member Initialization List) 第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data Member 3.4 “继承”与Data Member 只要继承不要多态(Inheritance without Polymorphism) 加上多态(Adding Polymorphism) 多重继承(Multiple Inheritance) 虚拟继承(Virtual Inheritance) 3.5 对象成员的效率(Object Member Efficiency) 3.6 指向Data Members的指针(Pointer to Data Members) “指向Members的指针”的效率问题 第4章 Function语意学(The Semantics of Function) 4.1 Member的各种调用方式 Nonstatic Member Functions(非静态成员函数) Virtual Member Functions(虚拟成员函数) Static Member Functions(静态成员函数) 4.2 Virtual Member Functions(虚拟成员函数) 多重继承下的Virtual Functions 虚拟继承下的Virtual Functions 4.3 函数的效能 4.4 指向Member Functions的指针(Pointer-to-Member Functions) 支持“指向Virtual Member Functions”之指针 在多重继承之下,指向Member Functions的指针 “指向Member Functions之指针”的效率 4.5 Inline Functions 形式对数(Formal Arguments) 局部变量(Local Variables) 第5章 构造、解构、拷贝 语意学(Semantics of Construction,Destruction,and Copy) 纯虚拟函数的存在(Presence of a Pure Virtual Function) 虚拟规格的存在(Presence of a Virtual Specification) 虚拟规格中const的存在 重新考虑class的声明 5.1 无继承情况下的对象构造 抽象数据类型(Abstract Data Type) 为继承做准备 5.2 继承体系下的对象构造 虚拟继承(Virtual Inheritance) 初始化语意学(The Semantics of the vptr Initialization) 5.3 对象复制语意学(Object Copy Semantics) 5.4 对象的功能(Object Efficiency) 5.5 解构语意学(Semantics of Destruction) 第6章 执行期语意学(Runting Semantics) 6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete运算符 针对数组的new语意 Placement Operator new的语意 6.3 临时性对象(Temporary Objects) 临时性对象的迷思(神话、传说) 第7章 站在对象模型的类端(On the Cusp of the Object Model) 7.1 Template Template的“具现”行为(Template Instantiation) Template的错误报告(Error Reporting within a Template) Template中的名称决议方式(Name Resolution within a Template) Member Function的具现行为(Member Function Instantiation) 7.2 异常处理(Exception Handling) Exception Handling快速检阅 对Exception Handling的支持 7.3 执行期类型识别(Runtime Type Identification,RTTI) Type-Safe Downcast(保证安全的向下转型操作) Type-Safe Dynamic Cast(保证安全的动态转型) References并不是Pointers Typeid运算符 7.4 效率有了,弹性呢? 动态共享函数库(Dynamic Shared Libraries) 共享内存(Shared Memory) 1>.确认要提炼的表达式没有副作用。 2>.声明一个不可以修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。 3>.用这个新变量取代原来的表达式。 4>.测试。 4.范例:如下所示: 1>.源代码如下所示: function price(order) { // price is b 一、inline的介绍 inline这个关键字,估计只要学习过c++的都知道,在函数的应用中,就有内联函数这一个用法(具体用法请参阅以前的文章,此处不展开)。内联函数保证了函数在每个编译单元中都有一个相同的副本,通过牺牲代码何种来换取时间开销的方法一定是可以让每个人有比较深刻的印象的。那么,有些人一定会问,“有没有内联变量?”,还真有,在c++17中就推出了这个inline变量。为什么需要inline变量,这就和一些实际中c++的应用有关系了。 一般来说,任何语言的编程都要尽量保持着风格和思想的一致,即使可 C++17中引入了内联变量inline variables)的概念,它允许我们在头文件中定义全局变量,而不必担心重复定义的问题。与内联函数类似,内联变量也可以在多个编译单元中使用而不会出现链接错误,因为编译器会将它们视为多个实例的同一变量,而不是多个不同的变量