多态

去完成某个行为, 当不同的对象去完成时会产生不同的状态

多态的定义和实现

多态的构成条件

多态是在不同继承关系的类的对象, 去调用同一函数, 产生了不同的行为

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

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

虚函数

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

虚函数的重写(覆盖)

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

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

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

void Func(Person& p) {
	p.BuyTicket();
}

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

虚函数重写的两个例外

  • 协变(基类与派生类函数返回值类型不同)

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

    // 例
    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;}
    };
    
  • 析构函数的重写(基类和派生类析构函数的名字不同)

    如果基类的析构函数为虚函数, 此时派生类析构函数只要定义, 无论加不加virtual关键字, 都与基类的析构函数构成重写

重载, 覆盖(重写), 隐藏(重定义)

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

抽象类

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

接口继承和实现继承

普通函数的继承是一种实现继承, 派生类继承了基类函数, 可以使用函数, 继承是函数的实现. 虚函数的继承是一种接口继承, 派生类继承的是基类虚函数的接口, 目的是为了重写, 达成多态, 继承的是接口.

多态的原理

class Base {
public:
	virtual void Func1() {
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
// sizeof(Base)为8. 除了_b成员外, 还多一个_vfptr放在对象的前面

对象中的这个指针叫做虚函数表指针. 一个含有虚函数的类中都至少有一个虚函数表指针, 因为虚函数的地址要被放到虚函数表中, 虚函数表也叫作虚表

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:
	virtual void Func1() {
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main() {
	Base b;
	Derive d;
	
	return 0;
}
  • 派生类对象d也有一个虚表指针, d对象由两部分构成, 一部分是父类继承下来的成员, 虚表指针也就是存在部分的另一部分是自己的成员
  • 基类b对象和派生类对象虚表是不一样的. Func1完成了重写, 所以d的虚表中存的是重写的Derive::Func1, 所以虚函数的重写也叫覆盖, 覆盖就是指虚表中虚函数的覆盖
  • Func2继承下来后是虚函数, 所以放进了虚表, Func3也继承下来了, 但不是虚函数, 所以不会放进虚表
  • 虚函数表本质是一个存虚函数指针的指针数组, 这个数组最后面放了一个nullptr
  • 派生类虚表的生成:
    • 先将基类中的虚表内容拷贝一份到派生类虚表中
    • 如果派生类重写了基类中某个虚函数, 用派生类自己的虚函数覆盖虚表中基类的虚函数
    • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  • 虚表存的是虚函数指针, 不是虚函数. 虚函数和普通函数一样, 都是存在代码段的, 只是它的指针又存到了虚表中
  • 对象中存的不是虚表, 而是虚表指针

动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定), 在程序编译期间确定了程序的行为, 也称为静态多态
  • 动态绑定又称为后期绑定(晚绑定), 在程序运行期间, 根据具体拿到的类型确定程序的具体行为, 调用具体的函数, 也称为动态多态

常见问题

  • inline可以是虚函数吗?

    不能, 因为inline函数没有地址, 无法将地址放在虚函数表中

  • 静态成员可以是虚函数吗?

    不能, 因为静态成员没有this指针

  • 构造函数可以是虚函数吗?

    不能, 因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的

  • 析构函数可以是虚函数吗?

    可以, 最好把基类的析构函数定义为虚函数

  • 对象访问普通函数快还是虚函数快?

    如果是普通函数, 是一样快的.

    如果是指针对象或者是引用对象, 则调用的普通函数快, 因为构成多态, 运行时调用虚函数需要到虚函数表中去查找

  • 抽象类的作用

    抽象类强制重写了虚函数, 另外抽象类体现了接口继承关系

 


我们的征途是星辰大海!