从C++开始的编程生活(17)——多态

前言

本系列文章承接C语言的学习,需要有**++C语言的基础++** 才能学会哦~

第17篇主要讲的是有关于C++的++多态++ 。
C++已经进入进阶,加油!!

目录

前言

多态

语法

实现多态的重要条件!!

重写(覆盖)的条件

协变(了解即可)

析构函数的重写

override和final关键字

纯虚函数和抽象类

抽象类

虚函数表

多态的原理

动态绑定和静态绑定

虚函数表原理


多态

多态分为**++静态多态++++动态多态++**

**静态多态:**传不同参数,编译出的函数就会不同(如函数重载、函数模板等)。
**动态多态:**同一个行为(函数),不同的对象完成的方式或者结果不一样(下文重点讲)。

语法

cpp 复制代码
//函数重写Buyticket()
class Peraon
{
public:
    virtual void BuyTicket() { cout << "买票------全价" << endl;}//①
};

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

void Func(Person* ptr)
{
    ptr->Buyticket();
}

int main()
{
    Person ps;
    Student st;    
//根据传入对象,调用指定的函数
    Func(&ps);//调用①
    Func(&st);//调用②

    return 0;
}

实现多态的重要条件!!

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

父类指针既可以指向父类也可以指向子类。
被调用的函数必须是虚函数(关键字virtual在这里和虚继承的virtual没有关系)。
子类必须对父类的虚函数进行重写

重写(覆盖)的条件

同名同参数同返回值虚函数函数体不同,即可构成重写。如果++没有virtual修饰,就会被函数隐藏。++

++在满足多态的情况下,会调用子类重写的虚函数,但是函数声明遵循的是父类,因为继承了父类的整个函数签名++(连virtual也继承了)。

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()
{
    B* p = new B;
    p->test();

    return 0;
}

观察上述代码,我们可以看到B的func满足了重写A的func的条件,函数声明的属性用的是父类的声明,但是调用的是子类的函数体,所以最后输出的结果是B->1,让val使用了父类的缺省值。

这里也告诉我们,在写多态的时候,父子类最好不要有不同的缺省值,不然就会有意外的事情发生。

协变(了解即可)

子类重写父类虚函数的时候,与父类虚函数的返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,这就叫做协变。(算是特殊情况,了解即可)。

析构函数的重写

子类的++析构函数只要定义了,无论是否函数名是否相同,都可以与父类的析构函数构成重写++。++编译器会把子类的析构看成重写,并且进行特殊处理++。

特殊处理:把父类和子类的函数名在编译时都换为destructor()

override和final关键字

override放在函数的括号后面修饰。++被修饰的函数必须重写++ ,不然就报错,可用于自检。
final也放在函数的括号后面修饰,++被修饰的函数禁止重写++,不然就报错,可用于自检。

纯虚函数和抽象类

(如果懂java可以类别为java的抽象接口)

纯虚函数:在虚函数的后面写上=0,那么这个函数就叫做纯虚函数,纯虚函数不需要定义实现,就算实现了也必须被重写,所以只用声明即可。

抽象类

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

抽象类不可以实例化出对象,如果其子类不对其纯虚函数进行重写,那么子类也是抽象类,也不能实例化出对象。

虚函数表

简称虚表,存储了抽象类中所有虚函数的指针。

++抽象类存储时,除了存储成员变量外,还会存储一个隐藏的虚函数表指针++,这个指针指向一个指针数组,存储了虚函数的指针。

cpp 复制代码
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

如上的抽象类,运用内存对齐的知识,在32位下,该类的大小为12字节,而不是8字节,因为需要先存储虚函数表指针。

抽象类存储如上(_vftptr为虚函数表指针)

多态的原理

当抽象类被继承之后,其子类也会继承一个隐藏的成员------即虚函数表指针。但是指向的虚函数表不一样,子类的虚表指针指向的是自己的虚函数表。

因此,在进行多态调用的时候,++会在对应的虚表中找到对应的虚函数进行调用++。

cpp 复制代码
void Func(Person* ptr)
{
    //因为指针类型是Person*,所以传入的子类指针,只能访问父类成员的切片
    ptr->BuyTicket();
}

int main()
{
    Person ps;
    Student st;

    Func(&ps);//父类对象,调用父类对象的虚函数表中的函数
    Func(&st);//子类对象,调用子类对象的虚函数表中的函数
}

如上,因为Func的参数为父类指针,

故:
子类调用的时候,++只能访问到父类成员的切片,具有安全性++;
同时因为父子类存储的虚表不同,++传入父类就调用父类的虚函数,传入子类就调用子类的虚函数,从而实现多态++(调用了同一个函数Func却有不同的效果)。

总结:指针指向谁,调用时就实现谁。

动态绑定和静态绑定

静态绑定: 对不满足多态条件的函数调用,++在编译的时候就绑定好++,编译时就已经确定了要调用的地址。

动态绑定: 对满足多态条件的函数调用,++在运行的时候才绑定++,也就是运行时到指定对象的虚函数表中查询要调用的地址来调用。

虚函数表原理

父类对象 的虚函数表中存放着父类所有虚函数的地址。

子类由继承的父类和自己的成员组成,子类的虚表在继承的父类成员里,但是和父类的虚表不是相同的

子类重写了父类的虚函数后,就会用重写后的虚函数覆盖虚函数表里的函数。没重写就不覆盖。

子类的虚函数表包括父类的虚函数地址、重写父类的虚函数地址、自己的虚函数地址(但是vs监视窗口会隐藏)。

⑤同类型对象使用的虚函数表是一样的。如Person p1和Person p2使用的是同一个虚表,不会再另外开辟空间。

⑥一般虚表这个数组最后会放一个0x00000000进行标记(vs有放,g++没放,具体看编译器定义)。

虚函数存放在代码段(也叫常量区)。在vs中,虚表也存放在代码段(虚表存放C++标准没要求)。

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func1() { cout << "func1" << endl; }
	void func2() { cout << "func2" << endl; }

	int _a1 = 1;
	int _a2 = 2;
};

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

	int _a3 = 3;
	int _a4 = 4;
};

int main()
{
	Person p;
	Student st;
	return 0;
}

运行后,启动监视窗口

可见++父子类对象的虚函数表地址是不一样的++,但是继承下来的_a1和_a2确实一样的。

补充:虽然函数名可以代表函数指针,但是取成员函数的地址要加取地址符号,这是语法规定,nowhy。

❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤

相关推荐
无心水2 小时前
1、Go语言工作区和GOPATH实战指南:从项目初始化到部署
开发语言·后端·架构·golang·go·gopath·go mod init
少控科技2 小时前
QT高阶日记008
开发语言·qt
阿蒙Amon2 小时前
C#每日面试题-Task和ValueTask区别
java·开发语言·c#
Frank_refuel2 小时前
C++之多态详解
开发语言·c++
TDengine (老段)2 小时前
TDengine R 语言连接器进阶指南
大数据·开发语言·数据库·r语言·时序数据库·tdengine·涛思数据
FAFU_kyp2 小时前
Rust 泛型(Generics)学习教程
开发语言·学习·rust
VekiSon2 小时前
ARM架构——C 语言+SDK+BSP 实现 LED 点灯与蜂鸣器驱动
c语言·开发语言·arm开发·嵌入式硬件
研☆香2 小时前
JavaScript 历史列表查询的方法
开发语言·javascript·ecmascript
Elnaij2 小时前
从C++开始的编程生活(18)——二叉搜索树基础
开发语言·c++