内联函数
影响性能的一个重要因素是内联技巧。内联函数也可称为内嵌函数。
在C++中,函数调用需要建立栈环境,进行参数的复制,保护调用线程,返回时,还有进行返回值复制,恢复调用现场。这些工作都是与完成特定任务的操作武功的额外开销。程序效率由于改下工作而受到影响,所以,流行的CPU都已经将函数调用的额外工作硬件化了,以此来建减少运行开销。尽管如此,调用工作还是有一些微小的开销的,如果频繁调用很少语句的小函数,则这些开销对性能的影响还不好说。例如,下面代码频繁的调用一个小函数:
1 //==================== 2 // f0601.cpp 3 //频繁调用一个小函数 4 //==================== 5 #include<iostream> 6 using namespace std; 7 //------------------------------------ 8 bool isnumber(char); // 函数声明 9 //------------------------------------ 10 int main() 11 { 12 char c; 13 while(cin>>c && c != '\n') // 反复读入字符,若为回车便结束 14 if(isnumber(c)) 15 cout<<"you entered a digit.\n"; 16 else 17 cout<<"you entered a non-digit.\n"; 18 } 19 //---------------------------------- 20 bool isnumber(char ch) 21 { 22 return ch>='0' ** ch <= '9' ? 1 : 0; 23 }
程序不断到输入设备中读取数据,频繁调用isnumber函数。isnumber是个小函数,所以函数调用的开销相对来说占的比重就大了。为了免去调用开销,提高效率,可将程序改写为:
1 //==================== 2 // f0602.cpp 3 // 将小函数“融化”在调用处 4 //==================== 5 #include<iostream> 6 using namespace std; 7 //------------------------------------ 8 int main() 9 { 10 char c; 11 while(cin>>c && c!= '\n') 12 if(c >= '0' && c <= '9' ?1 : 0) //将调用改为直接判断 13 cout<<"you entered a digit.\n"; 14 else 15 cout<<"you entered a non_digit.\n"; 16 } 17 //====================
该程序在if语句中用表达式替换了函数调用。在程序运行上,因为免去了大量的函数调用开销,提高了执行效率。
由于isnumber函数比相应的表达式可读性好,所以若程序中多处出现isnumber,而又将其替换为复杂的实现语句的话,就会降低程序的可读性。我们既要用函数调用来体现其结构化和可读性,又要是效率尽可能地高。解决办法就是将这种小函数声明为内联(inline):
1 //==================== 2 // f0603.cpp 3 // 内联函数 4 //==================== 5 #include<iostream> 6 using namespace std; 7 //----------------------------------- 8 inline bool isnumber(char); // 内联声明 9 //----------------------------------- 10 int main() 11 { 12 char c; 13 while(cin>>c && c != '\n') 14 if(isnumber(c)) 15 cout<<"you entered a digit.\n"; 16 else 17 cout<<"you entered a non_digit.\n"; 18 } 19 //----------------------------------- 20 bool isnumber(char ch) 21 { 22 return ch >= '0' && ch <= '9' ? 1 : 0; 23 } 24 //====================
对函数的内联声明必须在调用之前。因为内联函数的代码在程序运行时是直接潜在调用处执行的,他不影响链接,只在编译时确定运行代码。因此编译时,在调用之前看到内联声明就是十分必要。例如下列程序没有内联:
1 //==================== 2 // f0604.cpp 3 // 未声明内联 4 //==================== 5 #include<iostream> 6 using namespace std; 7 //----------------------------------- 8 bool isnumber(char); // 此处无inline 9 //----------------------------------- 10 int main() 11 { 12 char c; 13 while(cin>>c && c != '\n') 14 if(isnumber(c)) 15 cout<<"you entered a digit.\n"; 16 else 17 cout<<"you entered a non_digit.\n"; 18 } 19 //----------------------------------- 20 inline bool isnumber(char ch) // 此处为 inline 21 { 22 return ch >= '0' && ch <= '9' ? 1 : 0; 23 }
编译并不认为那是内联函数,对待isnumber函数调用就如调用普通函数那样,产生该函数的调用代码并进行链接。
内联函数体应该尽可能小,且要结构简单,这样签入的代码才不会影响调用函数的主体结构。所以内联函数中,不能含有复杂的结构控制语句,如switch和while。如果内联函数有这些语句,则编译将无视内联声明,只是视同普通函数那样产生调用代码。
当然递归函数属于结构复杂的函数,也不能用来做内联函数。
经验上,内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用的开销相对来说微不足道,所以也没有必要将函数内联。
特别是在自定义数据类型的时候,会涉及大量的小函数定义,这些小函数回被频繁使用,所以,内联函数针对它们特别适合。
另外,内联函数放入头文件。内联函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。内联函数可能要在程序中只出现一次,而且在所有源文件中,其定义必须是完全相同的。把内联函数的定义放在头文件中,可以确保在调用函数式所使用的定义是相同的,并且保证在调用点该函数的定义对编译器可见。
内联函数使用的场合一般为:
(1)函数体适当小,这样就是嵌入工作容易进行,不会破坏原调用主体。
(2)程序中特别是在循环中反复执行该函数,这样就使嵌入的效率相对较高。
(3)程序并不多处出现该函数调用,这样就是嵌入工作量相对较少,代码量也不会剧增。