C++11&QT复习 (十一)

Day7-5 多态(重要)

1. 多态的基础

面向对象的四大基本特征:抽象、封装、继承、多态

多态:对于同一种指令(如警车鸣笛),针对不同对象(警察、普通人、嫌疑犯),产生不同的行为(行动、正常行走、藏起来)。

2. 多态的分类

  1. 静态多态(编译时确定):
    • 函数重载
    • 运算符重载
    • 模板
  2. 动态多态(运行时确定):
    • 通过虚函数实现

示例代码:

int add(int x, int y) {
   }
double add(double dx, double dy) {
   }
std::string add(std::string s1, std::string s2) {
   }

add(1, 2);
add("hello", "world");

3. 虚函数

概念

虚函数是成员函数,在前面加上 virtual 关键字,使其在派生类中可以被覆盖,实现动态绑定。

性质
  • 在基类中声明为虚函数的函数,在派生类中仍然是虚函数(即使没有显式声明 virtual)。
  • 重定义(重写、覆盖):派生类中的虚函数必须保持相同的名称、返回类型、参数列表
  • 构造函数不能是虚函数,但析构函数应该是虚函数,否则可能会导致内存泄漏
示例代码
#include <iostream>

/*
决不能在基类的构造/析构函数里调用虚函数
因为在构造基类时,子类还没构造,无法调用子类的虚函数
在析构基类时,子类已经被析构了,无法调用子类的虚函数
*/
class Shape {
   
protected:
    double x = 0.0, y = 0.0;
public:
    // 如果这个类有被继承的可能性,必须把析构函数声明为虚的
    // 否则会调用编译时的静态版本,即子类可能错误调用基类的析构函数,导致内存泄漏
    virtual ~Shape() = default;  // 虚析构函数,防止内存泄漏
    // 普通函数,编译时确定,如果指针实际指向子类,但此时被转为了基类,也只是调用基类的函数,即静态绑定
    void print() const {
    std::cout << "(" << x << ", " << y << ")" << std::endl; }
    // virtual 关键字使得函数成为虚函数
    // 如果没有实现函数体,可以加上 = 0 使得成为纯虚函数,纯虚函数会导致类成为虚类,而无法实例化,即无法调用构造函数
    // 虚函数,运行时决定,如果指针实际指向子类,但此时被转为了基类,可以调用正确的版本,即子类的版本,这种特性称为多态,也叫动态绑定
    virtual void v_print() const {
    std::cout << "virtual(" << x << ", " << y << ")" << std::endl; }
};

// 多态必须经继承实现
class Circle : public Shape {
   
private:
    double radius = 100.0;
public:
    // 覆盖了基类的同名版本
    void print() const {
    std::cout << "(" << x << ", " << y << ") " << radius << std::endl; }
    // override 是可选的,但强烈建议加上,表明自己的意图,也能在编译时发现一些错误,比如重写了一个非虚函数,或者忘记继承
    // final 关键字可以禁止子类重新实现该虚函数
    void v_print() const override final {
    std::cout << "virtual(" << x << ", " << y << ") - " << radius << std::endl; }
};

int main()
{
   
  Circle c;
  Shape* s = &c;
  s->print(); // 编译时决定,静态绑定,总是调用基类,因为表面上这是个指向基类的指针
  s->v_print(); // 运行时决定,动态绑定,调用子类的版本,因为实际上这个指针指向了子类

  // 为了实现多态,编译器不得不安插一些代码到类里,运行时才能调用正确的虚函数,细节请阅读《深度探索C++对象模型》
  sizeof(Shape); // 24字节 = 2个double + 为了实现多态的一个指针
  sizeof(Circle); // 32字节 = 基类的2个double + 自己的1个double + 为了实现多态的一个指针

  // RTTI 称为运行时类型识别,这是和多态一起诞生的概念
  // dynamic_cast 可以尝试将基类指针转为子类,如果不是对应的子类,转换结果为空指针
  // dynamic_cast 必须在存在虚函数的情况下,即多态下,如果没有虚函数,只是普通的继承,没法使用dynamic_cast,编译报错
  auto pC = dynamic_cast<Circle*>(s);
  if (pC)
  {
   
    pC->v_print();
  }
  // RTTI 的运行时信息,typeid(类名)可以生成对应的 type_info 类,name()是其中一个成员函数
  // 这是非常有限的反射
  // 对反射感兴趣可以去学习 Java/C# 等纯面向对象语言,其强大的运行时系统提供了完备的类型系统,包括反射
  std::cout << typeid(Shape).name() << std::endl;
  std::cout << typeid(Circle).name() << std::endl;

  return 0;
}

