C 与 C++ 中的inline关键字

在很长一段时间内,我对 inline 的理解就是用来建议编译器将函数内联展开至调用处,这样的看法在古早之前或许没错,但在现在已经完全不对了。现代的编译器在决定是否将函数调用进行内联展开时,几乎不参考函数声明中inline修饰符;而inline现在也被添加了新的语义,inline关键字不仅能修饰函数,也可修饰变量(C++17以后),甚至能修饰命名空间(C++11以后)

第一定义:内联

inline 最常见的理解即是内联函数。

每个函数在调用时,都会先寻找到函数所在的内存地址,然后将函数的参数复制到堆栈中,对于逻辑比较复杂的函数,函数调用的开销通常微不足道,但如果函数执行的时间很短,进行函数调用所需的时间就可能比实际执行函数代码所需的时间多得多。内联函数即是在函数调用时,插入或替换内联函数的整个代码,从而减少了寻址和压栈出栈的开销。

inline 关键字会向编译器提出建议,在此处将函数展开。需要注意的是,这仅仅是一个建议,编译器可以忽略这个请求,例如,当函数包含循环、跳转等逻辑;函数是递归调用的;函数包含静态变量等等情况,编译器可能会拒绝该提议。

对于定义在类中的函数,会隐式声明为inline函数。

当显式声明 inline 时,必须将inline关键字与函数实现放在一起才生效

C 与 C++ 中 inline 的区别

C 与 C++ 的inline都有建议内联的作用,但是它们之间有所不同。如果编译器对某个使用inline声明的函数拒绝内联,那么这个函数就会在每个包含它的翻译单元中生成一份定义,如果是普通函数,多重定义会导致链接错误,为什么内联函数不会呢?

C++的做法是,如果出现重复的内联函数定义,链接器不会报符号重复,而是在这里面任选一个

对C语言来说,允许函数拥有外部定义(external definition)和内联定义(inline definition)。外部定义就是普通的函数定义,内联定义仅在当前翻译单元有效,不同翻译单元之间,允许同时存在外部定义和内联定义,如果同时具有两种定义,编译器自行决定使用外部定义还是内联定义,请看下面的例子:

c
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
// main.c int main() {    printf("%d/n", foo()); } ​ // foo.h inline int foo() {    return 114; } ​ // foo.c #if 1           // 此时,会调用foo.h中的foo()函数 #include "foo.h" extern inline int foo(); ​ #else           // 此时,调用本文件中实现的foo()函数extern inline int foo() {    return 441; } #endif

由此,在使用C语言的 inline 关键字时,可能会产生问题:如果你在头文件中未实现 inline 函数 foo(),而在源文件中实现了它,如果编译器决定不使用内联,编译器会尝试使用foo()的外部定义版本,在链接时就会出现函数未定义的报错。

inline 的第二定义

在C++17时,inline 被赋予了新的含义:“容许多次定义”而不是“优先内联”,而之前关于函数展开的定义已经完全不在C++标准中了

实际上,随着编译技术的发展,现在的编译器已经能很好的判断出是否需要内联一个函数,inline本身的“偏好内联”作用已经没什么效果和必要性了,而第二个定义其实是 inline 的副作用。

以前的编译器做不到链接期内进行内联优化,如果需要内联,则必须将该函数完整定义到头文件。如果一个函数定义到头文件且不是static的,就会出现多个目标文件中出现同一个函数的情况,这时就需要有inline关键字来允许该函数或变量可在多个目标文件中重复定义。

为什么内联函数需要区别对待?这是因为函数被内联后就成为了另一个函数内部的代码片段,而不是一个函数“实体”,如果函数成功内联,理论上不需要在头文件中出现定义。但是,是否内联由编译器决定,如果多个编译单元内都有拒绝内联的地方,那么就会导致函数的重定义,这时,inline关键字允许链接时有多个具有相同签名的实体出现,但只会从中随机挑一个

C++ 头文件中 static 和 inline 修饰函数或变量的区别

如前面所言,inline 和 static 都可以声明一个全局唯一的变量,那么它们的区别有哪些?

主要区别如下:

  • inline 不能修饰局部变量

  • 类的非 const 静态成员变量初始化,C++ 17可以通过 static inline 在类内直接初始化,C++17之前必须在类外初始化(const static 修饰的变量也可以类内初始化)

  • C++17之后,类的静态成员变量在类内通过 static 声明,在类外(但是在头文件中)初始化不加 inline 的话可能会导致重定义从而出现链接错误,而加了 inline 就不会出错,类似有无inline修饰的全局函数;C++ 17之前必须在.cpp中初始化静态成员才不会出现重定义的错误,在.h中初始化还是会导致重定义错误,因为C++17之前的标准不支持inline修饰类的静态成员变量;

  • 类中的函数其实可以认为是都隐式加了 inline 的,因为类中的所有函数在全局都只有一份,而有无 static 修饰只是限制该函数对类数据成员的使用(类的 static 函数只能使用 static 成员变量,类的普通成员函数可以使用所有的成员变量 );

对于非类的成员函数及变量:

  • inline 修饰的函数或变量在全局保留一份

  • static 修饰的函数或者变量会在各自的编译单元都保留一份(好吧我认为这段话有点奇怪,什么时候会出现这样的情况?对于非inline的static函数,通常在.cpp文件中进行声明和实现,并且仅进行内部链接,如果多个编译单元都具有同一个static函数的定义,会在链接时出现重定义的错误)

  • static inline 修饰的函数或者变量与 static 单独修饰的效果一致

