硬核两万字带你理解C++之多态

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核两万字带你理解C++之多态相关的知识,希望对你有一定的参考价值。

多态

文章目录


多态: 多态就是函数调用的多种形态,调用函数更加灵活,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

话不多说,我们首先来看这样的一段程序,让大家感知一下多态是什么意思,这是一段不构成多态的程序:

class Person

public:
    void BuyTicket()
    
        cout<<"正常排队 - 全价买票"<<endl;
    
protected:
    int _age;
    string _name;

class Student : public Person

public:
    void BuyTicket()
    
        cout<<"正常排队 - 半价买票"<<endl;
    
protected:
	//...

class Soldier : public Person

public:
    void BuyTicket()
    
        cout<<"优先排队 - 全价买票"<<endl;
    
protected:
	//...

void Func(Person* ptr)

    ptr->BuyTicket();

int main()

    Person ps;
    Student st;
    Soldier sd;
    
    Func(&ps);
    Func(&st);
    Func(&sd);
    
    return 0;

可以看到此时并没有构成多态,都打印的是父类的函数,因为形成多态具有两个条件

1、子类重写父类的虚函数

2、必须是父类的指针或者引用去调用虚函数

这两个条件什么意思,我们下面讲解,我们再来看看构成多态的程序:

class Person

public:
    virtual void BuyTicket()
    
        cout<<"正常排队 - 全价买票"<<endl;
    
protected:
    int _age;
    string _name;

class Student : public Person

public:
    virtual void BuyTicket()
    
        cout<<"正常排队 - 半价买票"<<endl;
    
protected:
	//...

class Soldier : public Person

public:
    virtual void BuyTicket()
    
        cout<<"优先排队 - 全价买票"<<endl;
    
protected:
	//...

void Func(Person* ptr)

    //ptr指向父类对象调用父类的虚函数,指向子类对象调用子类虚函数
    ptr->BuyTicket();

int main()

    Person ps;
    Student st;
    Soldier sd;
    
    Func(&ps);
    Func(&st);
    Func(&sd);
    
    return 0;

此时满足了多态条件,看到了指针指向的对象是什么类就调用哪个类的虚函数:

那么什么是虚函数呢?多态的原理是什么呢?上面为什么就满足了多态的条件呢?等等问题我们下面开始深入理解多态:

有些书籍会把多态进行更细划分:

  • 静态的多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为/形态

  • 动态的多态:父类指针或引用调用重写虚函数,不同的对象去调用,就有不同的行为/行为,父类指针或者引用指向父类,调用的就是父类的虚函数,父类指针或者引用指向哪个子类,调用的就是子类的虚函数

int main()

    int i;
    char ch;
    cin >> i;
    cin >> ch;
    cout << i << endl;
    cout << ch << endl;
    return 0;

这里其实就是静态的多态,看起来我们用的是一个函数,但是实际不是的,这个底层就是多态实现的:operator>>(int i); operator>>(char i);

int main()

    int i = 0,j = 1;
    double d = 1.1, e = 2.2;
    swap(i,j);
    swap(d,e);
    return 0;

多态就是函数调用的多种形态,以上都是静态的多态,这里的静态是指编译时确定的,在编译阶段通过函数修饰规则去找函数。动态的多态:不同的类型对象去完成同一件事情,产生的动作是不一样的

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。
Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

  • 必须通过基类的指针或者引用调用虚函数

  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person

public:
    virtual void BuyTicket()
    
        cout<<"买票-全价"<<endl;
    
;

注意:

  • 只有类的非静态成员函数才可以加virtual
  • 虚函数这里virtual和虚继承中用的virtual是同一个关键字,但是他们之间没有关系,这里的虚函数是为了实现多态,虚继承是为了解决菱形继承的数据冗余和二义性

构成多态的条件之一是虚函数重写:

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

class Person

public:
    virtual void BuyTicket()
    
        cout<<"买票-全价"<<endl;
    
