C++基础整理(7)之关键字 inline(内联函数)
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++ 的 inline/内联函数
提示:本文为 C++ 中 inline 的用法和举例
一、inline关键字(内联函数)
在C++中,inline是一个用于函数定义的修饰符,格式是直接在函数返回类型的最前面加上这个inline,产生效果是建议编译器将整个函数体代码段插入到每个调用点然后展开,从而消除函数调用的开销(代码转移到函数对应的内存地址执行,会记忆转移前的地址,反复调用就会转移多次),inline直接将函数嵌入到函数开始调用的那块。
inline return_type function_name(parameter list) {
// 函数体
}
使用inline关键字的主要目的是为了提高函数的执行效率。当函数体较小且被频繁调用时,内联函数通常能带来性能上的提升。这是因为内联函数可以避免函数调用的开销,包括参数传递、堆栈的创建和销毁等。然而,需要注意的是,inline是一个对编译器的建议,编译器可以选择忽略这个建议,特别是当长且复杂(如大量循环等)的内联函数会导致嵌入后的代码膨胀反而影响性能。
1、inline使用举例
下面是内联函数的示例:
inline int add(int a, int b) {
return a + b;
}
在上面的例子中,add函数被声明为内联函数。当编译器遇到add函数的调用时,它可能会尝试将函数的代码直接插入到调用点,而不是进行常规的函数调用。
2、类成员函数的内联
类的成员函数可以被inline。可以定义成内联函数。
对于在类体内部定义的成员函数,其实默认就是inline的,此时成员函数一般都省略写inline关键字。然而,是否内联还是取决于编译器,编译器可能会根据函数的大小和其他因素来决定是否将函数内联。
但应当特别留意的是,若成员函数在类体外部定义而非内部定义,那么系统并不会默认将其视为内联(inline)函数。这意味着在调用这些成员函数时,会遵循与普通函数调用相同的机制,包括可能的压栈和跳转开销。如果希望将这些成员函数指定为内联函数,以期望获得性能上的优化,则必须显式地在函数声明前使用inline关键字进行声明。这样做可以指导编译器在可能的情况下将函数体直接插入到调用点,从而消除函数调用的开销,提高程序执行的效率。
下面给一个外部定义类成员函数内联的例子:
// MyClass.h 文件里
class MyClass {
public:
MyClass(int value);
inline int getValue() const; // 成员函数在类内声明为inline,也可以不加inline,默认就是
private:
int m_value;
};
// MyClass.cpp 文件里
#include "MyClass.h"
// MyClass的构造函数
MyClass::MyClass(int value) : m_value(value) {}
// getValue成员函数在类外定义为inline
inline int MyClass::getValue() const { // 注意这里也使用了inline关键字
return m_value;
}
3、inline缺陷
尽管内联函数可以提高性能,但它们也有一些缺点和限制:
代码膨胀:如果内联函数体很大,或者函数被频繁调用,那么将函数体插入到每个调用点可能会导致代码体积显著增加,从而可能降低指令缓存的效率,反而影响性能。
编译时开销:内联函数可能导致编译时间增加,因为编译器需要处理更多的代码。
链接时问题:内联函数通常需要在头文件中定义,而不是在源文件中。这可能会导致多个定义的问题,除非正确地使用inline关键字和链接器选项来避免这些问题。
编译器优化:编译器并不一定会内联所有标记为inline的函数。是否内联一个函数取决于多种因素,包括函数的大小、调用频率以及编译器的优化设置等。
因此,在决定是否使用内联函数时,需要权衡其带来的性能提升与可能带来的代码膨胀和编译时开销。在大多数情况下,最好让编译器自动决定哪些函数应该内联,而不是显式地使用inline关键字。
4、 内联(inline)与宏定义(define)的区别比较
内联函数与宏定义在C++编程中都有展开代码来提高代码执行效率的作用,它们区别如下:
(1)首先,从定义和性质上看,宏定义并非真正的函数,它在预处理阶段进行文本替换,即用宏体替换所有的宏名。而内联函数本质则仍然是一种函数,它在编译时直接嵌入到目标代码中,替换了函数调用,从而消除了函数调用的开销。内联函数具有普通函数所有的特性,比如有返回值、参数列表等,可以进行类型安全检查,而宏定义则没有这些特性。
(2)从使用方式和调试角度看,宏定义在定义时需要小心处理宏参数,以避免出现二义性,而内联函数则不存在这个问题。此外,由于内联函数是函数,因此它可以进行调试,而宏定义则不能。
(3)从作用范围上看,内联函数作为类的成员函数时,可以访问类的所有成员(公有、保护、私有),而宏定义则不能。
(4)在代码展开方面,虽然宏定义和内联函数都实现了代码的直接插入,但它们的处理时机不同。宏定义在预处理阶段就完成了所有的替换工作,而内联函数则是在编译阶段进行插入。这样的差异使得内联函数在效率提升的同时,还能确保代码的安全性和可读性。通过避免函数调用的压栈和清栈开销,内联函数进一步提高了程序的执行效率
总结
C语言中32位浮点数的格式
以 GNU C为例,它遵循 IEEE 754-2008标准中制定的浮点表示规范。在该规范中定义了 5种不同大小的基础二进制浮点格式,包括:16位,32位,64位,128位,256位。其中,32位的格式被用作标准 C类型的 float,64位的格式被用作标准 C类型的 double,128位的格式被用作标准 C类型的 long double。
每种浮点格式都由三部分组成,包括:1个比特的符号位、若干比特的指数位、若干比特的小数位,如下图所示:
以 C语言中 float类型(32位浮点)为例,它的符号位占 1位,指数位占 8位,小数位占 23位,如下图所示:
值得注意的是,
1)指数位编码的指数以 2为底数,若要表示很小的数,例如 1/(2^n) == 2^(-n),则指数位会出现负数 (-n),那么还需要专门的符号位来表示,因此指数位是以 127为偏移的,也就是说,一个数在用浮点数来表示时,其指数位的值是实际指数值加上 127。对于 64位浮点数,它的指数位是 11个比特,以 1023为偏移。
2)浮点数的小数部分是将该浮点数表示为 2^n * 1.m后,0.m那部分的数值,也就是说,整数部分始终是 1,且被忽略了(不用保存),这样就能省出一位来表示更高的精度。
举例来说,
1)0.5 = 2^(-1) * 1.0,其指数位为 (-1 + 127),小数位为 0.0,其二进制是:0 01111110 00000000000000000000000,也就是 0x3F000000。
2)5 = 2^(2) * 1.25,其指数位为 (2 + 127),小数位为 0.25,其二进制是:0 10000001 01000000000000000000000, 也就是 0x40A00000。
测试代码如下:
#include
int main(void)
{
float a = 0.5;
unsigned int *b = (unsigned int*)&a;
printf("b = 0x%X\n", *b);
a = 5;
printf("b = 0x%X\n", *b);
}
程序运行结果如下:
$ gcc -o main main.c
$ ./main
b = 0x3F000000
b = 0x40A00000
从 32位浮点的指数宽度来看,理论上,它可以表示很大的数,比如:2^(128) * 1.9999999,这比我们 32位整型能表示的范围(2^(32) - 1)大的多,但受限于浮点小数位的宽度,有些 32位整型数是无法用浮点数来表示的,也就是会出现空洞,原因是浮点数的小数部分精度有限,当小数部分比 1/(2^23)还小时,就无法准确表示该整型数,例如:2^(24) + 1这个数用 32位整型来表示是没有问题的,但用 32位浮点来表示,则会被舍入为:2^24。
测试代码如下:
#include
int main(void)
{
float a = 1 << 24;
unsigned b = 1 << 24;
printf("a = %f, b = %u\n", a, b);
a += 1;
b += 1;
printf("a = %f, b = %u\n", a, b);
return 0;
}
程序运行结果如下:
$ gcc -o main main.c
$ ./main
a = 16777216.000000, b = 16777216
a = 16777216.000000, b = 16777217
参考资料: