ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 简介 在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。 但是在c++出现之后,使用预处理宏会出现两个问题: * 第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。 * 第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。 为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function). 内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。 # 预处理宏的缺陷 预处理器宏存在问题的关键是我们可能认为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来是一样的,因为也容易被混淆。但是其中也会有一些微妙的问题出现: 问题一: ~~~ #define ADD(x,y) x+y inline int Add(int x,int y){ return x + y; } void test(){ int ret1 = ADD(10, 20) * 10; //希望的结果是300 int ret2 = Add(10, 20) * 10; //希望结果也是300 cout << "ret1:" << ret1 << endl; //210 cout << "ret2:" << ret2 << endl; //300 } ~~~ 问题二: ~~~ #define COMPARE(x,y) ((x) < (y) ? (x) : (y)) int Compare(int x,int y){ return x < y ? x : y; } void test02(){ int a = 1; int b = 3; //cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3 cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2 } ~~~ 问题三: 预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。 # 内联函数 ## 内联函数基本概念 在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。 * 在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。 ~~~ inline void func(int a); ~~~ 以上写法没有任何效果,仅仅是声明函数,应该如下方式来做: ~~~ inline int func(int a){return ++;} ~~~ ~~~ inline void mycompare(int a, int b) { int rel = a < b ? a : b; cout << "结果是: " << rel << endl; } int main() { mycompare(1, 2); system("pause"); return EXIT_SUCCESS; } ~~~ 注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事预处理器无法完成的。 内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。 ## 类内部的内联函数 为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是**在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。** ~~~ class Person{ public: Person(){ cout << "构造函数!" << endl; } void PrintPerson(){ cout << "输出Person!" << endl; } } ~~~ 构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。 ## 内联函数和编译器 内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理? 对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。 当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。 类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。 # 限制 c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译: * 不能存在任何形式的循环语句 * 不能存在过多的条件判断语句 * 函数体不能过于庞大 * 不能对函数进行取址操作 **内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。** 尽管大多数教科书中在函数声明和函数定义处都增加了 inline 关键字,但我认为 inline 关键字不应该出现在函数声明处。这个细节虽然不会影响函数的功能,但是体现了高质量 C++ 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。 **更为严格地说,内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格。 ** 在多文件编程中,我们通常将函数的定义放在源文件中,将函数的声明放在头文件中,希望调用函数时,引入对应的头文件即可,我们鼓励这种将函数定义和函数声明分开的做法。但这种做法不适用于内联函数,将内联函数的声明和定义分散到不同的文件中会出错,请看下面的例子 main.cpp ~~~ #include <iostream> using namespace std; //内联函数声明 void func(); int main(){ func(); return 0; } ~~~ module.cpp ~~~ #include <iostream> using namespace std; //内联函数定义 inline void func(){ cout<<"inline function"<<endl; } ~~~ 上面的代码能够正常编译,但在链接时会出错。func() 是内联函数,编译期间会用它来替换函数调用处,编译完成后函数就不存在了,链接器在将多个目标文件(`.o`或`.obj`文件)合并成一个可执行文件时找不到 func() 函数的定义,所以会产生链接错误。 内联函数虽然叫做函数,在定义和声明的语法上也和普通函数一样,但它已经失去了函数的本质。函数是一段可以重复使用的代码,它位于虚拟地址空间中的代码区,也占用可执行文件的体积,而内联函数的代码在编译后就被消除了,不存在于虚拟地址空间中,没法重复使用 联函数看起来简单,但是有很多细节需要注意,从代码重复利用的角度讲,内联函数已经不再是函数了。我认为将内联函数作为带参宏的替代方案更为靠谱,而不是真的当做函数使用。 **在多文件编程时,我建议将内联函数的定义直接放在头文件中,并且禁用内联函数的声明(声明是多此一举** # 内联函数定义在类外部 ~~~ class Student{ public: char *name; int age; float score; void say(); //内联函数声明,可以增加 inline 关键字,但编译器会忽略 }; //函数定义 inline void Student::say(){ cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl; } ~~~ 这样,say() 就会变成内联函数。 这种在类体外定义 inline 函数的方式,必须将类的定义和成员函数的定义都放在同一个头文件中(或者同一个源文件中),否则编译时无法进行嵌入(将函数代码的嵌入到函数调用出)