C++ 多态与虚函数

我是研究僧i 提交于 2020-02-05 10:38:56

这一篇介绍一下 C++ 面向对象三大特征之一的多态(之前面试某大厂的实习生被问到多态,后来又了解到一些设计模式,才体会到多态的强大,在这里把对多态的一点点浅显认识总结一下)

如有侵权,请联系删除,如有错误,欢迎大家指正,谢谢

多态

  • 父类的一个指针,可以有多种执行状态(父类的指针调用子类的函数),即多态
  • 多态实际上只是一种思想,而虚函数是实现这个思想的语法基础

虚函数

虚表

  • 若对象有虚函数,对象空间最开始 4Byte(32Bit目标平台)或 8Byte(64bit目标平台)内容是虚表(虚函数列表)的首地址,叫虚指针
  • 在实例化对象时,编译器检测到虚函数(virtual修饰的成员函数)时,会将虚函数的地址放到虚表(类似于一个存放函数指针的数组)中
  • 当实例化子类时,检测到有虚函数的重写,编译器会用子类重写的虚函数地址覆盖掉之前父类的虚函数地址,当调用虚函数时,检测到函数是虚函数就会从虚表中找对应的位置调用,若子类没有重写,虚表中的虚函数地址就还是父类的,若子类中有重写,虚表记录的就是子类重写的虚函数地址,即实现了父类的指针调用子类的函数
  • 虚表中先记录父类中的虚函数地址,接着记录子类中虚函数地址(若子类重写父类的虚函数则是覆盖)
  • 最后虚表还有一个尾值是 0
class Test {
public:
	virtual void vfunc() {
		cout << "Test virtual function" << endl;
	}
};

cout << sizeof(Test) << endl;
Test* p = new Test;
p->vfunc();
// 将类指针p强转为long long指针,访问前8Byte(long long*每次操作8Byte)获取到虚表指针,再将虚表指针转为long long指针,访问前8Byte获取到函数指针,将函数指针强转为void(*)()类型(类中的虚函数类型),即可调用函数
reinterpret_cast<void(*)()>(reinterpret_cast<long long*>(*reinterpret_cast<long long*>(p))[0])();
// 将上面的函数指针下移8Byte获得虚表的尾值0
cout << reinterpret_cast<long long*>(*reinterpret_cast<long long*>(p))[1] << endl;

// 运行结果(64bit目标平台下):
// 8
// Test virtual function
// Test virtual function
// 0

虚函数

  • 在一般成员函数前面加 virtual 即可将成员函数声明为虚函数
class Test {
public:
	virtual void func() {  // 虚函数
		cout << "virtual function" << endl;
	}
};

Test().func(); // 运行结果:virtual function ,这是通过临时变量调用的,后面再详细介绍一下临时变量
  • 在单一 class 中实现虚函数意义并不大,虚函数主要是为了实现子类函数重写父类函数的作用
  • 要实现多态,通常父类中的虚函数与子类中的函数的返回值类型、函数名和参数列表必须都相同的,但是在协变的情况下返回值类型可以不一样,协变即虚函数的返回值类型为所在类的指针或引用
// ====== 一般多态的实现 ======
class TestA {
public:
	virtual void vfunc() {
		cout << "TestA virtual function" << endl;
	}

	virtual ~TestA() { }
};

class Test : public TestA {
public:
	virtual void vfunc() {  // 子类的 virtual 可写可不写
		cout << "Test virtual function" << endl;
	}

	~Test() { }
};

TestA* t = new Test; // 父类指针指向子类空间(多态的实现)
t->vfunc();          // 运行结果:Test virtual function,父类指针调用子类函数(多态的实现)
delete t;

// ====== 协变的情况 ======
class TestA {
public:
	virtual TestA& vfunc() {  // 或者是 virtual TestA* vfunc() 返回所在类的指针
		cout << "TestA virtual function" << endl;
		return *this;         // return this; 对应返回所在类的指针
	}

	virtual ~TestA() { }
};

class Test : public TestA {
public:
	virtual Test& vfunc() {
		cout << "Test virtual function" << endl;
		return *this;
	}

	~Test() { }
};

TestA* t = new Test; // 父类指针指向子类空间(多态的实现)
t->vfunc();          // 运行结果:Test virtual function,父类指针调用子类函数(多态的实现)
delete t;
  • 子类重写的函数默认是虚函数,也可以显式的加上 virtual,也可以不加
  • 虚函数不能是内联函数,加上 inline 是没有效果的
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数(在多态中应写虚析构)
class TestA {
public:
	virtual void vfunc() {
		cout << "TestA virtual function" << endl;
	}

	virtual ~TestA() {
		cout << "TestA dtor" << endl;
	}
};

class TestB : public TestA {
public:
	void vfunc() {
		cout << "TestB virtual function" << endl;
	}

	~TestB() {
		cout << "TestB dtor" << endl;
	}
};

class Test : public TestB {
public:
	void vfunc() {
		cout << "Test virtual function" << endl;
	}

	~Test() {
		cout << "Test dtor" << endl;
	}
};

TestB* t = new Test;
t->vfunc();
delete t;

// 运行结果: Test virtual function
//          Test dtor
//          TestB dtor
//          TestA dtor

// 1. class TestB 的 vfunc() 函数前面没有加 virtual 但是它的子类依然可以重写,说明class TestB 的 vfunc() 函数重写了 class TestA 的 vfunc() 函数后自己默认就是虚函数了(前面的 virtual 可写可不写了)
// 2. 析构函数写成虚函数后释放子类的空间时,子类的析构函数执行后还会执行父类的析构函数,避免了内存泄漏

纯虚函数

  • virtual void fun() = 0; // 这是纯虚函数的形式
  • 纯虚函数可以没有函数实现,有纯虚函数的类不能实例化对象,继承有纯虚函数的父类的子类必须在子类中实现它,子类才能实例化对象,如果不在子类中实现它,子类也不能实例化对象
  • 抽象类,有纯虚函数的类就是抽象类
  • 接口类,除数据成员和构造函数外,其余全是纯虚函数的类,子类继承接口类必须实现全部的纯虚函数
  • 构造函数不可以是纯虚函数
class TestA {
public:
	virtual void vfunc() = 0; // 纯虚函数

	virtual ~TestA() { }
};

class Test : public TestA {
public:
	virtual void vfunc() {  
		cout << "Test virtual function" << endl;
	}

	~Test() { }
};

Test* p = new Test;
p->vfunc();
// 解释在虚表代码的注释处
reinterpret_cast<void(*)()>(reinterpret_cast<long long*>(*reinterpret_cast<long long*>(p))[0])();

// 运行结果:
// Test virtual function
// Test virtual function

虚析构

  • 在多态中,如果释放父类指针(指向子类的父类指针),只会调用父类的析构函数,将父类的析构函数声明为虚函数(虚析构,加 virtual 修饰的析构函数),就会先调用子类的析构函数再调用父类的析构函数,所以在多态中,要用虚析构
  • 父类的析构函数加了 virtual 修饰,delete 会调用子类和父类的析构函数,子类可以显式的加 virtual ,也可以不加, 默认是有的 virtual
  • 还有一点需要注意的,delete 谁的指针就会调用谁的析构函数

如果未特殊说明,以上测试均是在win10 vs2017 64bit编译器下进行的

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!