;
class Student : public Person

public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    
        cout<<"买票-半价"<<endl;
    
;
class Soldier : public Person

public:
    //子类的虚函数重写了父类的虚函数
    virtual void BuyTicket()
    
        cout<<"买票-优先"<<endl;
    
;
void f(Person& p)

    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();

void f(Person* p)

    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p->BuyTicket();

int main()

    Person p;//普通人
    Student st;//学生
    Soldier so;//军人
    
    f(p);
    f(st);
    f(so);
    
    f(&p);
    f(&st);
    f(&so);
    return 0;

注意多态需要满足的条件:

  • 必须通过基类的指针或者引用调用虚函数

  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

正常的虚函数重写,要求虚函数的函数名、参数、返回值都要相同,但是协变例外

虚函数重写的两个例外

  • 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A;
class B : public A ;
class Person 

public:
	virtual A* f() 
    
        cout<<"A* Person::f()"<<endl;
        return new A;
    
;
class Student : public Person 

public:
	virtual B* f() 
    
        cout<<"B* Student::f()"<<endl;
        return new B;
    
;
int main()

    Person p;
    Student s;
    
    Person* ptr = &p;
    ptr->f();
    
    ptr = &s;
    ptr->f();
    return 0;

  • 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person

public:
	~Person() cout << "~Person()" << endl;
;
class Student : public Person 

public:
	~Student()  cout << "~Student()" << endl; 
;
int main()

    Person p
    Student s;
 
    return 0;

不构成重写:

先析构子类的对象,调用子类的析构函数,在子类的析构函数结束时自动的调用了父类的析构函数,最后调用父类的析构函数析构父类

构成重写:

在普通场景下,父子类的析构函数是否构成重写不重要,没什么影响,那么看下面的场景(new对象特殊场景):

class Person

public:
    //建议把父类的析构函数定义成虚函数
    //这样子类的虚函数方便重写父类的虚函数
	~Person() cout << "~Person()" << endl;
;
class Student : public Person 

public:
    //Student和Person析构函数名看起来不相同,但是他们构成虚函数重写
	~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;

不构成重写:

为什么会这样呢?

delete p1;//p1->destuctor()+operator delete(p1)
delete p2;//p2->destuctor()+operator delete(p2)

因为delete时底层会去调用该对象类的析构函数和operator delete,不是虚函数时,他们构成隐藏,因为p1和p2都是父类指针,所以他们都是去调用父类的析构函数

构成重写时:

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

注意

当子类的虚函数重写了父类的虚函数,子类虚函数不写virtual关键字也认为它是虚函数,完成了重写:

class Person

public:
    virtual void BuyTicket()
    
        cout<<"买票-全价"<<endl;
    
;
class Student : public Person

public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket()
    
        cout<<"买票-半价"<<endl;
    
;
void f(Person& p)

    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();


int main()

    Person p;//普通人
    Student st;//学生
    
    f(p);
    f(st);
    return 0;

但是父类重写的函数不加virtual就不行,因为子类是先继承父类的虚函数,继承下来以后有virtual属性了,子类只是重写这个virtual函数,严格来说这个也算是C++语言设计的坑

针对虚函数重写,给一个建议:尽量不要写协变,尽量严格按重写要求来,这样代码更容易进行维护

C++11 override和final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序
写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

如果不想虚函数被重写,那么就在虚函数后面加关键字final:

class Person

public:
    virtual void BuyTicket() final
    
        cout<<"买票-全价"<<endl;
    
;
class Student : public Person

public:
    //子类的虚函数重写了父类的虚函数
    void BuyTicket()
    
        cout<<"买票-半价"<<endl;
    
;
void f(Person& p)

    //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
    p.BuyTicket();


int main()

    Person p;//普通人
    Student st;//学生
    
    f(p);
    f(st);
    return 0;

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Person

public:
    virtual void BuyTicket() 
    
        cout << "买票-全价" << endl;
    
