C++多态

目录

多态的概念

多态的定义及实现

多态的构成条件

虚函数的重写

虚函数重写的两个例外

C++11 override和final

函数重载、覆盖(重写)、隐藏(重定义)的对比

抽象类

概念

接口继承和实现继承

多态的原理

虚函数表

动态绑定和静态绑定

继承和多态常见的面试问题


多态的概念

多态就是函数调用的多种形态,使用多态能使不同的对象去实现同一件事,产生不同的动作和结果

比如:买票事件,学生买票有折扣,普通人全价,军人买票优先

多态的定义及实现

多态的构成条件

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为

1.必须使用父类的指针或引用调用

2.被调用的函数必须是虚函数virtual修饰),且必须在子类中完成重写

//多态

class person
{
public:
    void virtual buy_trick()
    {
        cout << "普通人-全价\n";
    }
};

class student :public person
{
public:
    void virtual buy_trick()
    {
        cout << "学生-7.5折\n";
    }
};

void func(person& p)//或者指针
{
    p.buy_trick();
}

int main()
{
    person p;
    student s;
    func(p);
    func(s);//不同的对象调用同一函数产生不同的结果

}

虚函数的重写

虚函数的重写又叫虚函数的覆盖,若父类和子类(不同类域)中有一个完全相同的虚函数(返回值类型,函数名,参数列表都相同)此时我们称该子类的虚函数重写了父类的虚函数

class person
{
public:
    void virtual buy_trick()
    {
        cout << "普通人-全价\n";
    }
};

class student :public person
{
public:
    void virtual buy_trick()
    {
        cout << "学生-7.5折\n";
    }
};

注意: 在重写基类虚函数时,派生类的虚函数不加virtual关键字也可以构成重写,主要原因是因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性。但是这种写法不是很规范,因此建议在派生类的虚函数前也加上virtual关键字

虚函数重写的两个例外

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

//基类
class A
{};
//子类
class B : public A
{};
//基类
class Person
{
public:
    //返回基类A的指针
    virtual A* fun()
    {
        cout << "A* Person::f()" << endl;
        return new A;
    }
};
//子类
class Student : public Person
{
public:
    //返回子类B的指针
    virtual B* fun()
    {
        cout << "B* Student::f()" << endl;
        return new B;
    }
};

int main()
{
    Person p;
    Student s;
    Person* ptr = &p;//ptr为指向父类对象的指针
    Person* ptr1 = &s;//ptr1为指向子类对象的指针(两者的类型都为父类指针)
    ptr->fun();//调用父类的虚函数
    ptr1->fun();//调用子类的虚函数
}

基类Person当中的虚函数fun的返回值类型是基类A对象的指针,派生类Student当中的虚函数fun的返回值类型是派生类B对象的指针,也构成重写

