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++ 语言基础
