掌握C++内联与异常:构建高效且健壮的应用程序
目录
内联函数
什么是内联函数
宏函数与内联函数
内联函数注意事项
异常处理
内联函数
在C++中,通常定义以下函数来求取两个整数的最大值
int max(int x, int y)
{
return x > y ? x : y;
}
为这么一个小的操作定义一个函数的好处有:
(1)阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多;
(2)如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多;
(3)使用函数可以确保统一的行为,每个测试都保证以相同的方式实现;
(4)函数可以重用,不必为其他应用程序重写代码。
虽然有这么多好处,但是写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行。即对于这种简短的语句使用函数开销太大。
在C语言中,我们使用带参数的宏定义这种借助编译器的优化技术来减少程序的执行时间,请定义一个宏完成以上的max函数的功能
那么在C++中有没有相同的技术或者更好的实现方法呢?答案是有的,那就是内联(inline)函数。内联函数作为编译器优化手段的一种技术,在降低运行时间上非常有用。
什么是内联函数
内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
定义函数时,在函数的最前面以关键字“inline”声明函数,该函数即可称为内联函数(内联声明函数)。
inline int max(int x, y)
{
return x > y ? x : y;
}
宏函数与内联函数
在C程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但是看起来像函数。编译预处理器用拷贝宏代码的方式取代函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。
使用宏代码最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生意向不到的边际效应。例如:
#define MAX(a, b) (a) > (b) ? (a) : (b)
int result = MAX(20,10) + 20//result的值是多少?
int result2 = MAX(10,20) + 20//result2的值是多少?
//result = MAX(i, j) + 20; 将被预处理器扩展为: result = (i) > (j) ?(i):(j)+20
可以修改宏代码为
#define MAX(a, b) ((a) > (b) ? (a) : (b))
可以解决上面的错误了,但也不是万无一失的,例如:
int i = 4,j = 3;
result = MAX(i++,j);
cout << result << endl; //result = 5;
cout << i << endl; //i = 6;
//使用MAX的代码段经过预处理器扩展后,result = ((i++) > (j) ? (i++):(j));
宏的另一个缺点就是不可调试,但内联函数是可以调试的。内联函数不是也像宏一样进行代码展开吗?怎么能够调试呢?其实内联函数的”可调试“不是说展开后还能调试,而是在程序的调试(Debug)版本里它根本就没有真正内联,编译器像普通函数那样为它生成含有调试信息的可执行代码。在程序的发行(Release)版本里,编译器才会实施真正的内联。
那C++的内联函数是如何工作的呢?
对于任何内联函数,编译器在符号表(符号表是编译器用来收集和保存字面常量和某些符号常量的地方)里放入函数的声明,包括名字、参数类型、返回值类型。如果编译器没有发现内联函数存在错误,那么该函数的代码也会被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换)。如果正确,内联函数的代码就会直接替换函数调用语句,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能执行类型安全检查和自动类型转换。
—— 内联函数就是在普通函数定义之前加上inline关键字
(1)inline是一个建议,并不是强制性的,后面会学到inline失效的情况
(2)inline的建议如果有效,就会在编译时展开,可以理解为是一种更高级的代码替换机制(类似于宏——预处理)
(3)函数体内容如果太长或者有循环之类的结构,不建议inline,以免造成代码膨胀;比较短小的代码适合用inline。
C++的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员,所以在C++中应尽可能的用内联函数取代宏函数。
对比总结:
宏函数 优点:只是进行字符串的替换,并没有函数的开销,对于比较短小的代码适合使用;
缺点:没有类型检查,存在安全隐患,而且比较容易写错。
如果使用普通函数的方式又会增加开销,所以一些时候可以采用内联函数(结合了宏函数和普通函数的优点)。
inline函数本质也是字符串替换(编译时),所以不会增加开销,但是有类型检查,比较安全。
内联函数注意事项
-
如果要把inline函数声明在头文件中,则必须把函数定义也写在头文件中。若头文件中只有声明没有实现,被认为是没有定义替换规则。
如下,foo函数不能成为内联函数:
inline void foo(int x, int y);//该语句在头文件中
void foo(int x, int y)//实现在.cpp文件中
{ //... }
因为编译器在调用点内联展开函数的代码时,必须能够找到 inline函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。
当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且需要为每个源文件拷贝一份内联函数的定义(每个源文件里的定义必须是完全相同的)。相比之下,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。
从测试文件出发,找到头文件,发现此函数是inline函数,那么要展开替换,必须要有明确的替换规则,但是在头文件中并没有发现替换规则,所以报错未定义问题。
inline函数在头文件必须有定义。
-
谨慎使用内联
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?事实上,内联不是万灵丹,它以代码膨胀(拷贝)为代价,仅仅省去了函数调用的开销,从而提高程序的执行效率。(注意:这里的“函数调用开销”是指参数压栈、跳转、退栈和返回等操作)
如果执行函数体内代码的时间比函数调用的开销大得多,那么 inline 的效率收益会很小。另外,每一处内联函数的调用都要拷贝代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:
-
如果函数体内的代码比较长,使用内联将导致可执行代码膨胀过大。
-
如果函数体内出现循环或其他复杂的控制结构,那么执行函数体内代码的时间将比函数调用开销大得多,因此内联的意义并不大。
实际上,inline 在实现的时候就是对编译器的一种请求,因此编译器完全有权利取消一个函数的内联请求。一个好的编译器能够根据函数的定义体,自动取消不值得的内联,或自动地内联一些没有inline 请求的函数。因此编译器往往选择那些短小而简单的函数来内联。
异常处理
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw.
抛出异常即检测是否产生异常,在 C++ 中,其采用 throw 语句来实现,如果检测到产生异常,则抛出异常。该语句的格式为:
throw 表达式;
-
先定义抛出异常的规则(throw),异常是一个表达式,它的值可以是基本类型,也可以是类;
double division(double x, double y)
{
if(y == 0)
throw "Division by zero condition!";
return x / y;
}
try-catch语句块的语法如下:
try {
//语句块
} catch(异常类型) {
//具体的异常处理...
} ...
catch(异常类型) {
//具体的异常处理...
}
try-catch语句块的catch可以有多个,至少要有一个,否则会报错。
-
执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch块后面的语句,所有 catch 块中的语句都不会被执行;
-
如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个catch 块后面继续执行。
注意:catch的是类型,不是具体信息。