10.【C++进阶】多态
本博客梳理C++面向对象三大特性的多态特性
一、多态的概念
- 多态就是多种形态,分为编译时多态(静态多态)和运行时多态(动态多态)
- 编译时多态:函数重载和函数模板,在编译时就完成了实参传给形参的参数配置
- 运行时多态:运行时调用函数产生的多种形态
二、多态的定义与实现
1.多态的构成条件
(1)必须基类的指针或者引用调用虚函数(这样才能传不同类型的对象)
(2)被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖
2.虚函数
类的成员函数前面加virtual修饰,则这个成员函数被称为虚函数
3.虚函数的重写/覆盖
(1)重写/覆盖:派生类中有一个跟基类完全相同的虚函数(两者返回值类型、函数名、参数列表(只看参数类型,不看缺省值)完全相同【三同】),称派生类的虚函数重写了基类的虚函数
(2)重写基类虚函数时,派生类虚函数不加virtual也构成重写
(3)派生类虚函数即使私有,通过基类指针也能在多态下进行调用——因为:重写写的是虚函数的实现,声明用的是基类的,基类声明公有就能调用了
class Person
{
public:
virtual void BuyTicket()
{ cout << "买票-全价" << endl; }
};
class Student : public Person
{
private:
//virtual void BuyTicket()
void BuyTicket()
{ cout << "买票-打折" << endl; }
};
void Func(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
//st.BuyTicket();//此函数要在Student的BuyTicket函数公有时才能调
return 0;
}

4.重写/覆盖的相关问题
(1)协变
派生类重写基类虚函数时,基类虚函数返回基类对象的指针or引用,派生类虚函数返回派生类对象的指针or引用,称为协变
(2)析构函数的重写
- 编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成desturctor
- 因此基类析构函数加了virtual,派生类析构函数就构成重写
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派生类的析构函数重写了基类的析构函数,下⾯的delete对象调用析构函数,才能
// 构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}

(3)override和final关键字
- C++11提供override【推翻】关键字,可以帮助检测是否重写;若不想让派生类重写这个虚函数,可以用final修饰

5.重载、重写、隐藏对比

三、纯虚函数和抽象类
- 在虚函数后面加上=0,则函数为纯虚函数,包含纯虚函数的类叫抽象类
- 纯虚函数不需要定义实现,只要声明(语法上可以定义实现,但没意义,因为要被派生类重写)
- 抽象类不能实例化出对象,因此派生类不重写纯虚函数也是抽象类,纯虚函数某种程度上强制了派生类进行重写,不重写就没法实例化对象
class A
{
public:
virtual void func() = 0;
};
class B : public A {
public:
virtual void func()//派生类要重写纯虚函数,否则派生类也是抽象类
{
cout << "B::func()" << endl;
}
};
int main()
{
//error C2259:“A”: 无法实例化抽象类
A a;
A* pB = new B;
pB->func();//抽象类不影响多态使用
return 0;
}
四、多态的原理
1.静态多态(函数重载)的原理
在Linux下查看两个函数的底层调用,可以看出编译器在区分两个不同的函数时,添加了i和d作为后缀,以示区分,i对应int,d对应double
2.动态多态的原理
(1)底层原理
ptr在调用BuyTicket函数时,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向对象的虚函数表中确定对应虚函数的地址,也解释了基类指针/引用既能调用基类虚函数,又能调用派生类虚函数的现象
(2)动态绑定与静态绑定
①静态绑定:不满足多态条件的函数调用在编译时绑定,编译时就确定函数地址
②动态绑定:满足多态条件的函数调用在运行时绑定,运行时到指向对象的虚函数表中找到调用函数的地址
(3)虚函数表
①派生类先把基类的虚表拷贝下来,重写的虚函数地址完成覆盖,然后加上自己的虚函数地址,不会自己在生成虚表指针
②同类型的对象共用一张虚表,不同类型的对象有各自的虚表
③派生类的虚表中包括:a.基类的虚函数地址;b.派生类重写的虚函数地址;c.派生类自己的虚函数地址
④虚表本质是一个存虚函数指针的指针数组
⑤虚函数存在代码段,虚表在VS下存在代码段(常量区)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
// 多态也会发生在多个派生类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}

更多推荐

所有评论(0)