目录
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度(这可能会对代码的可读性和维护性产生一定的影响)。
宏由于类型无关,也就不够严谨(宏定义中的代码是简单的文本替换,不会进行类型检查)。
先来看一下,宏的优缺点:
优点:
-
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
使用宏更快的原因有以下两点:
避免函数调用开销:在使用函数时,每次调用函数都需要进行函数调用的开销,包括函数栈帧的创建和销毁、参数传递等。而使用宏定义时,宏会在预处理阶段直接进行文本替换,不需要额外的函数调用开销。
减少函数调用的间接性:函数调用会引入间接性,即在调用函数时需要跳转到函数的代码段执行,然后再返回到调用处。而使用宏定义时,代码会直接嵌入到调用处,减少了跳转和返回的开销。
-
更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
下面我举一些实际的例子,更能体现出这一点:
实例一:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
double b = 2.5;
int result1 = SQUARE(a);
double result2 = SQUARE(b);
printf("Square of %d is %d\n", a, result1);
printf("Square of %.2f is %.2f\n", b, result2);
return 0;
}
分析:
该代码可以实现:无论是整数还是浮点数,都可以正确计算它的平方值。
如果写一个函数来实现一个数的平方值,只能设计和传固定的数据类型的参数。
实例二:
例如我们使用malloc函数开辟空间时,需要写成这种形式:
int* p = (int *)malloc(10 * sizeof(int));
写成这样比较麻烦,但是可以用宏定义简化书写:
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type)) //对MALLOC进行宏定义
int* p = MALLOC(10, int);
实例三:
当我们想要实现“打印一个数据”功能,如果用函数实现的话:
打印一个整形:
int b = 15;
printf("the value of b is %d\n", b);
打印一个浮点数:
float f = 4.5f;
printf("the value of f is %f\n", f);
打印不同数据类型的数据时,只需要改变打印形式即可,相同的部分 “the value of x is ...” 无需变化,我们会发现,这样实现会使代码比较冗余(在调用printf函数时,相同的部分要重复书写)
但是,如果使用宏定义,就可以使代码比较简洁:
#define PRINT(n, format) printf("the value of "#n" is " format "\n", n)
int b = 15;
PRINT(b, "%d");
float f = 4.5f;
PRINT(f, "%f");
这里,使用 # ,可以把一个宏参数变成对应的字符串
即 #n-->"n" ,又因为字符串是有自动连接的特点,所以PRINT 就会打印出
"the value of b is %d\n" 的形式
缺点:
-
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度(这可能会对代码的可读性和维护性产生一定的影响)。
-
宏是没法调试的。
函数可以进行调试,因为函数是在编译时被解析和生成的,可以在运行时进行调试,可以被停在某个断点并逐步执行,检查每行代码的执行情况。编译器会生成与函数相关的调试信息,包括函数名、参数、局部变量等,以便在调试器中查看和修改这些信息。
而宏是在预处理阶段被展开的,宏的展开是在编译器解析源代码之前进行的。宏展开后的代码会被直接插入到源代码中,而不是作为一个独立的实体存在。因此,宏在编译时并没有生成与函数类似的调试信息,调试器无法直接对宏进行调试。
-
宏由于类型无关,也就不够严谨(宏定义中的代码是简单的文本替换,不会进行类型检查)。
-
宏可能会带来运算符优先级的问题,导致程容易出现错。
下面定义一个宏实现求一个数的二倍
#define DOUBLE(a) a+a
#include <stdio.h>
int main()
{
int c = 10 * DOUBLE(2);
printf("c = %d\n", c);
return 0;
}
结果是:
c = 22
原因就是因为运算符的优先级问题(10会先与2结合运算):
int c = 10 * 2 + 2;
所以使用宏时,最好写成这种形式:
#define DOUBLE(a) ((a) + (a))
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
-
宏的参数可能会带一些副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
#include <stdio.h>
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
结果是:
x=6 y=10 z=9
分析:
int z = ( (x++) > (y++) ? (x++) : (y++) )
(5++)<(8++),所以后面的(x++)不执行,只执行(y++)
在执行后面的(y++)前,受前面的(y++)的影响,y会先变成9(此时会直接赋值给z,所以z的值是9),再经过后面(y++)后,y最终的值是10;后面的(x++)不执行,前面的(x++)执行,所以x最终的值是6
下面给出几种常见易错的宏使用场景(用ADD函数做例子):
常见ADD宏定义会有这几种写法:
#define ADD(x, y) ((x) + (y))
#define ADD(x, y) (x + y)
#define ADD(x, y) (x) + (y)
#define ADD(x, y) x + y
对于将ADD替换成 x + y ,进行以下运算是会出问题的:
int a = ADD(1, 2)* ADD(2, 2);
int a = 1 + 2 * 2 + 2;
我们想要的结果其实是(1+2)*(2+2) = 12。
我们改进一下,将ADD替换成 (x + y),进行以下运算是会出问题的:
int a = 1, b = 2;
ADD(a | b, a & b);
--ADD(a | b + a & b);
这种你就需要考虑运算符优先级的问题,所以这种写法也不好。
还要在改进一下:
将 ADD(x, y) 替换成 ( (x) + (y) )
从上面几个例子也就能看出宏定义在运算时,需要注意的细节还是挺多的(比较麻烦~)。
补充一点:在进行宏定义时,其后面是不需要加分号的。如果加分号,会导致下面这种情况:
#define ADD(x, y) ((x) + (y));
int a = ADD(1, 2) + 3;
((1) + (2)); +3;
总结:
给个图片易于你做比较:
由上述介绍,可以知道宏和函数都有它们特定的优缺点,那么有没有一种方法可以将两者的优点结合起来呢? 答案是有的——内联函数
内联函数:
内联函数是在函数调用的地方直接将函数的代码插入,而不是进行函数调用。内联函数通过减少函数调用的开销来提高程序的执行效率(宏的特性)。
内联函数是通过在函数声明前加上关键字"inline"来定义的。编译器会根据定义和调用情况来决定是否将函数作为内联函数处理。通常情况下,较小的函数或频繁被调用的函数适合定义为内联函数。
使用内联函数的主要优点是:
- 减少了函数调用的开销:每次函数调用都需要保存当前函数的上下文、设置堆栈帧、跳转到函数体并返回,这些操作都需要时间和空间。内联函数直接将函数体插入调用点,避免了这些开销。
- 提高了程序的执行效率:内联函数可以避免了函数调用和返回的开销,尤其在循环中频繁调用的情况下,可以显著提高程序的执行速度。
- 避免了函数调用时的函数参数压栈和弹栈:内联函数在调用点的参数直接使用,无需将参数压栈或保存在寄存器中,在一定程度上减少了内存读写操作。
需要注意的是(宏带来的缺点):
- 内联函数适合用于较短的函数体,避免对过长的函数进行内联。
- 内联函数可能会增加可执行文件的体积,因为每个调用点都会复制函数体的副本。
- 内联函数不能递归调用自身,因为其没有函数调用和返回的过程。
- 编译器决定是否将函数作为内联函数处理,"inline"关键字只是一个建议,并不是强制的。
总之,内联函数是通过在函数调用点直接插入函数体来减少函数调用开销,提高程序执行效率的一种机制。它可以在一定程度上优化代码,但需要根据具体情况和编译器的选择进行使用。
内联函数也是一种函数,所以它也有函数的一些特性:
参数传递:内联函数可以像普通函数一样接收参数。参数可以通过值传递、指针传递或引用传递的方式进行传递。
返回值:内联函数可以像普通函数一样返回值。返回值可以是基本数据类型、结构体、指针等。
作用域:内联函数的作用域与普通函数相同,在定义的作用域内可见。根据内联函数的定义位置,其作用域可以是全局的或局部的。
局部变量:内联函数可以在函数体内部定义局部变量,这些局部变量在每次函数调用时都会重新创建并销毁。递归:内联函数不支持递归调用,因为内联函数没有函数调用和返回的过程。
函数重载:内联函数可以像普通函数一样进行函数重载,即在作用域中定义多个同名但参数列表不同的内联函数。
访问限制:内联函数可以像普通函数一样根据访问修饰符(public、private、protected)来限制对函数的访问。
需要注意的是,在调用函数时,内联函数是直接将函数体插入调用点,没有创建一个新的栈帧,所以无法进行递归。
补充一点:
在C++中,宏声明常量通常会用const、enum替换;
宏声明函数会用inline(内联函数)替换。
-
内联函数的声明和实现必须在一起
这是因为编译器需要在函数调用点处将函数的定义插入代码。如果声明和实现分离,编译器无法即时获取到函数的定义,从而无法进行内联展开,这样就会导致编译错误。
通常情况下,我们会将内联函数的定义放在类的头文件中,以便于在每个使用该类的地方进行内联展开。这样可以确保所有的调用点都能够正确地展开内联函数。
函数在类的内部定义,并且没有显式标记为inline
。然而,由于它是在类的内部定义的,并且函数体比较简单,编译器会将其视为内联函数。
需要注意的是,编译器并不一定会将所有声明为inline
的函数都展开为内联函数。inline
关键字只是对编译器的建议,最终是否采用内联还是普通函数调用,取决于编译器的具体实现和优化策略。
以下是一个示例,演示了在类内部定义的成员函数如何被视为内联函数:
#include <iostream>
class MyClass {
public:
// 在类内部直接定义成员函数,编译器视为内联函数
void inlineFunction() {
std::cout << "This is an inline function." << std::endl;
}
};
int main() {
MyClass obj;
obj.inlineFunction(); // 内联函数的调用
return 0;
}
在上述示例中,inlineFunction
函数在类的内部定义,并且没有显式标记为inline
。然而,由于它是在类的内部定义的,并且函数体比较简单,编译器会将其视为内联函数。我们在 main()
函数中创建了一个 MyClass
对象 obj
并调用了 inlineFunction
函数。编译器会将函数体的代码插入到函数调用点处,从而避免了实际的函数调用开销。
如果你觉得这篇博客对你有帮助的话 ,希望你能够给我点个赞,鼓励一下我。感谢感谢……