C++ 多态如何实现?
1、虚函数
使用关键字 virtual 可以声明一个虚函数。虚函数在派生类中仍然为虚函数,即使派生类中没有使用关键字 virtual 声明。
class Base { public: Base() = default; virtual ~Base(); virtual void virtualFunc(); void nonOverride(); }; class Derive : public Base { public: Derive() = default; ~Derive(); void virtualFunc(); // virtual function };
虚函数具有以下几点性质
1)一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数;
2)virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义;
3)所有虚函数都必须有定义(纯虚函数例外);
4)对虚函数的调用可能在运行时才被解析(运行时多态);
5)派生类中的函数只有与基类虚函数的调用形式一样(有一个例外,返回对象本身的指针或引用)才构成覆盖(重写);
比如基类 Base::VirtualFunc() 返回 char,而派生类 Derive::VirtualFunc() 返回 int,编译时报错,表示返回值不兼容。
class Base { public: virtual ~Base() = default; virtual char VirtualFunc() { reutrn 0; } }; class Derive : public Base { public: int VirtualFunc() { return 0; } }; int main() { Derive d; Base& b = d; b.VirtualFunc(); return 0; }
编译报错
virtual.cc:12:7: error: conflicting return type specified for ‘virtual int Derive::VirtualFunc()’ 12 | int VirtualFunc() { return 0; } | ^~~~~~~~~~~ virtual.cc:7:16: note: overridden function is ‘virtual char Base::VirtualFunc()’ 7 | virtual char VirtualFunc(); | ^~~~~~~~~~~
如果返回值是对象本身的指针或者引用,是可以的。
class Base { public: virtual ~Base() = default; virtual Base* VirtualFunc() { return nullptr;} }; class Derive : public Base { public: Derive* VirtualFunc() { static Derive s_derive; return &s_derive; } }; int main() { Derive d; Base& b = d; b.VirtualFunc(); return 0; }
6)override 关键字显式说明派生类覆盖继承于基类的一个虚函数(编译器会帮助检查是否覆盖虚函数,没有的话会报错,不是也会报错);
class Base { public: virtual ~Base() = default; virtual Base* VirtualFunc() { return nullptr; } void Func2() {} }; class Derive : public Base { public: Derive* VirtualFunc() override { static Derive s_derive; return &s_derive; } void Func2() override {} }; int main() { Derive d; Base& b = d; b.VirtualFunc(); return 0; }
编译报错
virtual.cc:17:8: error: ‘void Derive::Func2()’ marked ‘override’, but does not override 17 | void Func2() override {} | ^~~~~
7)使用作用域运算符可以回避虚函数的机制;
#include <iostream> class Base { public: virtual ~Base() = default; virtual void Out() { std::cout << "Base\n"; } }; class Derive : public Base { public: void Out() { Base::Out(); std::cout << "Derive\n"; } }; int main() { Derive d; Base& b = d; b.Out(); return 0; }
Derive::Out() 函数可以通过 Base::Out() 回避虚函数机制,调用基类实现。
wuyong@Thinkpad:~/debug$ ./a.out Base Derive
8、在虚函数体的位置(声明语句的分号前)书写 =0 就可以将一个虚函数说明为纯虚函数(= 0 标志只能出现在类的虚函数声明处);
也可以为纯虚函数提供定义,不过函数体必须在类外部。
2、抽象基类
含有纯虚函数的类时抽象基类,抽象基类不能被实例化,即使为纯虚函数提供了定义。
class Base { public: virtual ~Base() = default; virtual void Out() = 0; }; void Base::Out() {} int main() { Base b; b.Out(); return 0; }
编译报错,提示不是定义一个抽象基类的对象。
virtual.cc:12:8: error: cannot declare variable ‘b’ to be of abstract type ‘Base’ 12 | Base b; | ^ virtual.cc:3:7: note: because the following virtual functions are pure within ‘Base’: 3 | class Base { | ^~~~ virtual.cc:9:6: note: ‘virtual void Base::Out()’ 9 | void Base::Out() {} | ^~~~
抽象基类正是为了接口和实现分离而存在的,只能定义抽象基类的指针或者引用,通过虚函数动态绑定特性,调用派生类真正的实现,而不需要知道派生类实现细节。
比如在 DB 声明数据库操作接口,其实现在 DBImpl 类中。main 函数中就不需要知道 DBImpl 的实现细节,通过抽象基类固定的接口操作数据库,而 DBImpl 的实现改变也不会对 main 函数整个操作有影响(只要接口保持不变)。
/// db.h #include <string> class DB { public: static int Open(const std::string& name, DB** dbptr); DB() = default; virtual ~DB() = default; DB(const DB&) = delete; DB& operator=(const DB&) = delete; virtual int Put(const std::string& key, const std::string& value) = 0; virtual int Delete(const std::string& key) = 0; virtual int Get(const std::string& key, std::string* value) = 0; }; /// dbimp.h class DBImpl : public DB { public: explicit DBImpl(const std::string& dbname) : dbname_(dbname) {} ~DBImpl() override = default; DBImpl(const DBImpl&) = delete; DBImpl& operator=(const DBImpl&) = delete; int Put(const std::string& key, const std::string& value) override; int Delete(const std::string& key) override; int Get(const std::string& key, std::string* value) override; private: std::string dbname_; }; /// dbimpl.cc #include <iostream> int DB::Open(const std::string& name, DB** dbptr) { if (!dbptr) { return -1; } *dbptr = new DBImpl(name); if (*dbptr == nullptr) { return -1; } return 0; } int DBImpl::Put(const std::string& key, const std::string& value) { std::cout << "[Put] " << key << " " << value << std::endl; return 0; } int DBImpl::Delete(const std::string& key) { std::cout << "[Delete] " << key << std::endl; return 0; } int DBImpl::Get(const std::string& key, std::string* value) { std::cout << "[Get] " << key << std::endl; return 0; } /// main.cc int main() { DB* dbptr; DB::Open("testdb", &dbptr); dbptr->Put("key", "value"); dbptr->Delete("key"); delete dbptr; }
3、虚函数实现
虚函数是通过虚函数表 vtable 实现的,每个具有虚函数的 class 都具有虚函数表,通过 class 的大小我们可以看出一些端倪。
#include <iostream> class Base { public: void* private_; }; class Base2 { public: virtual void Out() { std::cout << "Cout" << std::endl; } virtual void Out2() { std::cout << "Cout2" << std::endl; } public: void* private_; }; int main() { std::cout << "sizeof(Base) = " << sizeof(Base) << std::endl; std::cout << "sizeof(Base2) = " << sizeof(Base2) << std::endl; Base b; Base2 b2; std::cout << &b << ' ' << &(b.private_) << std::endl; std::cout << &b2 << ' ' << &(b2.private_) << std::endl; return 0; }
执行后输出为(ADM64 平台,Debian系统)
sizeof(Base) = 8 sizeof(Base2) = 16 0x7ffdc4ef5298 0x7ffdc4ef5298 0x7ffdc4ef5280 0x7ffdc4ef5288
可以看到,具有虚函数的 Base2 比 Base 大 8 个字节(一个指针的大小),我们猜测 Base2 具有如下布局
+-----------+ | vtable_ | ---------->+-------------+ +-----------+ | func out | | private_ | +-------------+ +-----------+ | func out2 | +-------------+
为了验证我们的想法,做如下尝试,强制
#include <iostream> class Base { public: void* private_; }; class Base2 { public: virtual void Out() { std::cout << "Cout" << std::endl; } virtual void Out2() { std::cout << "Cout2" << std::endl; } public: void* private_; }; int main() { Base2 b2; typedef void (*PFunc)(); uint64_t** vtable = reinterpret_cast<uint64_t**>(&b2); PFunc out = (PFunc)((*vtable)[0]); PFunc out2 = (PFunc)((*vtable)[1]); out(); out2(); return 0; }
输出为
Cout Cout2
印证了我们的想法。为了进一步印证我们的想法,我们验证如下事情:对于派生类重写的虚函数,应该指向不同的地址,而没有重写的虚函数,基类和派生类应该指向同一地址。
#include <iostream> class Base { public: virtual void Out() { std::cout << "Cout" << std::endl; } virtual void Out2() { std::cout << "Cout2" << std::endl; } }; class Derive : public Base { public: virtual void Out() { std::cout << "Derive::Cout" << std::endl; } }; int main() { typedef void (*PFunc)(); { Base b; uint64_t** vtable = reinterpret_cast<uint64_t**>(&b); PFunc out = (PFunc)((*vtable)[0]); std::cout << (void*)(*vtable)[0] << std::endl; PFunc out2 = (PFunc)((*vtable)[1]); std::cout << (void*)(*vtable)[1] << std::endl; out(); out2(); } { Derive d; uint64_t** vtable = reinterpret_cast<uint64_t**>(&d); PFunc out = (PFunc)((*vtable)[0]); std::cout << (void*)(*vtable)[0] << std::endl; PFunc out2 = (PFunc)((*vtable)[1]); std::cout << (void*)(*vtable)[1] << std::endl; out(); out2(); } return 0; }
输出为
0x561426d43320 0x561426d43358 Cout Cout2 0x561426d43390 0x561426d43358 Derive::Cout Cout2
进一步印证了我们的猜想。
4、多重继承虚函数实现
#include <iostream> class Base { public: virtual void Out() { std::cout << "Cout" << std::endl; } uint64_t val; }; class Base2 { public: virtual void Out2() { std::cout << "Cout2" << std::endl; } uint64_t val2; }; class Derive : public Base, public Base2 { public: virtual void Out2() { std::cout << "Derive::Cout2" << std::endl; } virtual void Out3() { std::cout << "Derive::Cout3" << std::endl; } uint64_t val3; }; int main() { Derive d; std::cout << "sizeof(Derive) = " << sizeof(Derive) << std::endl; std::cout << &d << std::endl; std::cout << &d.val << std::endl; std::cout << &d.val2 << std::endl; std::cout << &d.val3 << std::endl; return 0; }
从 Derive 的布局来看,多重继承也有相似的虚函数实现:每一个基类都会有自己的虚函数表,派生类的虚函数表的数量根据继承的基类的数量来定。
sizeof(Derive) = 40 0x7fff72a07df0 0x7fff72a07df8 0x7fff72a07e08 0x7fff72a07e10
还是和之前一样,我们看看 vtable 如何布局,已经 Derive 新定义的虚函数在那个 vtable 中。
首先,如果有另外一个 Derive2 继承 Derive,并且没有添加虚函数,那么第一个 vtable 应该具有 Derive 定义的三个虚函数,因此猜测布局如下:
+---> +----------------------+ +----------+ | | Base1::Out | | vtable |---+ +-----------------------+ +----------+ | Derive::Out2 | | val1 | +-----------------------+ +----------+ | Derive::Out3 | | vtable |---+ +-----------------------+ +----------+ | | val2 | +---> +-----------------------+ +----------+ | Base2::Out2() | | val3 | +-----------------------+ +----------+
我们通过代码
int main() { Derive d; typedef void (*PFunc)(); uint64_t** vtable1 = reinterpret_cast<uint64_t**>(&d); std::cout << &(*vtable1)[0] << std::endl; std::cout << &(*vtable1)[1] << std::endl; std::cout << &(*vtable1)[2] << std::endl; for (int i = 0; i < 3; ++i) { std::cout << &(*vtable1)[i] << std::endl; PFunc func = (PFunc)((*vtable1)[i]); func(); } std::cout << std::endl; uint64_t** vtable2 = reinterpret_cast<uint64_t**>((char*)&d + 16); for (int i = 0; i < 3; ++i) { std::cout << &(*vtable2)[i] << std::endl; PFunc func = (PFunc)((*vtable2)[i]); func(); } return 0; }
输出为
0x55ae7e38ed20 0x55ae7e38ed28 0x55ae7e38ed30 0x55ae7e38ed20 Cout 0x55ae7e38ed28 Derive::Cout2 0x55ae7e38ed30 Derive::Cout3 0x55ae7e38ed48 Derive::Cout2 0x55ae7e38ed50 Segmentation fault
和我们的猜想符合。
欢迎关注公众号“源知源为”,阅读更多技术干货
C/C++ 语言基础