C++修炼:多态

Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客: <但凡.

我的专栏: 《编程之路》《数据结构与算法之美》《题海拾贝》《C++修炼之路》

目录

1、多态

1.1、多态引入

1.2、虚函数

1.3、协变

1.4、析构函数的重写

1.5、override和final

1.6、重写、重写、隐藏的对比

2、纯虚函数和抽象类

3、多态的原理

3.1、虚函数表指针

3.2、多态是如何实现的

3.3、虚函数表


C++三大特性:封装,继承,多态。今天我们来看第三个,多态。

1、多态

1.1、多态引入

什么是多态?简单来讲就是多种形态。 我们调用一个函数,但是产生了不同的结果,这就是多态。多态分为静态多态和动态多态。静态多态是编译时多态,我们之前的函数重载和函数模板都是静态多态。而今天我们主要来看动态多态,也叫运行时多态(以下简称多态)。

多态必须通过继承来实现,并且必须有两个重要条件(前提):

1、必须是基类的指针或者引用调用虚函数。

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

下面我们来开一个具体的使用场景:

cpp 复制代码
#include <iostream>
using namespace std;

// 基类 Shape
class Shape {
public:
    // 虚函数 - 用于实现多态
    virtual void draw(){
        cout << "Drawing a generic shape" << endl;
    }
};

// 派生类 Circle
class Circle : public Shape {
public:
    virtual void draw(){  // 重写基类虚函数
        cout << "Drawing a circle" << endl;
    }
};

// 派生类 Square
class Square : public Shape {
public:
    virtual void draw(){  // 重写基类虚函数
        cout << "Drawing a square" << endl;
    }
};
int main() {
    // 创建不同形状的对象
    Circle circle;
    Square square;

    Shape& c = circle;
    Shape& s= square;

    c.draw();
    s.draw();
    return 0;
}

以上代码可以体现出动态多态。我们在调用c.draw的时候,实际上走的不是基类的draw,而是派生类circle的draw,调用s.draw同理。为什么会调到这两个函数呢?因为我们满足多态的两个必要条件,就构成了多态。

首先基类和派生类的draw都进行了虚函数重写(都加了virtual关键字),并且我们访问函数的时候使用基类的引用访问的。 注意在这千万不要想着基类引用这不是切片吗?切片不应该调用派生类中基类那一部分吗?注意我们进行了虚函数重写,那么和之前普通函数走的就不是一个逻辑了。

在这里如果只有基类的draw函数加了virtual关键字,但是派生类没有加这个关键字,实际上也构成虚函数重写。但是这样写是不规范的,所以说我们尽量都加上virtual关键字。

1.2、虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

虚函数重写/覆盖的条件是:派生类中必须有根基类完全相同的虚函数(即派生类虚函数类型,返回值,函数名字,参数列表完全相同),我们就称派生类重写了基类的虚函数。

现在我们看一道多态场景的选择题:

以下程序输出结果是什么()

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

cpp 复制代码
class A
 {
 public:
     virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
     virtual void test(){ func();}
 };
 
 class B : public A
 {
 public:
     void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
 };
 
 int main(int argc ,char* argv[])
 {
    B*p = new B;
    p->test();
    return 0;
 }

老规矩先说结果,这道题选B。这道题其实还是有点复杂的,但是没关系我们一点一点来分析。

**首先这两个类中的虚函数构成重写。**因为我上面提到过就算派生类中的虚函数不写virtual关键字也是构成重写的。并且虚函数的重写并没有说参数的缺省值一定要相同。

然后,我们p调用test函数,test函数要通过this指针调用func函数, 问题就来了,调用的是哪个func函数?我们明确一点,B继承A并不是说A中的成员拷贝到了B中。所以说我们访问B中A的那一部分其实并不是用B*的this指针,而是A*的this指针。如果是A*this指针,但是这个指针指向的不是一个A类对象啊,而是一个B类指针。又因为这里的两个虚函数重写,涉及到多态而不是切片,满足多态的两个必要条件,所以调用的是B中的func。

接下来我们想为什么B中的func选项不选D而选B呢?这里我也不卖关子了,因为这就是语法固定想也想不到。**在多态中我们构成重写的两个虚函数,实际上是用的基类虚函数的声明,加上派生类虚函数的定义。**而我们val的值是跟着声明走的,所以说打印出来val的值应该是声明的值,也就是1.

1.3、协变

这是一种特殊情况,当我们派生类的虚函数返回值为基类对象的指针或引用,而派生类返回派生类对象的指针或引用(其他参数相同),此时两虚函数也构成重写,我们称之为协变。