2.析构函数的重写(基类与派生类析构函数的名字不同,也构成重写

class person
{
public:
    virtual ~person()
    {
        cout << "~person()\n";
    }
};
class student :public person
{
public:
    virtual ~student()
    {
        cout << "~student()\n";
    }
};

new一个父类对象和子类对象,并均用父类指针指向它们,分别用delete调用析构函数并释放对象空间

int main()
{
    person* p = new person;
    person* ps = new student;

    delete p;
    delete ps;
}

这里存在内存泄露的问题,如果子类对象中有需要释放的空间,子类没有重写析构函数,两个对象都只调用父类的析构,因此,为了避免出现这种情况,比较建议将父类的析构函数定义为虚函数(子类可重写析构函数,构成多态)

知识扩展:
在继承当中,子类的析构函数和父类的析构函数构成隐藏的原因就在这里,这里表面上看子类的析构函数和父类的析构函数的函数名不同,但是为了构成重写,编译后析构函数的名字会被统一处理成destructor();


C++11 override和final

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

final:修饰虚函数,表示该虚函数不能再被重写。

class person
{
public:
    void virtual buy_trick()final
    {
        cout << "普通人-全价\n";
    }
};

class student :public person
{
public:
    //编译报错,无法实现重写
    void virtual buy_trick()
    {
        cout << "学生-7.5折\n";
    }
};

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

class person
{
public:
    void virtual buy_trick()
    {
        cout << "普通人-全价\n";
    }
};

class student :public person 
{
public:
    //erro,编译报错,不构成重写,这里参数列表不同
    void virtual buy_trick(int i)override
    {    
        cout << "学生-7.5折\n";
    }
};

函数重载、覆盖(重写)、隐藏(重定义)的对比

重载:

在同一作用域中,函数名相同,参数列表不同(参数的顺序,个数,类型),与函数返回值无关

重写:

在不同作用域中,函数名相同,参数列表相同,返回值相同(协变除外),两者必须为虚函数

重定义:

在不同作用域中,函数名相同,子类和父类的同名函数不构成重写就是重定义(隐藏)

抽象类

概念

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象

//抽象类&接口类
class car
{
public:
    virtual void derive() = 0;//纯虚函数
};

int main()
{
    car c;//erro,抽象类不可实列化对象
}

 派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

//抽象类&接口类
class car
{
public:
    virtual void derive() = 0;//纯虚函数
};

class db:public car
{
public:
    //完成重写才能实列化对象
    virtual void derive()
    {}
};
int main()
{
    db b;
}

抽象类存在的意义是什么?

抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:人、动物等。

抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口

所以如果不实现多态,不要把函数定义成虚函数

多态的原理

虚函数表

下面是一道常考的笔试题:Base类实例化出对象的大小是多少?

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1;
};

如果只看成员变量,会认为是4,但答案是8

Base b;
cout << sizeof(b);

b对象当中除了_b成员外,实际上还有一个_vfptr放在对象的前面(vs版本下)

 对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

虚函数表中存什么?

下面Base类当中有三个成员函数,其中Func1和Func2是虚函数,Func3是普通成员函数,子类Derive当中仅对父类的Func1函数进行了重写

class Base
{
public:
    //虚函数
    virtual void Func1()
    {
        cout << "Base::Func1()" << endl;
    }
    //虚函数
    virtual void Func2()
    {
        cout << "Base::Func2()" << endl;
    }
    //普通成员函数
    void Func3()
    {
        cout << "Base::Func3()" << endl;
    }
private:
    int _b = 1;
};
//子类
class Derive : public Base
{
public:
    //重写虚函数Func1
    virtual void Func1()
    {
        cout << "Derive::Func1()" << endl;
    }
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

