C++多态
Posted DR5200
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多态相关的知识,希望对你有一定的参考价值。
文章目录
一.多态的概念
多态 : 函数调用的多种形态,使我们调用函数更加灵活
静态多态 : 函数重载()
#include<iostream>
using namespace std;
int main()
int i;
char ch;
cin>>i; // cin.operator>>(int i);
cin>>ch; // cin.operator>>(char ch);
cout<<i<<endl; // cout.operator<<(int i);
cout<<ch<<endl; // cout.operator<<(char ch);
int i = 0,j = 1;
int d = 1.2,e = 2.5
swap(i,j);
swap(d,e);
动态多态 : 不同类型的对象,去完成同样一件事情,产生的动作和结果是不一样的
二.多态的定义和实现
(1).虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
注意 :
(1). 只有类的非静态成员函数才能加virtual,普通函数不能加virtual
(2). 内联函数,构造函数不能做虚函数
(3). 析构函数可以定义为虚函数(在父类及其派生类中都动态分配内存空间时,必须把父类的析构函数定义为虚函数,实现撤销对象时的多态性).
(4). 虚函数这里virtual和虚继承中的virtual是同一个关键字,但是它们之间没有关系,虚函数这里是为了实现多态,虚继承是为了解决二义性和数据冗余
class A
public:
virtual void func()
;
(2). 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数
class A
public:
virtual void func()
cout << "A::func()" << endl;
;
class B : public A
public:
// 子类的虚函数重写了父类的虚函数
virtual void func()
cout << "B::func()" << endl;
;
虚函数重写的两个例外 :
(1). 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引
用,派生类虚函数返回派生类对象的指针或者引用时,称为协变.
class C;
class D : public C;
class A
public:
virtual C* func() // virtual A* func()
cout << "A::func()" << endl;
return new C;
;
class B : public A
public:
// 子类的虚函数重写了父类的虚函数(协变)
virtual D* func() // virtual B* func()
cout << "B::func()" << endl;
return new D;
;
(2). 析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的
析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class A
public:
virtual ~A()
cout << "~A()" << endl;
;
class B : public A
public:
virtual ~B()
cout << "~B()" << endl;
;
int main()
A* p1 = new A;
A* p2 = new B;
delete p1; // 调用p1->destructor() + operator delete()
delete p2; // 调用p2->destructor() + operator delete()
在继承中我们讲过,父类的析构函数和子类的析构函数构成隐藏,原因在于编译器在调用析构函数时会将析构函数的名字统一改成destructor(),加上virtual关键字后构成重写,只有析构函数重写了,父类指针指向父类,delete调用父类析构函数,父类指针指向子类,delete调用子类析构函数。
若不加virtual,则根据指针的类型去调用析构函数,那么会调用两次父类析构函数,不会调用子类析构函数,如果子类进行了动态内存分配,就会导致内存泄露的问题
// 若没有重写虚函数
A* p1 = new A;
A* p2 = new B;
delete p1; // 调用p1->destructor() + operator delete()
delete p2; // 调用p1->destructor() + operator delete()
(3). 多态的构成条件
(1). 必须通过基类的指针或者引用(不能是对象,在讲解虚函数表时会解释)调用虚函数
(2). 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
#include<iostream>
using namespace std;
class A
public:
virtual void func()
cout << "A::func()" << endl;
;
class B : public A
public:
// 派生类重写基类的虚函数
virtual void func()
cout << "B::func()" << endl;
;
// 基类的引用调用虚函数
void f(A& a)
a.func();
// 基类的指针调用虚函数
void f(A* pa)
pa->func();
int main()
A* pa = new A;
f(pa);
A* pb = new B;
f(pb);
B b;
A& rb = b;
f(rb);
(4). override / final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写错而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
// override
检查派生类虚函数是否重写基类的虚函数,若没有重写编译报错
class A
public:
virtual void func()
cout << "A::func()" << endl;
;
class B : public A
public:
virtual void func()override // 加上override后,若不构成重写会报错
cout << "B::func()" << endl;
;
// final
修饰虚函数,表示该虚函数不能再被重写
class A
public:
virtual void func()final
cout << "A::func()" << endl;
;
class B : public A
public:
virtual void func() // 报错,基类虚函数不能被重写
cout << "B::func()" << endl;
;
(5). 重载/隐藏/重写的区别
重载 : 两个函数在同一作用域下,函数名相同,参数类型或个数或顺序不同,构成函数重载
隐藏(重定义) : 两个函数一个在基类作用域中,一个在派生类作用域中,只要函数名相同,就构成隐藏
重写 : 两个函数为虚函数,一个在基类作用域中,一个在派生类作用域中,函数名,返回值,参数类型,个数,顺序完全相同(协变除外),构成重写
两个基类和派生类的同名函数不构成重写就构成隐藏
三.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
抽象类的价值体现在哪里?
(1). 抽象类用来去表示没有实际对象对应的抽象类型,如 : 植物,人,动物
(2). 体现接口继承,强制子类去重写虚函数(子类如果不重写虚函数,那么子类也无法实例化出对象)
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
#include<iostream>
using namespace std;
class Person
public:
// 纯虚函数
virtual void func() = 0;
;
class Student : public Person
public:
// 强制重写虚函数,否则无法定义对象
virtual void func()
cout << "Student::func()" << endl;
;
class Teacher : public Person
public:
// 强制重写虚函数,否则无法定义对象
virtual void func()
cout << "Teacher::func()" << endl;
;
int main()
Person* ps = new Student;
Person* pt = new Teacher;
ps->func();
pt->func();
四.多态的原理
(1). 虚函数表
#include<iostream>
using namespace std;
class A
public:
virtual void func()
private:
int _a;
;
int main()
A a;
// 答案是8个字节
cout << sizeof(a) << endl;
return 0;
如果之前没有了解过虚表的小伙伴,可能会觉得是4个字节,但实际上是8个字节,这是因为一个含有虚函数的类对象中都会有一个虚函数表指针,该指针指向虚函数表,虚函数表实际上是一个虚函数指针数组,数组中存放虚函数的地址
接下来我们来研究一下派生类中虚表放了什么呢 ?
#include<iostream>
using namespace std;
class A
public:
virtual void func1()
cout << "A::func1()" << endl;
virtual void func2()
cout << "A::func2()" << endl;
void func3()
cout << "A::func3()" << endl;
private:
int _a;
;
class B : public A
public:
virtual void func1()
cout << "B::func1()" << endl;
private:
int _b;
;
int main()
A a;
B b;
return 0;
通过监视窗口观察对象的内存布局可以看到,对象a的虚表指针所指向的虚表中存放了两个虚函数的地址(func1,func2),因为func3不是虚函数,所以并没有将func3的地址存到虚表当中,对象b将对象a的虚表内容拷贝到b的虚表中,由于派生类func1虚函数重写了基类func1虚函数,因此将派生类func1虚函数的地址覆盖b虚表当中继承自a的func1虚函数的地址,因为func2没有完成重写,所以b虚表和a虚表当中func2地址一样
通过观察和测试,总结如下 :
(1). 基类a对象和派生类b对象虚表是不一样的,这里我们发现func1完成了重写,所以b的虚表中存的是重写的Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
(2). func2继承下来后是虚函数,所以放进了虚表,func3也继承下来了,但不是虚函数,所以不会放进虚表。
(3). 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
(4). 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
(5). 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在常量区的
#include<iostream>
using namespace std;
class A
public:
virtual void func1()
cout << "virtual void func1()" << endl;
virtual void func2()
cout << "virtual void func2()" << endl;
void func3()
cout << "void func3()" << endl;
;
int j = 0;
int main()
A a;
A* pa = &a;
printf("vftptr : %p\\n", *(int*)pa);
int i = 0;
printf("栈上地址 : %p\\n", &i);
printf("数据段地址 : %p\\n", &j);
int* k = new int;
printf("堆地址 : %p\\n", k);
const char* h = "hello world";
printf("常量区地址 : %p\\n", h);
return 0;
(6). 构成多态后,基类指针或引用调用虚函数时,不是编译时确定的,而是运行时到指定的对象中的虚表中去找对应的虚函数调用,所以指向父类对象,调用的就是父类的虚函数,指向子类对象,调用的就是子类的虚函数
(7). 如果不构成多态,调用虚函数就是编译时确定调用哪个虚函数,看的是指针或引用的类型,不会去虚表里找
注意 : 为什么多态的条件之一要求必须是父类的指针或引用去调用虚函数呢,父类的对象为什么不行呢?
使用父类的对象去调用发生切片的时候,不会将虚表指针切片给父类(拿个代码举例??)
(8). 对象中的虚表指针是在构造函数初始化列表初始化的(虚表在编译时就生成好了,在初始化列表中将虚表的地址传给虚表指针)
(2).单继承中的虚函数表
#include<iostream>
using namespace std;
class A
public:
virtual void func1() cout << "Base::func1" << endl;
virtual void func2() cout << "Base::func2" << endl;
private:
int a;
;
class B :public A
public:
virtual void func1() cout << "Derive::func1" << endl;
virtual void func3() cout << "Derive::func3" << endl;
virtual void func4() cout << "Derive::func4" << endl;
private:
int b;
;
int main()
A a;
B b;
return 0;
下面我们通过代码来获取虚表中的函数地址
(1). 先取b的地址,强转成一个int*的指针
(2). 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
(3).再强转成VFPTR *,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
(4) .虚表指针传递给PrintVTable进行打印虚表
#include<iostream>
using namespace std;
typedef void(*VFPTR)();
class A
public:
virtual void func1() cout << "A::func1" << endl;
virtual void func2() cout << "A::func2" << endl;
private:
int a;
;
class B :public A
public:
virtual void func1() cout << "B::func1" << endl;
virtual void func3() cout << "B::func3" << endl;
virtual void func4() cout << "B::func4" << endl;
private:
int b;
;
void PrintVTable(VFPTR vTable[])
cout << "虚表的地址 : " << vTable << endl;
for (int i = 0; vTable[i] != nullptr; i++)
printf("第%d个虚函数的地址 : %p\\n", i,vTable[i]);
VFPTR f = vTable[i];
f();
cout << endl;
int main()
A a;
B b;
PrintVTable((VFPTR*)*(int*)&b);
PrintVTable((VFPTR*)*(int*)&a);
return 0;
(3). 多继承中的虚函数表
#include<iostream>
using namespace std;
typedef void(*VFPTR)();
class A
public:
virtual void func1() cout << "A::func1" << endl;
virtual void func2() cout << "A::func2" << endl;
private:
int a;
;
class B
public:
virtual void func1() cout << "B::func1" << endl;
virtual void func2() cout << "B::func2" << endl;
private:
int b;
;
class C : public A, public B
public:
virtual void func1() cout << "C::func1" << endl;
virtual void func3() cout << "C::func3" << endl;
private:
int c;
;
void PrintVTable(VFPTR* vTable)
cout << "虚表地址 :" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; i++)
printf("第%d个虚函数地址 : %p\\n", i,vTable[i]);
VFPTR f 将继承重构为在 C++ 中保持多态功能的组合
C++ 类的多态一(virtual关键字--构造函数深刻理解)
C++ 继承多态关系中的赋值运算符的重载=operator()