关于C++多态
1. 多态分为静态多态和动态多态
1.1 静态多态
静态多态是编译期决定的:在编译期间就决定了行为,也就是在运行到静态多态的时候函数的执行流程都一样
包括两种:函数重载和模板
1.1.1函数重载
函数重载:函数名相同且参数列表不同称为函数重载
静态多态体现在哪里?
多态体现:在调用相同函数时表现出了不同的行为,
静态体现:在编译期间根据函数名和参数列表能唯一确定代码段中具体调用的函数,所以说函数重载的多态行为在编译期就决定了。
1.1.2模板
模板:在函数或类的前面加上template,就能使函数或类使用T作为参数对象,T的类型在具体调用的时候编译器能通过传入参数的类型自动生成相应的类或函数.
例:
template<class T>
void fun(T a){ cout<<sizeof(a)<<endl; };
main(){
int a ;
char b;
fun(a);
fun(b);
}在编译期间会通过此法语法语义分析,重新生成下面两个函数
void fun(int T){ cout<<sizeof(a)<<endl; };
void fun(charT){ cout<<sizeof(a)<<endl; };
静态多态体现在哪里?
多态体现:定义一个模板函数(或模板类)就能让函数产生不用的行为
静态体现:在编译期间,编译器是能够通过函数调用的地方推断出参数类型,也就是说在编译期间能确定具体的函数,所以就能生成对应的函数,生成之后就和普通函数的函数寻址方式一样了。
1.1.3静态多态总结
静态多态总结:通过上边两个静态多态的实现方法,能发现,模板是比函数重载更方便的,如果只是参数类型不同逻辑代码相同,那么模板方法不用定义多个函数的实现;如果参数类型不同逻辑代码也不同,那么是可以用模板的特化来实现;如果参数个数不同,那么推荐通函数重载。
函数模板编程难度很大,并且代码阅读能力差,一般写库的时候大佬们用的,个人建议非必要不用。
--------------------这篇文章重点想讲的-------------------
1.2 动态多态:在运行时期才能决定函数行为
包括虚函数
1.2.1先讲一些储备知识,不然初学者听着迷糊
- 动态多态指的是类的虚函数行为
- 虚函数的实现:通过虚函数指针和虚函数表来完成的
- 虚函数指针的初始化:在类定义的时候后,构造函数中完成虚函数指针指向(这一部分代码是编译期编译期间自动添加到构造函数末尾的)。
为啥在构造(运行)中完成指向:虚函数指针是指针,分配空间在运行时对象初始化才有,编译期指针(指针属于对象,或者说对象)没有分配内存,所以是无法赋值的 - 虚函数表:一个存放函数指针的指针数组(没错是数组,大小编译期能确定,下边讲),虚函数表中放的是函数指针,指向类中的虚函数的。
- 虚函数表的初始化:在编译期间确定,虚函数重写和覆盖也是发生在编译期
- 为啥在编译期间确定:虚函数是通过virtual关键字修饰,编译期间发现此关键字就将函数地址放到虚函数表中,这样基类的虚函数表就能确定了。
- 那子类的呢?子类的虚表需要考虑父类的虚函数吗?
需要,在编译的时候,如果发现子类继承了父类,那么将父类的虚表复制一份,然后子类的虚表为父类的虚表+子类的虚表,如果有虚函数重写,则将父类的虚函数覆盖,有子类独有的虚函数,则添加到父类虚函数后面, - 为啥说子类虚表是父类加子类呢?因为虚表是数组,大小固定,所以复制过来之后子类还需要添加数组长度。
- 虚表存放在哪个内存空间?
编译器确定且无需修改,并且相同类的不同对象的虚表都一样,所以全局有一份就行,所以存储在全局静态区
1.2.2 多态的体现:
回顾一下多态的定义:具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
父类指针或引用指向子类对象时,调用父类的虚函数,如果父类的虚函数被子类重写了,则函数调用的是子类的实现
例:class Base{ public: virtual void fun(cout<<“Base”);};
class Son1{public: void fun(cout<<“Son1”; )};
void Run(Base* b){ b->fun(); }
class Son1{public: void fun(cout<<“Son1”; );
void main( ){ Son1* s1 = new Son1;
Son2* s2 = new Son2;
Run(s1);
Run(s2);
}
//运行结果:Son1Son2
这里能看出当传入不同子类时,Run()函数的运行结果可能不同,符合多态的定义。
1.2.3 动态的体现
动态是指运行时决定,(那为啥是运行时多态呢,为啥这回就不是静态多态了?)
回顾上边的静态多态的静态:这静态是编译时就能决定函数的行为,(我觉得听着还是比较晦涩,来上例子)
void fun(int a){} //编译期间声称唯一标识如fun_int
void fun(char a){} //编译期间生成唯一标识如fun_char
main(){
int a;
char b;
fun(a); //编译期间会将这一步编程一个函数调用的指定符号(代码区唯一),打个比方替换成@fun_int;
fun(b); //同样,替换成@fun_char
}
比较这两个替换,是不是只要一执行就会调用对应的函数,不会调用其他的,这就是静态(编译期间已经决定了具体运行流程)
动态: 这里的动态指的是运行时才能确定,上例子吧
class Base{ public: virtual void fun(cout<<"Base"); //存在静态区
void only(){cout<<"only";}; //_only 存在代码区
};
class Son1{public: void fun(cout<<"Son1"; )};
void Run(Base* b){
b->only(); //这一步only因为不是虚函数,所以向上边例子一样,编译期就能替换成相应的调用 @_only
b->fun(); //编译期能检查出fun是虚函数,所以调用的时候会查虚表,但是虚表是通过虚函数指针找到的,虚函数指针是在运行时对象初始化是构造函数中初始化的,所以编译期间不能确定函数相应的调用。
//关于为啥在敲代码的时候还内有运行就行“->”出虚函数,这是不是虚指针还没赋值吗? //虽然没有赋值,但是编译期是能够知道你这个类有没有这个函数的,只是不能确定具体运行的是父类还是子类,可以理解成不在编译期间将虚函数替换成@fun,而是在运行期间通过虚指针找到对应的虚表,遍历虚表找到对应的虚函数指针,然后运行
}
class Son1{public: void fun(cout<<"Son1";);
void main( ){
Son1* s1 = new Son1;
Son2* s2 = new Son2;
Run(s1);
Run(s2);
}