目录

1. 多态的简介

 编译时多态(静态多态)

运行时多态(动态多态)

2. 多态的实现条件

(1)必须通过基类指针或引用调用虚函数

(2)被调用的函数必须是虚函数,并且派生类完成重写

3. 多态经典面试题

 4. 析构函数建议写成虚函数

5. override 与 final

override

final

6. 纯虚函数和抽象类

抽象类特点

(1)不能实例化对象

(2)派生类必须重写纯虚函数

7. 虚函数表(虚表)

对象大小问题

多态是如何实现的

动态绑定与静态绑定

静态绑定

动态绑定

虚函数表特点


1. 多态的简介

多态(Polymorphism)指的是“同一种行为,在不同对象下产生不同结果”。

C++中的多态主要分为:

  • 编译时多态(静态多态)

  • 运行时多态(动态多态)


 编译时多态(静态多态)

编译阶段就已经确定调用哪个函数。

常见方式:

  • 函数重载

  • 函数模板

例如:

#include <iostream>
using namespace std;

void Func(int x)
{
    cout << "int" << endl;
}

void Func(double x)
{
    cout << "double" << endl;
}

int main()
{
    Func(10);
    Func(3.14);

    return 0;
}

运行结果:

int
double

因为参数类型不同,编译器在编译阶段就确定了调用哪个函数。


运行时多态(动态多态)

运行时多态指:

同一个函数调用,在程序运行过程中,根据对象不同,调用不同函数。

例如:

  • 普通人买票 → 全价

  • 学生买票 → 打折

  • 军人买票 → 优先


2. 多态的实现条件

C++实现运行时多态必须满足两个条件:

(1)必须通过基类指针或引用调用虚函数

      成员函数前加 virtual 就是虚函数。

     只有成员函数才能是虚函数

     普通函数不能加 virtual


(2)被调用的函数必须是虚函数,并且派生类完成重写

虚函数重写(覆盖)

派生类中:函数名相同,参数相同,返回值相同

则构成重写(Override)

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

void Func(Person& ptr)
{
	// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	// 但是跟ptr没关系,而是由ptr指向的对象决定的。
	ptr.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);

	return 0;
}

调用父类的原因是:继承中规定,子类可以赋值给父类(切割),这样,不管传父类还是子类,都可以实现,指针指向谁,就调用谁的函数

运行结果:

买票-全价
买票-打折

如果把父类的virtual去掉,那么都不管传父类还是子类,都会调用父类的函数,因为此时不构成重写虚函数

如果不传指针和引用,也会都调用父类函数

运行结果:

买票-全价
买票-全价

在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承
后父类的虚函数被继承下来了在子类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态

 


3. 多态经典面试题

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;
}

输出:

B->1

原因:

func() 是虚函数 ,首先b在自己类中找test,如果有,就隐藏A中test,如果没有,就去A中找test,A中test的this指针是A* ,所以这里参数是父类指针或引用,就构成了多态,这里是B对象调用func,所以调用B中函数,但是出题人很恶心,你如果答案是B->0,那你就被出题人秒杀了,重写的规定是重写函数题,函数声明不会重写,所以缺省值还是1,所以答案是B->1


 4. 析构函数建议写成虚函数

看代码:

class A
{
public:
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A
{
public:
    ~B()
    {
        cout << "~B()" << endl;
        delete[] _p;
    }

private:
    int* _p = new int[10];
};

int main()
{
    A* p = new B;

    delete p;

    return 0;
}

如果基类析构函数不是虚函数,那么这里就调用A析构,因为虽然new的是B,但是不构成多态

则:

delete p;

只会调用:

~A()

不会调用:

~B()

导致:资源泄漏,内存泄漏

所以:

基类析构函数一般都建议写成虚函数。

在这种场景下,为了构成多态,所以编译器会被析构函数最后都处理成destructor,这里就解释了为什么在继承部分,析构构成了隐藏关系

5. override 与 final

override

用于检查是否真正完成重写。

class Car
{
public:
    virtual void Drive()
    {
    }
};

class Benz : public Car
{
public:
    virtual void Drive() override
    {
        cout << "Benz" << endl;
    }
};

如果函数名写错:

Dirve()

编译器会直接报错。


final

禁止派生类继续重写。

class Car
{
public:
    virtual void Drive() final
    {
    }
};

此时子类不能再重写 Drive。


6. 纯虚函数和抽象类

纯虚函数格式:在虚函数后面加 =0

virtual 函数 = 0;

例如:

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

这种函数叫:纯虚函数

包含纯虚函数的类叫抽象类


抽象类特点

(1)不能实例化对象

错误:

Car c;

(2)派生类必须重写纯虚函数

否则派生类仍然是抽象类。


7. 虚函数表(虚表)

含有虚函数的类中,

对象内部会存在一个:

vfptr

即:虚函数表指针


对象大小问题

class Base
{
public:
    virtual void Func1()
    {
    }

private:
    int _a;
    char _b;
};

32位下:

sizeof(Base)

结果:

12

原因:

成员 大小
int 4
char 1
对齐 3
虚表指针 4

总计:

12 bytes

多态是如何实现的

满足多态条件后:

ptr->BuyTicket();

不再是编译时确定函数地址。

而是运行时去对象中的虚表寻找真正函数地址。


动态绑定与静态绑定

静态绑定

编译阶段确定函数地址。

例如:普通函数,非虚函数


动态绑定

运行时通过虚表确定函数地址。

满足条件:基类指针/引用,调用虚函数


虚函数表特点

(1)每个有虚函数的类都有虚表

(2)同类型对象共用同一张虚表

(3)派生类会覆盖虚表中的虚函数地址

例如:

Base::func1

会被:

Derive::func1

覆盖。

更多推荐