C++ 17 之前类的非 const 静态成员必须在类外初始化

这是因为类的静态成员不属于任何对象,如果放在类内初始化,那么每个对象在构造的时候都会对这个静态成员进行初始化;而 const 修饰的静态成员变量是在类中初始化,对象在构造的时候已经默认该变量初始化了

C++17 之后可以在类内通过 inline static 直接进行声明与初始化,也可以在类内部进行声明,在类外(头文件中)通过 inline 进行初始化。因为新标准通过 inline 能够保证类内静态变量只初始化一次,在这之前 inline 不允许修饰类的静态成员变量。下面是一个定义静态数据成员的例子:

cpp
  • 01
  • 02
  • 03
  • 04
  • 05
// 在C++17以前,定义一个静态数据成员 struct D {static int n;}; // .h int D::n = 1;             // .cpp // 新的写法,你可以在头文件里定义并初始化它 struct D {inline static int n = 1}; struct DD {constexpr int n = 1}; // 隐式inline

inline 命名空间

C++11中引入了 inline 命名空间的用法,下面我们来分析一下这样的关键字有什么用。

命名空间

我们知道,C++中会引入命名空间,以避免不同的类库中出现命名冲突的情况。假设有下面两个命名空间:

cpp
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
namespace std_namespace1 { class Normal { public: void fun(int i); }; } namespace std_namespace2 { class Normal { public: void fun(int i); }; }

这里面定义了相同的 Normal 类和 fun() 函数,有了命名空间后,就可以使用 命名空间::类名 的方式,区分这两个类了。

cpp
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
std_namespace1::Normal n1; n1.fun(1); std_namespace2::Normal n2; n2.fun(2); // 如果使用 using 语句,可以将某个命名空间释放出来,就好像处于外层命名空间一样 using namespace std_namespace1; Normal n1; n1.fun(1);

内联命名空间

C++11中引入了内联命名空间,它的特点是不需要使用 using 语句就可以直接在外层命名空间使用该命名空间的内容,并且无需使用命名空间前缀:

cpp
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
inline namespace inline_namespace1 { class Inline1 { public: int ii; }; } namespace inline_namespace1 { class Inline2 { public: int jj; }; }

内联命名空间的声明方法就是在原本的命名空间前加上 inline 关键字。除此之外上面的代码还有以下特点:

  1. 两处声明的命名空间同名,它们属于同一命名空间。这是C++命名空间从来就有的特性。

  2. 第一处我们用 inline 修饰了命名空间,这是显式内联;而第二次没有使用 inline 修饰,但因为第一次已经使用过 inline,这里声明的还是内联命名空间,这种情况被称为隐式内联。

进行内联命名空间的声明后,就可以直接使用 Inline1Inline2 类,这与使用 using namespace 的效果类似。通过这样的机制,可以实现类库的版本控制:我们将最新版本命名为内联命名空间,这样,我们在使用最新类库的时候,就可以直接使用,如果需要使用以前版本的类库,则可以使用版本前缀加类名的方式。即便这样,inline 与 using namespace 似乎也并没有本质的不同,实际上在C++11之前,库的版本控制就是通过 using namespace 实现的,但是这种方式有两个明显的缺陷:

  1. 在内嵌命名空间声明的模板无法在外围命名空间中进行特化。例如下面这段代码就无法在命名空间 libfoo 中进行特化:

    cpp
    • 01
    • 02
    • 03
    • 04
    • 05
    • 06
    • 07
    • 08
    • 09
    • 10
    • 11
    namespace libfoo { namespace libfoo_2022 { template <typename T> T &foo(T &); } using namespace libfoo_2022; } namespace libfoo { template <> float &foo<float>(float &); }
  2. 不支持ADL。ADL的意思是在函数名字查找时自动将调用参数所属的命名空间包含进来,这样在函数调用时便无需显示指定作用域。不支持ADL就意味着用内嵌命名空间的类型变量作为参数调用外围命名空间的函数,或者用外围命名空间的类型变量作为参数调用内嵌命名空间的函数是行不通的。例如下面这段代码就无法直接调用 foo1foo2

    cpp
    • 01
    • 02
    • 03
    • 04
    • 05
    • 06
    • 07
    • 08
    • 09
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    namespace libfoo { class Bar1 {}; namespace libfoo_2022 { void foo1(Bar1); class Bar2 {}; } using namespace libfoo_2022; void foo2(Bar2); } int main() { libfoo::Bar1 bar1; libfoo::Bar2 bar2; (void)foo1(bar1); // compile error (void)foo2(bar2); // compile error ... }

    inline 在修饰命名空间时还有一些别的特点,比如若 inline 命名空间与外围命名空间包含了名字重复的符号时,使用外围命名空间作为作用域对该符号进行引用将会导致编译错误,因为编译器无法确定你究竟引用了哪个命名空间的符号。但如果使用 using namespace 的话上述情况则总是指向外围命名空间中的符号。

参考资料:

C++ keyword: inline - cppreference.com

C++11新特性(79)-内联命名空间(inline namespace)

知乎-C++ inline 有什么用?吼姆桑的回答