【C++】多态

目录

    • [1. 多态的概念](#1. 多态的概念)
    • [2. 多态的定义及实现](#2. 多态的定义及实现)
      • [2.1 多态的构成条件](#2.1 多态的构成条件)
      • [2.2 虚函数](#2.2 虚函数)
      • [2.3 虚函数的重写 (覆盖)](#2.3 虚函数的重写 (覆盖))
        • [! ! ! 一个需要极力避免的设计缺陷](#! ! ! 一个需要极力避免的设计缺陷)
          • [1. 编译期:默认参数的静态绑定](#1. 编译期:默认参数的静态绑定)
          • [2. 运行期:虚函数的动态绑定](#2. 运行期:虚函数的动态绑定)
        • 虚函数重写的两个例外:
          • [协变 (基类与派生类虚函数返回值类型不同):](#协变 (基类与派生类虚函数返回值类型不同):)
          • [析构函数的重写 (基类与派生类析构函数的名字不同):](#析构函数的重写 (基类与派生类析构函数的名字不同):)
      • [2.4 C++11 override 和 final](#2.4 C++11 override 和 final)
      • [2.5 重载、覆盖(重写)、隐藏(重定义) 的 对比](#2.5 重载、覆盖(重写)、隐藏(重定义) 的 对比)
    • [3. 抽象类](#3. 抽象类)
      • [3.1 概念](#3.1 概念)
      • [3.2 接口继承和实现继承](#3.2 接口继承和实现继承)
    • [4. 多态的原理](#4. 多态的原理)
      • [4.1 虚函数表](#4.1 虚函数表)
      • [4.2 多态的原理](#4.2 多态的原理)
      • [4.3 动态绑定与静态绑定](#4.3 动态绑定与静态绑定)

1. 多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。例如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2. 多态的定义及实现

2.1 多态的构成条件

在继承中要构成多态有两个条件:

  • 必须通过基类的指针或者引用调用虚函数。
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

2.2 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

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

2.3 虚函数的重写 (覆盖)

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

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

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

class Student : public Person {
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
	}
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
      void BuyTicket() { cout << "买票-半价" << endl; }*/
};

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);  //输出"买票-全价"
	Func(st);  //输出"买票-半价"
	return 0;
}
! ! ! 一个需要极力避免的设计缺陷
cpp 复制代码
class Person {
public:
	virtual void BuyTicket(int v = 100){ 
		cout << "买票-全价 " << v << endl;
	}
};

class Student : public Person {
public:
	virtual void BuyTicket(int v = 50) { 
		cout << "买票-半价 " << v << endl; 
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);  //输出"买票-全价 100"
	Func(st);  //输出"买票-半价 100"
	return 0;
}

原因如下:

1. 编译期:默认参数的静态绑定

Func 函数中,参数 p静态类型 (声明时的类型)是 Person&。 当编译器编译到 p.BuyTicket(); 这一行时,它发现没有传入参数,于是会去寻找默认参数。因为 p 的静态类型是 Person,编译器提取了 Person::BuyTicket 中的默认参数 v = 100

因此,在编译阶段,这行代码实际上已经被编译器替换成了类似这样的调用: p.BuyTicket(100);

2. 运行期:虚函数的动态绑定

虚函数具有多态性,具体调用哪个类的函数取决于对象的动态类型(实际传入的对象类型)。

  • 执行 Func(ps) : 传入的是 Person 对象,动态类型是 Person。运行时调用 Person::BuyTicket。结合编译期塞入的参数 100,输出:买票-全价 100
  • 执行 Func(st) : 传入的是 Student 对象,动态类型是 Student。运行时触发多态,调用子类 Student 重写的 BuyTicket 方法(打印 "买票-半价 ")。 关键点来了 :虽然函数调用被正确路由到了 Student::BuyTicket,但传入的参数值早在编译期就被静态绑定为 Person 类的默认值 100 了。因此最终输出:买票-半价 100
虚函数重写的两个例外:
协变 (基类与派生类虚函数返回值类型不同):

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

cpp 复制代码
class A{};
class B : public A {};

class Person 
{
public:
    virtual A* f() {return new A;}   
};
class Student : public Person 
{
public:
		virtual B* f() {return new B;}	
};
//返回值 A 与 B 必须构成继承关系
析构函数的重写 (基类与派生类析构函数的名字不同):

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。析构函数一定设计成虚函数

cpp 复制代码
class A {};
class B : public A {};

class Person {
public:
    // 析构函数重写:建议基类析构定义为虚函数
    virtual ~Person() { 
        cout << "~Person()" << endl;
    }
};

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

int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;

    delete p1;
    delete p2;
return 0;
}

2.4 C++11 override 和 final

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

cpp 复制代码
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

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

cpp 复制代码
class Base {
public:
    // 基类的一个虚函数,注意它带有 const 修饰符
    virtual void showData(int a) const {
        std::cout << "Base showData: " << a << std::endl;
    }
};

class Derived : public Base {
public:
    // 正确的重写:函数签名与基类完全一致,顺利通过编译
    void showData(int a) const override {
        std::cout << "Derived showData: " << a << std::endl;
    }

     //错误示例 1:忘记了 const 修饰符
     void showData(int a) override { 
 // 因为少了 const,它不再是基类那个虚函数了。
     }
    // 错误示例 2:拼写错误
     void showDates(int a) const override { 
 // 把 Data 拼成了 Dates,如果没有 override,这将被视为派生类特有的新函数,有了 override 就会直接拦截这个手误。
     }
    // 错误示例 3:参数类型不匹配
     void showData(double a) const override { 
 // 基类是 int,这里变成了 double,构成了隐藏而非重写,override 同样会报错。
     }
};

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

概念 作用域 函数名 / 参数 / 返回值
重载 两个函数在同一作用域 函数名相同、参数不同
**重写 (覆盖) ** 两个函数分别在基类和派生类的作用域 (两个函数必须是虚函数 ) 函数名、参数、返回值都必须相同 (协变例外)
重定义 (隐藏) 两个函数分别在基类和派生类的作用域 函数名相同

**两个基类和派生类的同名函数不构成重写就是重定义 **

3. 抽象类

3.1 概念

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

实践中,一个类型在现实中没有实体对象,不想实例化出对象,设计成抽象类。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

cpp 复制代码
class Animal {
public:
    virtual void sound() const = 0; // 纯虚函数,Animal为抽象类
};

class Cat : public Animal {
public:
    virtual void sound() const override {
        cout << "喵喵" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void sound() const override {
        cout << "汪汪" << endl;
    }
};

void AnimalSound(const Animal& anm) {
    anm.sound();
}

3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。如果不实现多态,不要把函数定义成虚函数。

4. 多态的原理

4.1 虚函数表

虚函数表,本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。 虚函数放在代码段,虚函数的地址存在虚表中。虚表放在常量区,虚表的地址放在对象中。

cpp 复制代码
class Base
{
public:
		virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
		int _a = 1;
};
int main()
{
    Base b;
}

问:sizeof(Base)是多少?

派生类虚表的生成总结:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中。
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
cpp 复制代码
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:
  	// 覆盖Base::Func1
    virtual void Func1() { cout << "Derive::Func1()" << endl; } 
private:
    int _d = 2;
};
int main()
{
    Base b;
    Derive d;
		return 0;
}

总结

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
    中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
    的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
    数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
    类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 注意
    虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
    他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的
    呢?验证发现vs下是存在代码段的。

4.2 多态的原理

cpp 复制代码
class Base {
public:
    virtual void Func1() { cout << "Base::Func1()" << endl; }
private:
    int _b = 1;
};
class Derive : public Base {
public:
    virtual void Func1() { cout << "Derive::Func1()" << endl; }
private:
    int _d = 2;
};
void Print(Base* base)
{
    base->Func1();
}
int main()
{
    Base b;
    Derive d;
    Print(&b);
    Print(&d);
  	d.Func1();
    return 0;
}

4.3 动态绑定与静态绑定

  • 静态绑定 :又称为前期绑定 (早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载、函数模版。
  • 动态绑定 :又称后期绑定 (晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
相关推荐
Shadow(⊙o⊙)1 小时前
日志与线程池
linux·c++·算法
我不是懒洋洋1 小时前
从零实现一个分布式限流器:令牌桶与漏桶
c++
观远数据1 小时前
经营闭环的最后一公里:从看见问题到解决问题之间还差什么
java·开发语言·数据库
Java面试题总结2 小时前
Go 语言大白话入门 6 - 判断与循环
开发语言·后端·golang
m0_617493942 小时前
Python OpenCV 孔洞检测与缺陷分析详解与实战
开发语言·python·opencv
hsg773 小时前
简述:Rust、GeoRust、自主研发GIS平台
开发语言·后端·rust
承渊政道3 小时前
飞算Java炫技赛:Java×Unity数字孪生园区平台的从零落地记录
java·开发语言·unity·技术分享·vibe coding·飞算javaai·飞算java ai炫技赛
不负岁月无痕3 小时前
STL --C++哈希表封装unordered_map/set
c++·visualstudio·哈希算法·散列表·unordered
wdfk_prog9 小时前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c