c++之多态

1.概念

多态分为编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。示例代码如下所示:

1)函数重载

复制代码
void print(int x) {
    std::cout << "Integer: " << x << std::endl;
}

void print(double x) {
    std::cout << "Double: " << x << std::endl;
}

void print(const std::string& x) {
    std::cout << "String: " << x << std::endl;
}

2)函数模板

复制代码
template <typename T>
void print(T x) {
    std::cout << "Value: " << x << std::endl;
}

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。也就是说,是在程序运行时才确定函数调用的具体实现形式,主要通过虚函数和继承机制实现。

2.虚函数

虚函数是实现运行时多态的关键。通过在基类中用virtual声明虚函数,派生类可以重写这些函数。调用虚函数时,程序会根据对象的实际类型动态选择调用的函数版本。

2.1 虚函数的重写/覆盖:

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

复制代码
class Base {
public:
    virtual void show() {
        std::cout << "Base::show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {  // 重写虚函数
        std::cout << "Derived::show" << std::endl;
    }
};

在运行时,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用对应的函数版本:

复制代码
    Base* ptr = new Derived();
    Base* ptr1 = new Base();
    ptr->show();  // 输出 "Derived::show"
    ptr1->show();//输出"Base::show"

2.2 析构函数的重写

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

复制代码
class A
{
	public :
	virtual ~A()
	{
		std::cout << "~A()" << std::endl;
	}
};
class B : public A {
public:
	~B()
	{
		std::cout << "~B()->delete:" << _p << std::endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
}

这是运行结果

有些人可能会好奇为什么输出结果是这样的,这里我解释一下:第一句delete p1 执行后,调用A的析构函数,输出~A(),之后执行delete p2 命令,先调用B的析构函数,输出

~B()->delete:0000027202410FB0,之后由于B继承了A,所以还要调用A的析构函数,输出~A()

3.override关键字

作用:1)明确告诉编译器,当前函数是用于覆盖基类中的虚函数。

2)避免错误:

如果没有使用 override,编译器不会检查当前函数是否正确覆盖了基类的虚函数。如果基类中的虚函数签名发生变化(例如参数类型或数量改变),派生类中的函数可能无法正确覆盖,但编译器不会报错。这种情况下,可能会导致运行时错误或逻辑问题。而使用 override 后,如果派生类中的函数无法正确覆盖基类的虚函数,编译器会报错,从而避免潜在的错误。例如

复制代码
class Base {
public:
    virtual void display(int x) {  // 基类虚函数的参数改变了
        std::cout << "Base::display" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {  // 编译错误,无法覆盖基类的虚函数
        std::cout << "Derived::display" << std::endl;
    }
};

在这种情况下,编译器会报错,提示 display 函数无法覆盖基类中的虚函数,从而避免了潜在的错误。

4.final关键字

作用:如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

例如:

复制代码
// error C3248 : "Car::Drive" : 声明为"final"的函数⽆法被"Benz::Drive"重写
class Car
{
	public :
	virtual void Drive() final {}
};
class Benz :public Car
{
	public :
	virtual void Drive() { std::cout << "Benz-舒适" << std::endl; }
};

5.纯虚函数和抽象类

如果希望在基类中声明一个虚函数,但不提供具体实现,可以将其定义为纯虚函数(在后面加=0)。包含纯虚函数的类称为抽象类,不能直接实例化。例如:

复制代码
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle" << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Square" << std::endl;
    }
};

通过抽象类的指针或引用,可以统一调用派生类的实现:

复制代码
Shape* shape1 = new Circle();
Shape* shape2 = new Square();

shape1->draw();  // 输出 "Drawing a Circle"
shape2->draw();  // 输出 "Drawing a Square"

6.小结

综上所述,实现多态的两个必须重要条件是

1)是基类的指针或者引用调用虚函数

2)被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

7.原理

1)虚函数表(V-Table):编译器为每个包含虚函数的类生成一个虚函数表(V-Table),表中存储了类中所有虚函数的地址。

2)对象的V-Pointer:每个对象都包含一个指向其类的V-Table的指针(V-Pointer)。当通过基类指针或引用调用虚函数时,程序会通过对象的V-Pointer找到对应的V-Table,再通过V-Table找到实际的函数地址

相关推荐
又熟了34 分钟前
WDG看门狗(独立看门狗和窗口看门狗)
c语言·stm32·单片机·嵌入式硬件
老歌老听老掉牙35 分钟前
Open CASCADE学习|实现裁剪操作
c++·学习·opencascade·裁剪
姜行运36 分钟前
数据结构【二叉搜索树(BST)】
android·数据结构·c++·c#
TPBoreas4 小时前
Jenkins 改完端口号启动不起来了
java·开发语言
TE-茶叶蛋4 小时前
Vuerouter 的底层实现原理
开发语言·javascript·ecmascript
柒柒的代码学习日记5 小时前
数组和指针典型例题合集(一维数组、字符数组、二维数组)
c语言
云闲不收5 小时前
设计模式原则
开发语言
秋名RG5 小时前
深入解析建造者模式(Builder Pattern)——以Java实现复杂对象构建的艺术
java·开发语言·建造者模式
技术求索者5 小时前
c++学习
开发语言·c++·学习
山猪打不过家猪6 小时前
(二)毛子整洁架构(CQRS/Dapper/领域事件处理器/垂直切片)
开发语言·.net