C++多态
目录
C++多态
1静态联编和动态联编
2多态实现原理
3多态实例
4抽象基类和纯虚函数
5虚析构和纯虚析构函数
5.1关于虚析构
5.2关于纯虚析构函数
6类型转换概念以及安全问题
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
多态的基本概念:
多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征
多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将“what”和“how”分离开来。
多态性改善了代买的可读性和组织性,同时也使创建的程序
具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
C++支持编译器多态(静态多态)和运行时多态(动态多态)
运算符重载和函数重载就是编译器多态(静态多态),而派生类和虚函数实现运行时多态(动态多态)。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)
如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。
而如果函数的调用地址不能编译,不能在编译期间确定,而需要在运行时才能决定,这就是晚绑定(动态多态,运行时多态)
发生多态条件:
- 有继承
- 子类重写父类虚函数
- 类型兼容,父类指针/引用指向子类对象
1静态联编和动态联编
.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1class A
2{
3public:
4 void show();
5
6};
7class AOne:public A
8{
9public:
10 void show();
11
12};
13void Test_show(A & a);
14
.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1void AOne::show()
2{
3 qDebug()<<"AOne::show()";
4}
5void A::show()
6{
7 qDebug()<<"A::show()";
8}
9
10//静态联编
11void Test_show(A & a)
12{
13 a.show();
14}
15
具体操作
1
2
3 1AOne object;
2Test_show(object);
3
因为Test_show函数参数是A类型的,所以不管实参传递是啥,都会调用A的成员函数。
Test_show函数如下:
1
2
3
4
5
6 1//静态联编
2void Test_show(A & a)
3{
4 a.show();
5}
6
Test_show()函数的地址早就绑定好了,在函数编译阶段就完成了绑定,早绑定,静态联编,编译阶段就确定好了地址,如果想调用AOne,不能提前绑定地址,需要在函数运行时,确定地址。
现在对父类中的show()函数进行修改,修改为动态联编,也就是将父类的函数变成虚函数,在
Test_show函数运行的时候去分配地址,而不是一开始就分配好。
1
2
3
4
5
6
7 1class A
2{
3public:
4 virtual void show();
5
6};
7
之后进行如下操作:
1
2
3
4
5 1AOne object;
2Test_show(object);
3A a_obeject;
4Test_show(a_obeject);
5
输出:
可以看出,这就是动态联编,函数在运行的时候进行判断,根据参数的类型,调用相应的show成员函数
此时,就发生了多态
2多态实现原理
父类的引用或者指针指向子类的对象
我们来看看A类所占用空间的大小:
此时的A只有一个虚成员函数
1
2
3
4
5
6
7 1class A
2{
3public:
4 virtual void show();
5
6};
7
1
2
3 1qDebug()<<"sizeof(A)"<<sizeof(A);
2
3
输出:
如果成员函数不是虚函数,那么sizeof(A)应该是1,
成员函数是虚函数,会诞生一个指针,如果虚继承类似名为
virtual function pointer——虚函数表指针:简称vfptr
拿class A举例,此时的class AOne中没有任何的内容;
class A中有虚函数表指针,指向自己的虚函数
而class AOne会将A中的所有全部继承,并且在一开始,AOne的虚函数表指针指向A中的虚函数表,
如①所示
对象创建时,调用构造函数时,将所有的虚函数表指针都指向自己的虚函数表,
如②所示
此时调用也只能调用A类中的show();
如果在AOne类中,添加同名成员函数:
1
2
3
4
5
6
7 1class AOne:public A
2{
3public:
4 void show();
5
6};
7
那么内部结构会发生变化:
AOne会将自己的虚函数表进行重写,覆盖掉A::show();
子类写和父类虚函数同名的函数——重写
**重写:**返回值,参数类型,参数顺序,函数名,
都相同。
重写(override):是指
子类重新定义父类虚函数的方法
**重载(overload):**是指允许存在
多个****同名函数,而这些函数的
参数表****不同
知识点补充:
重载、覆盖 与 隐藏 的详细释义自行Google;
(下述<函数参数相同>是指 参数个数 、 *参数类型 和 返回类型 *均相同)
一、重载(overload):
特征: 函数名相同 、函数参数不同、 必须位于同一个域(类)中;
二、覆盖(override):
特征: 函数名相同 、函数参数相同、 分别位于派生类和基类中、virtual(虚函数);
三、隐藏(hide):
即:派生类中函数隐藏(屏蔽)了基类中的同名函数。
情形1: 函数名相同、 函数参数相同、 分别位于派生类和基类中、virtual — 为 覆盖;
情形2: 函数名相同、 函数参数相同、 分别位于派生类和基类中 — 为 隐藏;(即跟覆盖的区别是基类中函数是否为虚函数)
情形3 : 函数名相同、 函数参数不同、 分别位于派生类和基类中 — 为 隐藏;(即与重载的区别是两个函数是否在同一个域(类)中)
关于隐藏的理解,在调用一个类的成员函数时,编译器会沿着类的继承链逐级向上查找函数的定义,如果找到了则停止查找;所以如果一个派生类和一个基类都有一个同名函数(不论函数参数是否相同),而
编译器最终选择了在派生类中的函数,那么就说这个派生类的成员函数“隐藏”了基类的同名函数,即它阻止了编译器继续向上查找函数的定义。(所以对于上述的情形3,同名函数,虽函数参数不同,但位于派生类和基类中时,基类函数会审美观点屏蔽。) 参考链接:http://www.cppblog.com/lingyun1120/archive/2011/04/27/145135.html
补充一点:关于隐藏的情形2,相当于是重新定义了基类中non-virtual函数,这样其实并不好,详情可参考 《Effective C++》中条款36:绝不重新定义继承而来的non-virtual函数。
(来自牛客网@MSean)
发生多态时,不是单纯的继承调用,注意!
此时,将父类的引用或者对象指向子类的对象
A * t = new AOne ;//发生多态
t.show();//调用的是AOne::show();
注意,在子类AOne的show成员函数,最好在前面加上
virtual关键字(可读性强)
用sizeof再次理解
如果不重写父类中的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1class calculate
2{
3public:
4 calculate(int itema = 1,int itemb = 2);
5 virtual ~calculate();
6 int a,b;
7 virtual void show();
8
9};
10class Add:public calculate
11{
12public:
13 Add(int a_itema = 1,int a_itemb = 2);
14 virtual ~Add();
15
16};
17class Sub:public calculate
18{
19public:
20 Sub(int b_itema = 1,int b_itemb = 2);
21 virtual ~Sub();
22
23};
24
操作如下:
1
2
3
4 1qDebug()<<"sizeof calculate:"<<sizeof(calculate);
2qDebug()<<"sizeof Add:"<<sizeof(Add);
3qDebug()<<"sizeof Sub:"<<sizeof(Sub);
4
输出:
因为有虚函数,所以 virtual function pointer 大小是4byte
两个int类型,8byte,子类全部继承,又没有重写虚函数,所以都是12byte,因为连虚函数表指针也继承了
如果重写虚函数,大小也是12byte,因为重写虚函数只是把以前的虚函数表指针指向的内容覆盖了,指针本身还是在的
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1class calculate
2{
3public:
4 calculate(int itema = 1,int itemb = 2);
5 virtual ~calculate();
6 int a,b;
7 virtual void show();
8};
9class Add:public calculate
10{
11public:
12 Add(int a_itema = 1,int a_itemb = 2);
13 virtual ~Add();
14 virtual void show();
15};
16class Sub:public calculate
17{
18public:
19 Sub(int b_itema = 1,int b_itemb = 2);
20 virtual ~Sub();
21 virtual void show();
22};
23
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 1#include <iostream>
2
3using namespace std;
4
5class A
6{
7public:
8 virtual void print()
9 {
10 cout << "A::print()" << "\n";
11 }
12};
13
14class B: public A
15{
16public: virtual void print()
17 {
18 cout << "B::print()" << "\n";
19 }
20};
21
22class C: public A
23{
24public: virtual void print()
25 {
26 cout << "C::print()" << "\n";
27 }
28};
29
30void print(A a)
31{
32 a.print();
33}
34
35int main()
36{
37 A a, *aa, *ab, *ac;
38 B b;
39 C c;
40 aa = &a;
41 ab = &b;
42 ac = &c;
43 a.print();
44 b.print();
45 c.print();
46 aa->print();
47 ab->print();
48 ac->print();
49 print(a);
50 print(b);
51 print(c);
52}
53
54
输出:
1
2 1A::print() B::print() C::print() A::print() B::print() C::print() A::print() A::print() A::print()
2
那么什么情况,什么函数,不能使用多态
什么函数不能声明为虚函数:
1.普通函数(不能被覆盖)
2.友元函数(C++不支持友元函数继承)
3.内联函数(编译期间展开,虚函数是在运行期间绑定)
4.构造函数(没有对象不能使用构造函数,先有构造函数后有虚函数,虚函数是对对象的动作)
5.静态成员函数(只有一份大家共享)
什么函数可以:
1.普通的成员函数
2.析构函数
虚函数依赖于
虚函数表,而
虚函数表依赖于
构造函数初始化
3多态实例
多态的出现,符合C++的一条行事风格
利于后期扩展,更有利于开发,可读性高,唯一缺点:效率比C低
开闭原则:
对扩展开放,对修改关闭
.h 父类是calculate 两个子类Add和**Sub **
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1class calculate
2{
3public:
4 calculate(int itema = 1,int itemb = 2);
5 virtual ~calculate();
6 int a,b;
7 virtual void show();
8
9};
10class Add:public calculate
11{
12public:
13 Add(int a_itema = 1,int a_itemb = 2);
14 virtual ~Add();
15 virtual void show();
16};
17class Sub:public calculate
18{
19public:
20 Sub(int b_itema = 1,int b_itemb = 2);
21 virtual ~Sub();
22 virtual void show();
23};
24
.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 1calculate::calculate(int itema,int itemb)
2{
3 qDebug()<<"进入calculate构造函数";
4 this->a = itema;
5 this->b = itemb;
6}
7calculate::~calculate()
8{
9 qDebug()<<"进入calculate析构函数";
10}
11Add::Add(int a_itema,int a_itemb):calculate(a_itema,a_itemb)
12{
13 qDebug()<<"进入Add构造函数";
14}
15Add::~Add()
16{
17 qDebug()<<"进入Add析构函数";
18}
19
20Sub::Sub(int b_itema,int b_itemb):calculate(b_itema,b_itemb)
21{
22 qDebug()<<"进入Sub构造函数";
23}
24Sub::~Sub()
25{
26 qDebug()<<"进入Sub析构函数";
27}
28void calculate::show()
29{
30 qDebug()<<"calculate::show():"<<endl
31 <<"this->a:"<<this->a<<endl
32 <<"this->b:"<<this->b;
33}
34void Add::show()
35{
36 qDebug()<<"Add::show()"<<endl
37 <<"ADD result:"<<this->a+this->b;
38
39}
40void Sub::show()
41{
42 qDebug()<<"Sub::show()"<<endl
43 <<"Sub result:"<<this->a - this->b;
44}
45
.cpp具体操作
1
2
3
4
5
6
7
8
9
10
11
12 1 calculate * p = new calculate(2,4);
2 p->show();
3 delete p;
4
5 p = new Add(4,4);
6 p->show();
7 delete p;
8
9 p = new Sub(0,4);
10 p->show();
11 delete p;
12
输出:
对以上内容进行简单的分析
Part 1:
1
2
3
4 1 calculate * p = new calculate(2,4);
2 p->show();
3 delete p;
4
对应的输出:
进入构造函数,输出a和b的值,delete对应析构函数
Part 2:
1
2
3
4
5 1 p = new Add(4,4);
2 p->show();
3 delete p;
4
5
先调用父类构造,再调用子类构造,之后是a和b两个值相加
最后,先子类析构,然后父类析构
Part 3:
1
2
3
4 1 p = new Sub(0,4);
2 p->show();
3 delete p;
4
先调用父类构造,再调用子类构造,之后是a和b两个值相减
最后,先子类析构,然后父类析构
注意,以上除了show函数被virtual修饰,析构函数也被virtual修饰
虚析构函数:
如果没有virtual修饰,则输出如下:
如果基类里有虚函数,定义了基类指针指向派生类,就会需要定义基类虚析构,这样,基类指针析构的时候,就会先析构派生
类,再析构基类。
如果不定义虚析构,就会基类指针直接析构基类。这样派生类对象销毁不完整。
补充:默认参数可以放在函数声明或者定义中,但只能放在二者之一
通常我们都将默认参数放在函数声明中
比如之前例子中所出现的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1class calculate
2{
3public:
4 calculate(int itema = 1,int itemb = 2);
5 virtual ~calculate();
6 int a,b;
7 virtual void show();
8
9};
10class Add:public calculate
11{
12public:
13 Add(int a_itema = 1,int a_itemb = 2);
14 virtual ~Add();
15 virtual void show();
16};
17class Sub:public calculate
18{
19public:
20 Sub(int b_itema = 1,int b_itemb = 2);
21 virtual ~Sub();
22 virtual void show();
23};
24
题目1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 1#include <iostream>
2
3using namespace std;
4
5class A
6{
7public:
8 virtual void print()
9 {
10 cout << "A::print()" << "\n";
11 }
12};
13
14class B: public A
15{
16public: virtual void print()
17 {
18 cout << "B::print()" << "\n";
19 }
20};
21
22class C: public A
23{
24public: virtual void print()
25 {
26 cout << "C::print()" << "\n";
27 }
28};
29
30void print(A a)
31{
32 a.print();
33}
34
35int main()
36{
37 A a, *aa, *ab, *ac;
38 B b;
39 C c;
40 aa = &a;
41 ab = &b;
42 ac = &c;
43 a.print();
44 b.print();
45 c.print();
46 aa->print();
47 ab->print();
48 ac->print();
49 print(a);
50 print(b);
51 print(c);
52}
53
54
输出:
1
2 1A::print() B::print() C::print() A::print() B::print() C::print() A::print() A::print() A::print()
2
题目2:
结构体也是一样的
1
2
3
4
5
6
7
8
9
10 1struct A{
2 void foo(){printf("foo");}
3 virtual void bar(){printf("bar");}
4 A(){bar();}
5};
6struct B:A{
7 void foo(){printf("b_foo");}
8 void bar(){printf("b_bar");}
9};
10
1
2
3
4 1A *p=new B;
2p->foo();
3p->bar();
4
输出:
1
2 1barfoob_bar
2
解释:
A *p=newB;// A类指针指向一个实例化对象B, B类继承A类,先调用父类的无参构造函数,bar()输出bar,B类没有自己显示定义的构造函数。
此时发生多态,不是单纯的继承调用,注意!
p->foo();//执行B类里的foo()函数,因为foo不是虚函数,所以直接调用父类的foo函数,输出foo
p->bar();//执行B类的bar()函数, 该函数为虚函数,调用子类的实现,输出b_bar
题目3:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 1#include<iostream>
2using namespace std;
3
4class Base
5{
6public:
7 virtual int foo(int x)
8 {
9 return x * 10;
10 }
11
12 int foo(char x[14])
13 {
14 return sizeof(x) + 10;
15 }
16};
17
18class Derived: public Base
19{
20 int foo(int x)
21 {
22 return x * 20;
23 }
24
25 virtual int foo(char x[10])
26 {
27 return sizeof(x) + 20;
28 }
29} ;
30
31int main()
32{
33 Derived stDerived;
34 Base *pstBase = &stDerived;
35
36 char x[10];
37 printf("%d\n", pstBase->foo(100) + pstBase->foo(x));
38
39 return 0;
40}
41
答案:2014
第一个foo:调用继承的函数,因为父函数有virtual修饰,被子类的同名函数覆盖(重写)。
第二个foo:调用父类函数,因为 父函数没有有virtual修饰。
此时发生多态,不是单纯的继承调用,注意!
(题目来源:牛客网)
4抽象基类和纯虚函数
如果父类中有纯虚函数,子类继承父类,必须在子类中实现纯虚函数。
纯虚函数:
virtual 返回值 函数名 = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1class calculate
2{
3public:
4 calculate(int itema = 1,int itemb = 2);
5 virtual ~calculate();
6 int a,b;
7 virtual void show();
8 virtual void pure() = 0;//纯虚函数
9
10};
11class Add:public calculate
12{
13public:
14 Add(int a_itema = 1,int a_itemb = 2);
15 virtual ~Add();
16 virtual void show();
17 virtual void pure();
18};
19
在calculate中,增加一行,**virtual void pure() = 0; **
因为父类中有纯虚函数,所以,子类中必须对其进行实现,所以可见Add类中有一行
virtual void pure();
在Cpp中
1
2
3
4
5 1void Add::pure()
2{
3 qDebug()<<"进入Add中的pure";
4}
5
如果父类中,有了纯虚函数,父类无法实例化(定义)对象,该父类为抽象类
所以calculate是抽象类, 尝试实例化(定义)对象
1
2
3 1calculate * p = new calculate(2,4);
2
3
可见,编译器无法创建calculate的对象
综上,
抽象类无法实例化对象
含有纯虚函数的类,其继承子类必须实现纯虚函数
5虚析构和纯虚析构函数
5.1关于虚析构
如果基类里有
虚函数,定义了基类指针指向派生类,就会需要定义基类虚析构,这样,基类指针析构的时候,就会先析构派生
类,再析构基类。
如果不定义虚析构,就会基类指针直接析构基类。这样派生类对象销毁不完整。
3中有详细解释
5.2关于纯虚析构函数
**格式:**virtual ~ 函数名 ()= 0;
类内声明,必须类外实现(定义)
如果类中含有纯虚析构函数,则该类也为抽象类,无法**实现(定义)**对象
6类型转换概念以及安全问题
结论:
不发生多态时:
向下类型转换是不安全的**(父/基类转换为子/派生类)**
向上类型转换是安全的**(子/派生类转换为父/基类)**
发生多态时:都是安全的
当不发生多态时:
子类的大小,是大于等于父类的,所以当出现如下操作
子类 * p = (子类 *)new (父类);
将本来寻址访问是①的指针,指向寻址范围扩大的②,程序出错
也就说,将**(父/基类转换为子/派生类),因为父类在上,子类在下**
所以也叫
向下类型转换是不安全的。
同理,
向上类型转换是安全的**(子/派生类转换为父/基类)是很安全的。**
当发生多态时:
多态就是父类指针/引用指向子类对象,本身就是
向上类型转换是安全的**(子/派生类转换为父/基类),固然是安全的。**
向下类型转换也都在寻址访问内,所以安全。