两个返回值不一定是该基类或者该派生类的指针或引用,也可以是其他两个继承关系的基类和派生类的指针或引用。

cpp 复制代码
#include <iostream>
using namespace std;

class A {};
class B : public A {};
class Person {
public:
    virtual A* BuyTicket()
    {
        cout << "买票-全价" << endl;
        return nullptr;
    }
};
class Student : public Person {
public:
    virtual B* BuyTicket()
    {
        cout << "买票-打折" << endl;
        return nullptr;
    }
};
void Func(Person* ptr)
{
    ptr->BuyTicket();
}

int main()
{
    Person ps;
    Student st;
    Func(&ps);
    Func(&st);
}

1.4、析构函数的重写

还记得上一篇我们说过,**析构函数在编译时底层都会把他处理成destructor,所以说这两个析构实际上是隐藏关系的。**当然隐藏是一个坑,但其实也没事,因为隐藏是基类的析构函数对于派生类对象隐藏了,我们派生类对象调不到父类的析构函数,不是特殊场景我们派生类对象也不会调到父类的析构函数。

但是下面这个情景是经常出现的,并且在面试题中也经常出现:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

现在以上情况是会造成内存泄露的问题的。 我们来分析一下,首先p1正常释放,没什么好说的,p2就不能正常释放了。因为p2是派生类B中基类A的那一部分的切片,他其实是调用不到B的析构的。

那怎么解决呢**?我们只需要在基类的析构函数前加上virtual关键字,让这两个关键字构成重写。这样的话我们就能通过多态解决这个问题。**

修改后:

cpp 复制代码
#include<iostream>
using namespace std;
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];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

1.5、override和final

这两个关键字是C++11新增的。 override可以显式标记派生类中重写的虚函数,帮助编译器检查是否正确地重写了基类的虚函数。并且也可以提高可读性,让别人看代码的时候一眼就知道这是个重写基类的虚函数。

**final可以标记这个虚函数不要被重写。**其中override标记在派生类中,final标记在基类中。

cpp 复制代码
class Base {
public:
    virtual void foo() final;  // 此函数不能被子类重写
};

class Derived : public Base {
public:
     virtual void foo() override; //报错:无法重写final函数
};

void Base::foo()
{
    cout << 1 << endl;
}
void Derived::foo()
{
    cout << 2 << endl;
}

1.6、重写、重写、隐藏的对比

**重载必须要求两个函数在同一个作用域。这是和重写,隐藏根本上就不同的。**重载要求函数名相同,参数不同,参数的类型或者个数不同,返回值可相同可不同。

**重写/覆盖要求两个函数必须在不用的作用域,必须在继承体系的基类和派生类两个作用域中。**函数名,参数,返回值都必须相同(协变除外),缺省值可以不同。两个函数都必须是虚函数。

**隐藏也是必须在继承体系的基类和派生类两个作用域中。**要求函数名相同。并且隐藏不只是函数,成员变量名称相同也构成隐藏。

2、纯虚函数和抽象类

在虚函数后面写上=0,这个虚函数就是纯虚函数了。 纯虚函数不需要定义,因为定义了也没用。包含纯虚函数的类叫抽象类,抽象类不可以实例化出对象。

我们可以这样理解,纯虚函数和抽象类就是为多态而生的。他就是为了方便后面重写他的派生类们。

cpp 复制代码
#include<iostream>
using namespace std;
class Base {
public:
    virtual void foo()=0;
};

class Derived : public Base {
public:
     virtual void foo() override; 
};

int main()
{
    Base b;//报错
    return 0;
}

编译报错:

3、多态的原理

3.1、虚函数表指针

想想这个问题,一个包含虚函数的类的大小是多少呢?

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	virtual void  func();//4
private:
	int _a;//4
	char _b;//1
	//12
};
void A::func()
{
	cout << 1 << endl;
}
int main()
{
	cout << sizeof A << endl;
	return 0;
}

**执行以上代码,输出结果为12。因为对齐规则的原因,输出结果必须为4的整数倍。**所以说这串代码可能不能说明一个虚函数占多少个字节。那我们再测试一下:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	virtual void  func();//4
private:
};
void A::func()
{
	cout << 1 << endl;
}
int main()
{
	cout << sizeof A << endl;
	return 0;
}

输出结果为4。以上代码都是在32为系统下测试的,如果是64为系统虚函数大小为8。

不知道看到这大家有没有什么想法,4和8恰好是32位系统和64位系统下一个地址所占的字节大小。

