内联函数与宏定义函数的异同

面试时被问到了这个问题,当时突然懵了,所以来整理一下。如有疏漏,还望指摘。
(结论在最后,懒得看的同学可以直接翻到总结)

内联函数与宏定义的函数很相似,都是在运行前,先将一些简短的函数,直接替换为实现代码,以此来省去函数调用时,参数入栈、程序跳转等的时间开支。C++引入了内联机制作为宏函数的改进与补充,但并不是替代,说明他们有很多相似的地方,但也有很多不同的性质。

宏定义的函数:

使用宏定义#define定义的函数。其在预编译阶段就被进行简单的替换,因为只是简单的替换,所以无需定义参数和返回值的类型,也不会对参数进行临时拷贝以及析构。如下宏定义函数:

    #define ADD(a, b) a+b
    void main(){
        int x=1, y=2, z;
        z = ADD(x,y);
    }

这段代码就会被替换为如下代码:

    void main(){
        int x=1, y=2, z;
        z = x+y;
    }

宏定义函数使用时值得注意以下三点:

  • 首先,宏定义函数不会进行类型检查,它不会检查传入的参数类型是否正确,传出的参数类型是否正确,因为在定义时,就不需要规定这些。

  • 其次,因为只是进行简单的替换,所以如果不注意的话,执行顺序可能会和预想的不一样。比如还是上面的宏定义函数,如果我想计算

    z=3 * ADD(x,y);
    

    就会被替换为

    z = 3 * x + y;
    

    这样在执行时,其实会先计算3*x,在计算+y,而与我们预想的逻辑不同。

  • 最后,因为不会制作传入参数的临时拷贝,而只是进行简单的替换,所以如果传入的参数是一个表达式(如x++),或者是一个函数(func())的话,这个表达式或者函数可能会被多次执行。如下:

    #define MUL2(a) a+a
    void main(){
        int x=1, y;
        y = MUL2(x++);
    }
    

    这段代码在执行时就会被替换为:

    void main(){
        int x=1, y;
        y = x++ + x++;
    }
    

    其中x++就会被多次执行。

内联函数:

使用关键词inline修饰的函数为内联函数。内联函数是指在编译阶段,编译器将内联函数展开,替换为等效的实现代码,而不是进行函数调用。内联函数在使用上,在程序员看来,和一般函数无二,需要定义入参类型,返回值类型等,执行完毕会对函数内部的临时变量进行析构等。如下内联函数:

    inline int addint(int a, int b){
        return a+b;
    }
    void main(){
        int x = 1, y = 2, z;
        z = addint(x ,y);
    }

这段代码,在编译时就会被替换下面这样(这里只是做一个简单的示意,实际替换应该会更复杂):

    void main(){
        int x = 1, y = 2, z;
        {
            int _a = x;	//制作传入参数的临时拷贝
            int _b = y;
            int temp;	//制作传出参数的临时拷贝
            temp = _a + _b;	//函数体
            z = temp;	//传出返回值
        }//代码块结束,析构一些临时变量
    }

编译器将函数替换为实现代码,并加入一些处理,使得其执行起来在程序员看来,与一般函数无二。
内联函数在使用时需要注意几点:

  • 首先,类内定义的函数,会被默认为内联函数。
  • 其次,即使定义了内联函数,编译器也会进行检查,如果函数太复杂,也不会进行替换(也可以使用某些关键字进行强制替换,但是性能可能会得不偿失)。
  • 再有,内联函数关键字和函数声明写在一起是无效的,必须与函数的定义写在一起才能生效。
  • 最后,如果内联函数需要在其他源文件中进行调用,那么内联函数的定义必须写在头文件中,写在源文件中会造成编译出错。
  • 最后的最后,在多个源文件中可以定义相同的内联函数,但是在一个源文件中,只能有一个定义相同的内联函数。

总结

从上面的介绍,我们可以看出以下几个相同点和不同点。

相同点:

  • 目的都是代码的替换(虽然内联函数不一定会被替换),以此省去函数调用的消耗。
  • 代码的逻辑都应该是简短的。
  • 如果被多个文件调用,那么宏函数和内联函数都应写在头文件中。

不同点:

  • 内联函数的替换是在编译阶段,宏定义的替换是在预编译阶段。
  • 内联函数会进行类型检查,宏定义函数不会进行类型检查。
  • 内联函数的参数中,表达式和函数只会被执行一次,宏定义函数中可能会被执行多次。
  • 内联函数的执行顺序和正常函数一样,宏定义函数替换后,表达式的执行顺序可能会有不同。
  • 内联函数不一定会被替换,如果代码过于复杂,编译器将不进行替换。而宏定义一定会被替换。

补充

我在后续学习中又发现他们还有一些别的不同点:

  • 内联函数一定是完整的逻辑,上文中的变量需要进行参数传递,函数体内括号也都需要一一对应,等等;而宏定义因为只是替换,因此逻辑可以不完整(比如说使用了函数体外的变量,比如说只有左括号却没有对应的右括号),只需要替换之后整个程序的逻辑是完整的即可。
  • 宏函数的参数可以不一定是完整变量名,甚至不一定是变量,可以是类型名、函数名、或者不完整的变量名、类型名、函数名等。比如:#define MACRO(type, name) type var_##name,这个时候你就可以调用MACRO(int, a)来构建一个名为var_aint型变量(关于##的用法请看我的另一篇博客)。(感觉这一点应该算是上一点的补充)
  • 可以使函数指针指向内联函数(但是此时不再内联,即不再替换,而是和正常函数一样进行跳转),但是不能指向宏函数。
  • 内联函数可以重载、继承,宏函数不可以。
  • 宏函数中使用的函数可以不必提前声明,只需在真正使用这个宏函数的代码前声明过即可。比如如下代码是正确的:
    #define TEST_MACRO0() TEST_MACRO1(1)
    #define TEST_MACRO1(i) TEST_MACRO2(i,2)
    #define TEST_MACRO2(i1, i2) TEST_MACRO3(i1, i2, 3)
    #define TEST_MACRO3(i1, i2, i3) {cout << i1 << i2 << i3 << endl;}
    
    int main(void) { TEST_MACRO0(); }
    
    可以看到,在定义TEST_MACRO0之前,是没有定义TEST_MACRO1的,但是只要在main函数真正调用TEST_MACRO0之前定义好TEST_MACRO1,那么代码就是正确的。

感觉说了这么多不同点,其实都是宏函数与内联函数两种不同基本性质的不同体现:

  • 宏函数只是预编译阶段纯粹的代码层面的简单替换。因为只是简答替换,所以可能会被执行多次,可能顺序会不同,参数可以不必是变量,逻辑不必完整。因为在预编译阶段的替换,所以不必提前声明。
  • 宏函数一定会被替换,而内联可以被替换也可以不替换。因为有可能不被替换,所以内联函数必须可以被当做正常函数使用,因此必须具备正常函数所有的基本特点,所以必须进行错误检测,参数必须正常合法,逻辑必须完备,参数中的函数和表达式必须只会被执行一次,顺序必须和正常函数相同,必须提前声明,可以使用函数指针,可以重载继承。
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值