C++梁哥笔记day4 | 我的日常分享

C++梁哥笔记day4

一、内联函数

宏函数与内联函数的比较

c++从c中继承的一个重要的特征就是效率。假如c++的效率明显低于c的效率,那么就会有很大的一批程序员不去用c++了。在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。但是c++出现之后,使用预处宏会出现两个问题:第一个在c中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理宏不能用作类的成员函数。

为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function)
内联函数为了继承宏函数的效率,没有函数调用时的开销,然后又可以像普通函数那样,可以进行参数、返回值类型的安全检查,有可以作为成员函数。

预处理宏的缺陷

  • 问题一:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #define ADD(x,y) x+y
    inline int Add(int x,int y){
    return x + y;
    }
    void test01(){
    int ret1 = ADD(10,20) * 10;//希望的结果是300 实际上是210
    int ret2 = Add(10,20) * 10;//希望的结果是300 实际上也是300
    cout<<"ret1="<<ret1<<endl;
    cout<<"ret2="<<ret2<<endl;
    }
    运行结果:
    图片

这个问题可以通过添加括号来解决 #define ADD(x,y) x+y ---> #define ADD(x,y) (x+y) ,问题二则无法解决

  • 问题二:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #define COMPARE(x,y) ((x) < (y) ? (x) : (y))
    inline int Compare(int x,int y){
    return x < y ? x : y;
    }
    void test02(){
    int a=1;
    int b=3;
    cout<<"COMPARE(++a,b)="<<COMPARE(++a,b)<<endl;//3
    cout<<"Compare(++a,b)="<<Compare(++a,b)<<endl;//2
    }
    运行结果:
    图片

按道理运行结果应该是3 2,实际上运行出来都是3,我有点迷啊

  • 问题三:
    预定于宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。

内联函数的概率

在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定于宏一样展开,所以不需要函数调用的开销(压栈入栈)。因此应该不使用宏,使用内联函数。在普通函数(非成员函数)前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。inline void func(int a);以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:inline void func(int a){ return a++;}
注意:编译器将会检查参数列表使用是否正确,并返回值(进行必要的转换)。这些是预处理器无法完成的。内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈、跳转、返回的开销。我们可以理解为内联函数是以空间换时间。

  • 类内部的内联函数
    为了定义内联函数,通常必须在函数第一前面放一个inline关键字,但是类内部定义内联函数时并不是必须的。任何类内部定义的函数自动成为内联函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Person{
    public:
    Person(){
    cout<<"构造函数"<<endl;
    }
    void PrintPerson(){
    cout<<"输出Person!"<<endl;
    }
    }
  • 内联函数和编译器的关系
    内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?
    对于任何类型的函数,编译器会将函数类型(包括函数名字、参数类型、返回值类型)放到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放到符号表。当的调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正确匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器并完成的。

    但是c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:
    1、 不能存在任何形式的循环语句
    2、 不能存在过多的条件判断语句
    3、 函数体不能过于庞大,不能对函数进行取址操作

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


二、缺省函数 函数的默认(缺省 )参数

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

1
2
3
4
5
6
7
8
int my_add(int x=10,int y=20){
return x + y;
}
void test03(){
cout<<my_add()<<endl;//30
cout<<my_add(20)<<endl;//40
cout<<my_add(100,200)<<endl;//300
}

运行结果:
图片

注意:

  1. 函数的默认参数从左到右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数

    1
    2
    3
    4
    5
    6
    7
    8
    int my_add(int x,int y=20,int z=30){
    return x + y + z;
    }
    void test04(){
    cout<<my_add(10)<<endl;//60
    cout<<my_add(10,40)<<endl;//80
    cout<<my_add(10,40,80)<<endl;//130
    }

    运行结果:
    图片

  2. 函数如果是分文件的,默认参数应设置在函数的声明中,在函数的定义中设置默认参数,编译器不会报错,但没有效果。

func.c

1
2
3
int my_add(int x=10,int y=20,int z=30){//在这里进行的默认参数的设置不会起效果 应该在声明中设置
return x + y + z;
}

main.c

1
2
3
4
5
6
7
8
9
10
extern int my_add(int x,y=100,z=200);
void test05(){
cout<<my_add(10)<<endl;//310
cout<<my_add(10,40)<<endl;//250
cout<<my_add(10,40,80)<<endl;//130
}
int main(){
test05();
return 0;
}

运行结果:
图片


三、函数的占位参数

c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明,一般情况下,在函数体内部无法使用占位参数。但是调用函数的时候必须要给占位参数传值。占着茅肯不拉屎。
占位参数也可以设置默认值。

1
2
3
4
5
6
7
8
9
10
11
void func1(int a,int b,int ){
cout<<a<<" "<<b<<endl;
}
void func2(int a,int b,int =20){
cout<<a<<" "<<b<<endl;
}
void test06(){
//func1(10,20);//错误调用 缺少一个参数
func2(10,20);//占位参数有默认值,可以不传值
func1(10,20,30);
}

运行结果:
图片

什么时候用,在后面操作符重载的后置++要用到这个


四、函数重载(c++多态的特性)

函数重载:同一个函数在不同场景下可以具有不用的含义。
函数重载的意义:方便函数名的使用

函数重载基本语法

实现函数重载的条件:同一作用域 参数个数不同 参数类型不同 参数顺序不同

注意:
1、函数的返回值不能作为函数重载的依据
2、函数重载和默认参数一起使用,要注意二义性

函数重载实现原理

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

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

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


五、C++和C混合编程 C库的

1、集成开发环境下
func.c

1
2
3
4
5
6
7
include<stdio.h>
int my_add(int x,int y){
return x+y;
}
int my_sub(int x,int y){
return x-y;
}

func.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef __FUNC_H__
#define __FUNC_H__

#if __cplusplus
extern "C"{
#endif
extern int my_add(int x,int y);
extern int my_sub(int x,int y);
#if __cplusplus
}
#endif

#endif

main.cpp

1
2
3
4
5
6
7
8
#include <stdio.h>
#include "func.h"
using namespace std;
int main(){
cout<<my_add(10,20)<<endl;
cout<<my_sub(10,20)<<endl;
return 0;
}

2、Ubuntu下
main.cpp func.c func.h
混合编译步骤:
gcc -c func.c -o func.o
g++ main.cpp fun.o -o main