美文网首页
C++ 函数(3)

C++ 函数(3)

作者: maskerII | 来源:发表于2022-06-17 22:14 被阅读0次

    1. 宏函数

    // 1. 宏函数的缺点
    #define ADD(x,y) x+y 
    #define COMAPD(x,y) ((x)<(y)?(x):(y))
    
    void test()
    {
        // 宏函数是直接替换的,在预处理阶段  ADD(10,20) * 2 会被替换成 10+20*2 
        int res1 = ADD(10,20) * 2; 
        cout << res1 << endl; // 50
    
        int a = 1;
        int b = 3;
        // 宏函数是直接替换的,在预处理阶段 COMAPD(++1,3) 会被替换成 ((++1)<(3)?(++1):(3))
        cout << "COMAPD(x,y) = " << COMAPD(++a,b) << endl; // 3
    }
    

    宏函数的缺点:

    • 宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。
    • 在C++中,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。

    2. 内联函数

    2.1 内联函数的基本概念

    为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).

    内联函数继承了宏函数的效率,没有函数调用时开销,又可以像普通函数那样,进行参数、返回值类型的安全检查,还可以作为成员函数。

    在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数,但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

    inline void func(int a);
    

    以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

    inline int func(int a){return ++a;}
    

    注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事是预处理器无法完成的。
    注意:加inline可能成为内联函数,可能不成为,是由编译器决定

    inline int Comapd(int x,int y){
        return x<y?x:y;
    }
    void test02() {
        int a = 1;
        int b = 3;
        // COMAPD(++a,b) 被替换为((++a)<(b) ? (++a):(b))
        cout << "COMAPD(x,y) = " << COMAPD(++a,b) << endl; // 3
    
        a = 1;
        b = 3;
        cout << "Comapd(x,y) = " << Comapd(++a,b) << endl; // 2
    }
    

    2.2 类内部的内联函数

    任何在类内部定义的函数自动成为内联函数

    class Person{
    public:
        Person(){ cout << "构造函数!" << endl; }
        void PrintPerson(){ cout << "输出Person!" << endl; }
    }
    

    2.3 内联函数和编译器

    在普通函数前面加inline是向编译器申请成为内联函数,可能成为内联函数,可能不成为,是由编译器决定

    对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。

    当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。

    类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。

    c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:

    • 存在任何形式的循环语句
    • 存在过多的条件判断语句
    • 函数体不能过于庞大
    • 对函数进行取址操作

    内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。

    2.4 内联函数 总结

    1. 在普通函数前面加inline是向编译器申请成为内联函数,可能成为内联函数,可能不成为,是由编译器决定

    2. 类的成员函数默认加inline,自动申请为内联函数

    3. 普通函数加inline申请成为内联函数,函数体和声明必须结合在一起

    4. 以下情况下不会成为内联函数:

    • 存在过多的条件判断语句
    • 函数体过大
    • 对函数进行取址操作
    • 有任何形式的循环语句

    5. 内联函数的优点

    • 继承了宏函数的效率,没有函数调用时开销
    • 可以像普通函数那样,进行参数、返回值类型的安全检查
    • 类的成员函数默认为内联函数

    3. 函数的默认参数

    c++在声明函数原型时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。

    // 1.函数的默认参数就是给函数的形参赋值
    int myfunc1(int a=0, int b = 0) { // int b=0,就是函数的默认参数,不一定是0
       return a+b;
    }
    
    void test01() {
        // 函数的默认参数的作用
        // 当函数内常要用到形参的某个值,但偶尔要使用其他值时,默认参数可以增加函数的灵活性
        cout << myfunc1(10,20) << endl;
        cout << myfunc1(10) << endl;
        cout << myfunc1() << endl;
    }
    

    函数的默认参数注意事项

    注意1:函数的默认参数后面的参数必须都是默认参数

    // 2.函数的默认参数注意事项
    // 注意1:函数的默认参数后面的参数必须都是默认参数
    // int myfunc2(int a,int b=10,int c) 错误
    // int myfunc2(int a,int b=10,int c){
    
    // }
    

    注意2: 如果函数声明和函数实现分开写,函数声明和函数实现不能同时设置默认参数。

    // 注意2: 函数的声明和实现不能同时有函数的默认参数
    // void myfunc3(int a,int b =0);
    // void myfunc3(int a,int b =10){
    
    // }
    

    函数的占位参数
    c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。

    • 占位参数,没有形参的名字
    • 占位参数在运算符重载时区分前加加或后加加
    • 占位参数也可以有默认值 int = 20
    void TestFunc01(int a,int b,int){
        //函数内部无法使用占位参数
        cout << "a + b = " << a + b << endl;
    }
    //占位参数也可以设置默认值
    void TestFunc02(int a, int b, int = 20){
        //函数内部依旧无法使用占位参数
        cout << "a + b = " << a + b << endl;
    }
    int main()
    {
        //错误调用,占位参数也是参数,必须传参数
        //TestFunc01(10,20); 
        //正确调用
        TestFunc01(10,20,30);
        //正确调用
        TestFunc02(10,20);
        //正确调用
        TestFunc02(10, 20, 30);
    
        return EXIT_SUCCESS;
    }
    

    4. 函数传递的三种方式

    值传递、指针传递、引用传递

    //值传递
    void ValueSwap(int m,int n){
        int temp = m;
        m = n;
        n = temp;
    }
    //地址传递
    void PointerSwap(int* m,int* n){
        int temp = *m;
        *m = *n;
        *n = temp;
    }
    //引用传递
    void ReferenceSwap(int& m,int& n){
        int temp = m;
        m = n;
        n = temp;
    }
    void test()
    {
        int a = 10;
        int b = 20;
        //值传递
        ValueSwap(a, b);
        cout << "a:" << a << " b:" << b << endl;
        //地址传递
        PointerSwap(&a, &b);
        cout << "a:" << a << " b:" << b << endl;
        //引用传递
        ReferenceSwap(a, b);
        cout << "a:" << a << " b:" << b << endl;
    }
    

    5. 函数重载

    在传统c语言中,函数名必须是唯一的,程序中不允许出现同名的函数。在c++中是允许出现同名的函数,这种现象称为函数重载。

    函数重载的目的就是为了方便的使用函数名。

    5.1 函数重载基本语法

    函数重载的条件:在同一作用域内,参数的个数不同,或参数的类型不同,或参数的顺序不同

    // 参数的个数不同
    void func()
    {
        cout << "func()" << endl;
    }
    
    void func(int a)
    {
        cout << "func(int a)" << endl;
    }
    // 参数的类型不同
    void func(char a)
    {
        cout << "func(char a)" << endl;
    }
    
    // 参数的顺序不同
    void func(int a, double b)
    {
        cout << "func(int a, double b)" << endl;
    }
    
    void func(double a, int b)
    {
        cout << "func(double a, int b)" << endl;
    }
    // 调用重载函数的注意
    // 1.严格的类型匹配,如果类型不匹配,就尝试转换,转换成功就调用,转换失败就报错
    
    void test01()
    {
        int a = 10;
        double b = 3.14;
        // 编译器是通过你调用函数时传入的参数来判断需要调用重载的哪个函数,我们调用函数时不需要写返回值,所以函数返回值不能成为函数重载的条件
        func();
        func(a);
        // func(b); // double转换不了int或char 报错
        func(a, b);
        func(b, a);
    
        char c = 'C';
        func(c);
    }
    

    函数重载注意事项

    1.严格的类型匹配,如果类型不匹配,就尝试转换,转换成功就调用,转换失败就报错

    void func(int a)
    {
        cout << "func(int a)" << endl;
    }
    void func(char a)
    {
        cout << "func(char a)" << endl;
    }
    void test03()
    {
        double b = 3.14;
        // 严格的类型匹配,如果类型不匹配,就尝试转换,转换成功就调用,转换失败就报错
        // func(b); double转换不了int或char 报错
    }
    

    2. 函数重载和函数的默认参数一起使用,需要注意二义性问题

    // 函数重载和函数的默认参数一起使用,需要注意二义性问题
    void myfunc(int a, int b = 10)
    {
    
        cout << "myfunc(int a,int b =10)" << endl;
    }
    
    void myfunc(int a)
    {
        cout << "myfunc(int a)" << endl;
    }
    
    void test02()
    {
        //  myfunc(10); // 错误 二义性问题,编译器不知道调用哪个函数
        myfunc(10, 30);
    }
    

    思考:为什么函数返回值不作为重载条件呢?
    当编译器能从上下文中确定唯一的函数的时,如int ret = func(),这个当然是没有问题的。然而,我们在编写程序过程中可以忽略他的返回值。那么这个时候,假如一个函数为
    void func(int x);另一个为int func(int x); 当我们直接调用func(10),这个时候编译器就不确定调用那个函数。所以在c++中禁止使用返回值作为重载的条件。

    5.2 函数重载的原理

    C++函数重载的原理是在汇编时,给各个函数取别名,C语言不能重载的原因是没有取别名

    编译器为了实现函数重载,也是默认为我们做了一些幕后的工作,编译器用不同的参数类型来修饰不同的函数名,比如void func(); 编译器可能会将函数名修饰成_func,当编译器碰到void func(int x),编译器可能将函数名修饰为func_int,当编译器碰到void func(int x,char c),编译器可能会将函数名修饰为_func_int_char我这里使用”可能”这个字眼是因为编译器如何修饰重载的函数名称并没有一个统一的标准,所以不同的编译器可能会产生不同的内部名。

    将源码转成汇编语言

    gcc -S 04_c++test.cpp -o 04_c++test.o
    
    void func(){}
    void func(int x){}
    void func(int x,char y){}
    

    以上三个函数在linux下生成的编译之后的函数名为:

    _Z4funcv //v 代表void,无参数
    _Z4funci //i 代表参数为int类型
    _Z4funcic //i 代表第一个参数为int类型,第二个参数为char类型
    

    6. C++ 调用 C函数

    C++函数在汇编时,会给函数取别名,C语言不会,这时如果C++调用C语言的函数,C++会去找别名的函数,但是C语言没有取别名,这时就会出错

    extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。

    // 这是告诉C++编译器,找下面的函数,要以C语言的方式去寻找
    #ifdef __cplusplus
    extern "C" {
    #endif
        void myfunc();
    #ifdef _cplusplus
    }
    #endif
    

    相关文章

      网友评论

          本文标题:C++ 函数(3)

          本文链接:https://www.haomeiwen.com/subject/leghvrtx.html