;
class Student : public Person

public:

    void BuyTicket(int i) override
    
        cout << "买票-半价" << endl;
    
;
void f(Person& p)

    p.BuyTicket();


int main()

    Person p;//普通人
    Student st;//学生

    f(p);
    f(st);
    return 0;

如何设计出一个不能被继承的类?将该类的成员的访问限定符设置为私有:

class A

private:
    A()
    

class B:public A

public:
    B()
    

int main()

    B b;
    return 0;

这个是可以被继承,但是父类的成员子类不可见,子类定义对象不能使用父类的成员。

在C++11中支持了final这个关键字:

class A final
;
class B:public A
;
int main()

    return 0;

final修饰一个类,这个类不能被继承,不管定不定义对象直接报错。

重载、覆盖(重写)、隐藏的对比

总结

多态:调用一个函数时,展现出多种形态(通过调用不同的函数,完成不同的行为)。多态分为静态的多态和动态的多态:比如函数重载就是静态的多态,在编译时确定地址。动态的多态:条件:1、子类继承父类,完成虚函数的重写 2、父类的指针或者引用去调用这个重写的虚函数。

父类的指针或者引用指向父类对象,调用的是父类的虚函数

父类的指针或者引用指向子类对象,调用的是子类的虚函数

虚函数的重写条件:1、要是虚函数 2、函数名、参数、返回值都相同

例外:

1、协变(返回值不一样,父类的虚函数返回的是基类对象指针和引用,子类的虚函数返回的是子类对象指针和引用)

2、析构函数

3、子类中的重写的虚函数可以不加virtual关键字(建议加上)

抽象类

概念

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

//抽象类
class Car

public:
    virtual void Drive() = 0;
;
int main()

    Car c;//不能实例化出对象
    return 0;

抽象类不能实例化出对象,可以更好的去表示现实世界中没有实例对象对应的抽象类型,比如:植物、人、动物,它体现了接口继承,强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。

//抽象类
class Car

public:
    virtual void Drive() = 0;
;
class Benz : public Car

public:
    virtual void Drive()
    
        cout<<"Benz-舒适"<<endl;
    
;
int main()

    //Car c;//不能实例化出对象
    Car* pBenz = new Benz;
    pBenz->Drive();
    return 0;

要注意和override区分,override检查子类虚函数是否完成重写。纯虚函数是强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

虚函数表

这里常考一道笔试题:sizeof(Base)是多少?

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base

public:
    virtual void Func1()
    
        cout << "Func1()" << endl;
    
private:
	int _b = 1;
    char _ch = 'a';
;
int main()

    cout << sizeof(Base) << endl;
    return 0;

为什么是12呢?根据内存对齐应该是8呀,这里为什么会是12。

是因为只要包含虚函数的类,该类的对象就包含一个虚函数表指针(简称虚表指针),这个虚函数表指针就是用来实现多态的:

这个虚表指针指向一个数组,这个数组的元素是函数指针,这里面的函数指针指向该类中的虚函数

虚函数被编译成指令后,还是和普通函数一样,存在代码段,只是它的地址放在虚表中

需要注意的是:

里跟虚继承那里是不一样的,他们虽然都用了virtual关键字,但是他们的使用场景完全不一样,解决的也是不一样的问题,他们之间没有关联,虚继承产生的是虚基表,虚基表里面存的是距离虚基类的偏移量

class Person 

public:
	virtual void BuyTicket()  cout << "买票-全价" << endl; 
     virtual void Func1()
     
         cout<<"Person::Func1()"<<endl;
     
;
class Student : public Person 

public:
	virtual void 狂肝两万字带你用pytorch搞深度学习!!!

Web 三件套两万字带你入门 CSS

Web 三件套两万字带你入门 CSS

两万字带你认识黑客在kali中使用的工具

硬核干货!7600字带你学会 Redis 性能优化点, 建议收藏!

C++多态