文章目录
一. 多态的概念
静态(编译时实现):函数重载。(看起来调用同一个函数,却实现了不同的功能)
int i = 1;
double d = 2.22;
cout << i; // cout.operator<<(int)
cout << d; // cout.operator<<(double)
动态(运行时实现):一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。
//在满足多态的条件下
class Person //父类
{
public:
virtual void who() { cout << "I am father" << endl; }
};
class Student : public Person //子类
{
public:
virtual void who() { cout << "I am child" << endl; }
};
void func(Person& p)//父类引用或指针做参数的函数
{
p.who();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
运行结果
二. 多态的定义及实现
2.1 多态的构成条件
继承中构成多态的两个条件:
1.必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:即被 virtual 修饰的类的非静态成员函数称为虚函数。
(ps:其他函数不能成为虚函数)
class Person //父类
{
public:
virtual void who() { cout << "I am father" << endl; }
};
2.3 虚函数的重写
概念:虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。(虚函数才有重写)
两个例外
1.析构函数名字不同也可以构成重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;//p1、p2都是基类类型指针
delete p1;
delete p2;//p2->destructor(),传入的参数p2是基类类型指针,满足多态条件
//不满足多态条件时,delete p1 和delete p2 调用的都是父类的析构
动态申请的对象,如果给了其父类指针管理,那么需要析构函数完成重写,构成多态
return 0;
}
- 子类重写的虚函数可以不加virtual,因为在继承的时候已经把父类虚函数的属性继承下来了(原本是为了防止子类析构忘加
virtual
,只调用了父类析构没有调子类析构,造成内存泄漏而设计的)为了规范,建议都写上virtual - 当两个函数构成重写,此函数在父类为公有,在子类为私有,子类切片过去做参数时,却也能访问到子类私有的重写函数
2. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
//要求返回值是父子关系的指针或引用
class A{};
class B : public A {};
class Person
{
public:
virtual A* f() {return new A;}
};
class Student : public Person
{
public:
virtual B* f() {return new B;}
};
2.4 构成多态的条件总结
(1)
被调用的函数必须是虚函数(2)
必须通过基类的指针或者引用调用- 派生类必须对基类的虚函数进行重写(三满足)
(3)
函数返回值 完全相同
(4)
函数名 完全相同
(5)
函数参数列表 完全相同
2.5 多态时函数调用规则总结
总结:
构成多态时:传的是哪个类型的对象,就调用传进来的类型的虚函数。跟fun()
参数p
的类型无关;
不构成多态时:fun()
参数p
是什么类型,就调p
的类型的函数
2.6 函数重载、重写、隐藏的对比
2.7 C++11提供的两个关键字,override 和 final
1. final:
修饰类,该类不能被继承
修饰虚函数,表示该虚函数不能再被重写
// 限制类被继承
class A final
{
protected:
int _a;
};
class B : public A //A类不能被继承
{
};
//限制虚函数被重写
class A
{
public:
virtual void f() final
{
cout << "A::f()" << endl;
}
protected:
int _a;
};
class B : public A
{
virtual void f() //此函数时无法被重写的,会报错
{
cout << "B::f()" << endl;
}
};
- 间接限制:把父类构造函数设为私有也不能构成重写(在子类实例化对象时,父类部分需要调用父类的构造函数)
class A
{
private:
A(int a = 0)
:_a(a)
{}
//单例模式:构造函数设为私有,不仅子类对象没法创建,父类对象也没法创建,可用单例模式创建父类对象
public:
static A Create(int a = 0)//成员函数必须是但对象来调用,但对象此时未能创建出来
{ //加上static后就可以不用对象来调用
//new A;
return A(a);
}
protected:
int _a;
};
// 间接限制,子类构成函数无法调用父类构造函数初始化成员,没办法实例化对象
class B : public A
{
};
int main()
{
A aa = A::Create(10);//指定类域函数
B bb;//父类构造函数为私有,子类的父类部分需要用父类构造函数构造,所以子类构造不出来
return 0;
}
2. override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
/ override用法:override写在子类重写函数后面,如果没构成重写会报错
class A
{
public:
virtual void f()
{
cout << "A::f()" << endl;
}
protected:
int _a;
};
class B : public A
{
virtual void f() override
{
cout << "B::f()" << endl;
}
};
三. 抽象类
3.1 概念
纯虚函数:在虚函数后面加上
=0
,则这个虚函数为纯虚函数
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类),抽象类
不能实例化出对象,派生类继承后也不能实例化出对象。
只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
以下出现的问题在下面介绍虚函数表时会有解答
//纯虚函数一般只声明不实现,实现无意义,调用不到
virtual void Drive() = 0;
void fun() { cout << "fun()" << endl; }
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW" << endl;
}
};
int main()
{
Car* p = nullptr;
//p->Drive();//调不到,报错
p->fun();//可以调到;
//fun函数是普通对象,在公共代码段,并不在对象内。这里并没有对空指针解引用,只是把p传给了this
Car* b = new BMW;//抽象类类型必须重写纯虚函数才能实例化对象
BMW b1;
return 0;
}
什么类适合定义成抽象类?
在现实世界没有对应具体事物的类。例如“车”就是一种抽象类的事物,在现实世界中有汽车、火车、自行车…一个“车”并不能描述出一辆对应的具体的车的特征。
3.2 接口函数和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
4.1 虚函数表
先从一道常考笔试题入手:以下sizeof(base)
大小是多少
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char ch = 'A';
};
正确答案为:12
按照类的大小计算规则和结构体大小的计算规则相同,那么算出来答案应该是8,但为什么是12呢?我们通过监视 base 的一个对象来探究答案
通过监视我们发现,在对象B里多了一个指针_vfptr
,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),指向虚函数表。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
有多个虚函数时虚函数表的样子
可以看出,虚函数表实际上是一个函数指针数组,数组里面存着虚函数的地址,调用相关函数时就从虚函数表中找到对应函数地址去找到函数进行调用。
有了以上认识,再回过头看看上面纯虚函数遗留的问题:为什么p->Derive()
会调用不到呢?
答:因为Drive()
是虚函数,虚函数的地址存在对象的虚函数表里,要调用虚函数必须通过对象中的虚函数表找到相应函数地址调用。创建不了对象则表示无法找到虚函数表,找不到函数地址,所以无法调用到。
那么再来讨论一下在以Base类为基类的派生类的虚函数表中又有些什么呢?
通过监视可以看到和得到以下结论
- 子类
s
中有两部分:从父类继承下来的部分;自己的部分。 - 基类
p
对象和派生类s
对象虚表是不一样的。这里子类对父类虚函数完成了重写,所以s
的虚表中存的是子类重写后的虚函数的地址。所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。 - 虚函数继承下来后还是虚函数,其地址放进了虚表,运行时通过虚表找到。普通函数继承后不是虚函数,不放进虚表,放在代码段,编译时通过符号表中的地址找到。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个
nullptr
- 派生类的虚表生成过程:
1.先将基类中的虚表内容拷贝一份到派生类虚表中
2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
一个容易混淆的问题:虚函数存在哪的?虚表存在哪的?
虚函数和虚表都存在代码段
注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
虚表存在代码段可以通过虚表地址和其他分区地址远近来验证。
再来探究为什么必须通过父类的指针或引用才能实现多态的问题?
先看看 切片给父类引用或指针 和 切片给父类对象 有什么不同
可以看到:切片的时候把子类中父类的部分切割赋值过去,但
引用p1虚表和子类的完全相同
,而对象p2的虚表则和子类的完全不同
,所以通过引用和指针传子类对象参数能调到子类的重写函数,通过对象就调不到。