 Base b;
 Derive d;
父类对象b和基类对象d当中除了自己的成员变量之外,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表

虚表当中存储的就是虚函数的地址,虚函数表本质是一个存虚函数指针的指针数组

父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址

子类对象d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址(虚函数的重写又叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法)

子类虚函数表生成步骤

1.先将父类的虚表内容拷贝一份到自己的虚表中

2.检查虚函数是否被自己重写,若重写,用自己虚函数地址覆盖父类的那个地址

3.子类自己的虚函数,按声明顺序添加到虚表里面

虚表是什么阶段初始化的?虚函数存在哪里?虚表存在哪里?

虚表是在构造函数初始化列表完成初始化的,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中,对象中存的不是虚表而是指向虚表的指针,虚表实际上也是存在代码段的

更能理解多态的两个条件

1.必须是虚函数的重写,子类需要完成虚表中虚函数地址的覆盖

2.必须用父类的指针或者引用调用,不能使用父类对象(分析如下)

使用父类指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象的子类对象中切出来的那一部分,这样通过虚函数指针找到不同虚表,调用的函数就不一样。

如果使用父类对象来调,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象当中的虚表指针指向的都是父类对象的虚表,同类型对象共享一张虚表那么虚表指针指向同一份虚表,不能构成多态

总结一下:

构成多态,指向谁就调用谁的虚函数,跟对象有关

不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关

动态绑定和静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载

动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

继承和多态常见的面试问题

1.以下关于纯虚函数的说法,正确的是()

A.声明纯虚函数的类不能实例化对象
B.声明纯虚函数的类是虚基类
C.子类必须实现基类的纯虚函数
D.纯虚函数必须是空函数

声明纯虚函数的类是抽象类(接口类)

2.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则()

A.A类对象的前4个字节存储虚表地址,B类对象的前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚基表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

B:erro,是虚表地址,不是虚基表地址

D:个数相同,内容不同,重写了就覆盖自己的地址过去

3、以下程序输出结果是什么?

#include <iostream>
using namespace std;
class A
{
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    virtual void test()
    {
        func();
    }
};
class B : public A
{
public:
    void func(int val = 0)
    {
        cout << "B->" << val << endl;
    }
};
int main()
{
    B* p = new B;
    p->test();
    return 0;
}
 

A.A->0 B.B->1 C.A->1 D.B->0
E.编译错误 F.以上都不正确

虚函数重写,重写的是函数实现,函数名,参数都是父类的

1:A   2:D  3:B

问答题

1、什么是多态?

多态是指不同继承关系的类对象,去调用同一函数,产生不同行为。多态又有静态多态和动态多态之分。

2、什么是重载、重写(覆盖)、重定义(隐藏)?

重载是同一作用域中,函数名相同,参数列表不同

重写是不同作用域中(基类,派生类),是虚函数,函数名相同,参数列表相同,且返回值相同(协变除外)

重定义也是不同作用域中(基类,派生类),函数名相同,但是不构成重写,就是重定义

3、多态的实现原理?

构成多态的基类对象和子类对象中都存有一个虚表指针,该虚表指针指向虚表虚表里面存放的是该类的虚函数地址。所以,当基类指针指向基类对象时,通过基类指针找到虚表指针,然后在虚表中找到的就是基类的虚函数地址,当基类指针指向子类对象时,同理,该基类指针找到虚表指针,然后虚表中找到的就是子类的虚函数地址

4、inline函数可以是虚函数吗?

可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数的地址要放到虚表中去,而内联函数是在调用的地方展开的,内联函数是没有地址的
5、静态成员函数可以是虚函数吗?
不可以,静态成员函数不属于任何一个对象,是没有this指针的,无法放进虚函数表中
而对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址
6、构造函数可以是虚函数吗?
不可以,对象中的虚表指针是在构造函数初始化列表阶段初始化的
7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,此时需要把父类的析构函数定义成虚函数,若我们new一个父类和子类对象,并用父类的指针指向它们,当delete调用析构函数并释放对象空间时,只有父类对象是虚函数,才能分别调用父类和子类的析构函数释放资源,否则使用父类指针delete对象,只会调用父类的析构函数,子类资源得不到释放
8、对象访问普通函数快还是虚函数更快? 
普通函数更快,普通函数可以直接访问,而虚函数需要先找到虚表指针,在通过虚表指针在虚表中找到虚函数地址,才能调用虚函数
9、虚函数表是在什么阶段生成的?存在哪的?
在构造函数初始化列表阶段生成,存在代码段(常量区)
10、C++菱形继承的问题?虚继承的原理?
菱形继承会导致子类对象中有两份父类的成员,导致二义性和代码冗余问题
虚继承对于相同的虚基类在对象中只会存储一份,若要访问虚基类成员,通过虚基表计算偏移量,找到对应的虚基类成员,解决了菱形继承的问题
11、什么是抽象类?抽象类的作用?
包含纯虚函数的类是抽象类
抽象类更好体现了虚函数的继承是一种接口继承,强制子类去实现纯虚函数,如果不实现,子类也是抽象类,抽象类是不能实列化对象的,抽象类也能体现世界上不能实列化抽象类型,比如动植物
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值