4. 虚函数原理(重要)

虚函数表(vtable)和虚函数指针(vptr)

  • 基类定义虚函数后,每个对象都会有一个虚函数指针(vptr),指向虚函数表(vtable)
  • 派生类继承后,vptr 指向其自己的 vtable。
  • 如果派生类重写了虚函数,vtable 中的对应函数入口地址会被替换为派生类的版本。
示例代码
//virtual1.cpp
#include <iostream>

using namespace std;

#if 0

class Base
{
   
public:
	Base(double base = 0.0)
		:_base(base)
	{
   
		cout << "Base(double base = 0.0)" << endl;
	}
	~Base()
	{
   
		cout << "~Base()" << endl;
	}

	virtual void print() const
	{
   
		cout << "void Base::_base = " << _base << endl;
	}

private:
	double _base;
};

class Derived : public Base
{
   
public:
	Derived(double base = 0.0, double derived = 0.0)
		:Base(base)
		,_derived(derived)
	{
   
		cout << "Derived(double base = 0.0, double derived = 0.0)" << endl;
	}
	~Derived()
	{
   
		cout << "~Derived()" << endl;
	}

	void print() const override
	{
   
		cout << "Derived::_derived = " << _derived << endl;
	}

private:
	double _derived;
};

void func(Base* pbase)
{
   
	pbase->print();
}

void test()
{
   
	//注意:
	/*构造函数不能被继承,但是虚函数是可以被继承的;构造函数发生的时机在编译的时候,而虚函数要体现多态
	,发生的时机在运行的时候。如果构造函数被设置为虚函数,那么要体现出多态,就要放在虚表中,需要使用
	虚函数指针找到虚表,而如果构造函数都不调用,那对象是完全没有创建出来的,对象都不完整,此时有没有
	虚函数指针都不一定。*/
	Base base(11.11);
	Derived derived(22.22, 33.33);

	cout << endl;
	func(&base); //Base *pbase = &base;
	func(&derived);//Base *pbase = &derived;
}


int main()
{
   
	test();
	return 0;
}

#endif // 0

当基类定义了虚函数,就会在该类创建的对象的存储布局的前面,新增一个虚函数指针,该指针指向虚函数表(简称虚表),虚表中存的是虚函数的入口地址(有多少虚函数都会入虚表)。当派生类继承基类的时候,会满足吸收的特点,那么派生类也会有该虚函数,所以派生类创建的对象的布局的前面,也会新增一个虚函数指针,该指针指向派生类自己的虚函数表(简称虚表),虚表中存的是派生类的虚函数的入口地址(有多少虚函数都会入虚表),如果此时派生类重写了从基类这里吸收过来的虚函数,那么就会用派生类自己的虚函数的入口地址覆盖从基类这里吸收过来的虚函数的入口地址。

//virtual2.cpp
#include <iostream>

using namespace std;

class Base
{
public:
	Base(double base = 0.0)
		:_base(base)
	{
		cout << "Base(double base = 0.0)" << endl;
	}
	~Base()
	{
		cout << "~Base()" << endl;
	}

	virtual void print() const
	{
		cout << "void Base::_base = " << _base << endl;
	}

private:
	double _base;
};

class Derived : public Base
{
public:
	Derived(double base = 0.0, double derived = 0.0)
		:Base(base)
		, _derived(derived)
	{
		cout << "Derived(double base = 0.0, double derived = 0.0)" << endl;
	}
	~Derived()
	{
		cout << "~Derived()" << endl;
	}

	void print() const o
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值