多态
- 1.多态的概念
- 2.多态的定义和实现
- 2.1多态构成条件
- 2.2虚函数
- 2.3虚函数的重写(覆盖)
- 2.4 C++11 override 和 final
- 2.5重载、重写(覆盖)、隐藏(重定义)的对比
- 3.抽象类
- 4.多态的原理
- 5.单继承和多继承关系的虚函数表
- 5.1单继承
- 5.2多继承
- 5.3菱形继承和多态
1.多态的概念
多态的概念:同样的一个行为,不同的对象去完成时会产生不同的状态。
例子:拿买票举例,军人、学生、普通人(子类)都是人(父类),但军人买票可以优选选票,学生买票可以半价,普通人买票就要全价了。因此要实现多态必先继承。
2.多态的定义和实现
2.1多态构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面的例子定义父类Person,让子类Student继承Preson。Person对象买票全价,Student对象买票半价。
已经形成了继承关系,实现多态还需要以下两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
大家先记住这两个条件,后面讲解虚函数和多态原理后大家就明白了。
2.2虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
//这里和菱形虚拟继承公用了关键字,但两者是没有关联的
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.3虚函数的重写(覆盖)
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写比较好理解,覆盖有点原理层面的意思。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了所以认为派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/ //这个写法是可以的
};
void Func(Person& p)
{
p.BuyTicket(); //因为是父类引用去调用,所以构成多态
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
谈一谈实现继承和接口继承:
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
虚函数重写的例外:
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但必须是父子关系的指针或者引用(也可以是其它父子类),称为协变。
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;}
};
- 析构函数的重写(基类与派生类析构函数的名字不同)
析构函数看似不同名,违背了虚函数重写的规则,但为了保证资源的正确释放,编译器会对析构函数做特殊处理,编译后会统一处理为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; //如果父类析构不设计为虚函数的话可能造成内存泄漏
delete p1;
delete p2;
return 0;
}
2.4 C++11 override 和 final
C++对虚函数重写的要求比较严格,有时会因为字母次序不同等导致无法构成重写,但这种情况编译器不会报错,等发现运行结果不对再来矫正就太麻烦了。故C++11引入了override和final两个关键字,可以帮助用户检测是否重写。
- final:修饰虚函数,表示该虚函数不能再被重写。
// final用来修饰虚函数用处不大,设计虚函数本就是为了让子类重写实现多态
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; } //这里会报错
};
//final还可以用来修饰类,被修饰的类不能被继承
class A final
{
//……
};
class B : public A //这里会报错
{};
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
2.5重载、重写(覆盖)、隐藏(重定义)的对比
- 重载:(1)两个函数在同一作用域内。 (2)函数名相同,参数列表必须有区别。
- 隐藏:(1)两个函数分别在基类和派生类的作用域。 (2)函数名相同即可。
- 重写:(1)两个函数分别在基类和派生类的作用域。 (2)函数名、参数、返回值都必须相同(除开例外) (3)两个函数必须都是虚函数。
上面的关系中,隐藏的条件比重写简单,也就是说两个基类和派生类的同名函数不构成重写就构成隐藏。
//重载
//在同一作用域并且参数列表有区别,属于重载
void fun(int a)
{}
void fun(double a)
{}
//隐藏
//两个同名函数在基类和派生类作用域内,不构成重写就属于隐藏
class A
{
void fun(int a)
{}
};
class B : public A
{
void fun(int a)
{}
};
//重写
//两个同名函数在基类和派生类作用域内,并且都是虚函数
//函数名、参数列表、返回值都必须一致(除去例外),才能构成重写
class C
{
virtual void fun(int a)
{}
};
class D : public C
{
virtual void fun(int a)
{}
};
3.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
//抽象类的应用场景:比如图形就可以设计为抽象类
//三角形、正方形等继承了该抽象类后重写虚函数,就形成一个具体的类,可以实例化
class Graphics
{
public:
virtual double GetArea() = 0
{}//抽象类里面可以设计图形共有的接口,比如求面积等
};
class Square : public Graphics //正方形
{
public:
virtual double GetArea()
{
return side_length * side_length;
}
private:
int side_length = 5;
};
class rotundity : public Graphics //圆形
{
public:
virtual double GetArea()
{
return 3.14 * radius * radius;
}
private:
int radius = 5;
};
int main()
{
Square sq;
rotundity ro;
cout << "正方形面积:" << sq.GetArea() << endl;
cout << "圆形面积:" << ro.GetArea() << endl;
}
4.多态的原理
//sizeof(Base)是多少? --答案是8(我是32位程序),除了一个int还有一个指针变量
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
我们再看看这个指针指向的内容:
我就直接说了,这个指针指向的是一张表(指针数组),表里面存储的是虚函数的函数地址,这个表称为虚函数表(简称虚表),这个对象中的指针称为虚表指针。至于为什么不直接在对象中存函数地址,主要是是节省空间(所有对象可公用虚表)。为什么这样设计呢?我们接着往下看。
//编译器是在编译阶段就确定了调用是否满足多态
//(1)对于满足多态的调用,编译器通过找虚表指针,接着找到虚表中函数地址进行调用
// 多态调用也只是傻傻的执行指令而已
//(2)对于不构成多态的普通调用,编译器通过函数名和参数类型就可以确定调用那个函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p) //满足多态条件
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
//这里不构成多态,对象调用而不是指针或引用,按正常调用规则即可
Mike.BuyTicket();
return 0;
}
这里可以结合汇编代码来理解:
虚函数重写其实就是继承父类后,拿自己的虚表去覆盖父类的虚表,因此重写也叫覆盖。
5.单继承和多继承关系的虚函数表
PS:这一部分不是特别重要。
5.1单继承
有前面的分析,单继承只需要说一点即可:对于那些未重写的虚函数,也是存在虚表中的,至于存储在第一个位置还是最后,就看编译器的实现了。
5.2多继承
class A
{
public:
virtual void fun(int a)
{}
};
class B
{
public:
virtual void fun(int b)
{}
};
class C : public A, public B //继承A、B两个类
{
public:
virtual void fun(int c)
{}
};
int main()
{
C c;
A* p1 = new C;
B* p2 = new C;
p1->fun();
p2->fun();
delete p1, p1 = nullptr;
delete p2, p2 = nullptr;
return 0;
}
上面的代码中C继承了A、B,并重写了fun(),覆盖两个位置,因此有两个虚表,这也是为了考虑A、B中不同名的虚函数,因此没有合并为一个虚表。但我们发现这两个虚表指向的内容是不一样的!!!
这实际是一种封装,代码中p1和p2最终调用的其实是同一个函数,p2在调用到真正的函数前做了一件事情,那就是调整指针变量指向。
5.3菱形继承和多态
之前说过实际之中应该避免菱形继承,菱形虚拟继承加多态会让对象模型变得异常复杂,这里不细讲,感兴趣的可以看看下面文章:
- C++虚函数表解析
- C++ 对象的内存布局