不卖关子了直接告诉大家,实际上包含虚函数的基类(注意是基类)无论这个基类中有多少个虚函数,这些虚函数只额外多占4个字节。 因为这些虚函数其实都存在虚函数表中(又叫虚表),而虚表放在我们的基类中。对于这个基类来说,再多几个虚函数也是把这几个虚函数放到虚表中,类中只记录一个虚表的地址(__vfptr)。每个含有虚函数的类都至少有一个虚函数指针。

3.2、多态是如何实现的

**首先我们了解一个概念,动态绑定和静态绑定。静态绑定就是在编译时就确定了运行时需要调用的参数的地址,但是动态绑定是运行时才能确定需要调用的函数的地址,**在这里虚函数属于动态绑定。

那到底是什么流程实现的多态呢?

cpp 复制代码
#include <iostream>
using namespace std;

// 基类 Shape
class Shape {
public:
    // 虚函数 - 用于实现多态
    virtual void draw(){
        cout << "Drawing a generic shape" << endl;
    }
};

// 派生类 Circle
class Circle : public Shape {
public:
    virtual void draw(){  // 重写基类虚函数
        cout << "Drawing a circle" << endl;
    }
};

// 派生类 Square
class Square : public Shape {
public:
    virtual void draw(){  // 重写基类虚函数
        cout << "Drawing a square" << endl;
    }
};
int main() {
    // 创建不同形状的对象
    Circle circle;
    Square square;

    Shape& c = circle;
    Shape& s= square;

    c.draw();
    s.draw();
    return 0;
}

在运行后,我们的c调用draw函数,由于c是Shape类,所以先去Shape中找到draw函数,检测到这个函数为虚函数,接着就会根据引用的对象circle去派生类中的虚表,查找draw函数。同理在s调用draw函数时,也是根据s引用的对象square到派生类中查找draw函数。

虽然都是一个Shape类型的变量去调用函数,但是根这个Shape类型的变量没什么关系,而是有这个变量引用的对象决定的调哪个函数。

3.3、虚函数表

虚函数表到底是个啥?虚函数是存在哪的?虚表是存在哪的?一个类有几个虚函数表?带着这些问题我们继续往下看。

虚函数表就是个函数指针数组。 既然他是个数组,那我们存的他的地址也就是4字节(32位)。他存放的每个函数指针指向每个虚函数。**一般情况下编译器会在这个数组最后放一个0x00000000标记。**但是根据编译器来定,vs系列编译器是有这个标记的,g++系列编译不会放。

接下来我们先解决一个类有几个虚函数表的问题。**首先如果是单继承(只继承一个基类)的情况下,基类有一个虚函数表,派生类有一个虚函数表。**派生类中的虚函数表存放的是派生类自己的虚函数,重写之后的基类的虚函数的地址,还有基类最原始的虚函数地址这三部分。而基类中的虚函数表存放的是最原始的虚函数地址。

如果是多继承的情况,大概率会继承几个基类,就有几个虚函数表。因为对于某个基类的虚函数是得单独维护的。而派生类自己的虚函数存放在其中一个虚表中。

虚函数是存在哪的呢?虚函数和普通函数一样,编译好是一段指令,存放在代码段(常量区)。注意普通函数不是存放在栈区的,而函数中定义的变量才是存放在栈区的。

虚函数表 是存放在哪里的呢?这个根据编译器来定,vs中是存放在代码段(常量区)的。

好了,今天的内容就分享到这,我们下期再见!

相关推荐
tan180°8 分钟前
Linux进程信号处理(26)
linux·c++·vscode·后端·信号处理
一只鱼^_11 分钟前
牛客练习赛138(首篇万字题解???)
数据结构·c++·算法·贪心算法·动态规划·广度优先·图搜索算法
一只码代码的章鱼19 分钟前
Spring的 @Validate注解详细分析
前端·spring boot·算法
邹诗钰-电子信息工程22 分钟前
嵌入式自学第二十一天(5.14)
java·开发语言·算法
恋猫de小郭43 分钟前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
李匠20241 小时前
C++GO语言微服务之Dockerfile && docker-compose②
c++·容器
↣life♚1 小时前
从SAM看交互式分割与可提示分割的区别与联系:Interactive Segmentation & Promptable Segmentation
人工智能·深度学习·算法·sam·分割·交互式分割
zqh176736464691 小时前
2025年阿里云ACP人工智能高级工程师认证模拟试题(附答案解析)
人工智能·算法·阿里云·人工智能工程师·阿里云acp·阿里云认证·acp人工智能
于壮士hoho1 小时前
Python | Dashboard制作
开发语言·python
2301_803554521 小时前
c++和c的不同
java·c语言·c++