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++基础 文章被收录于专栏

C/C++ 语言基础

全部评论
这个直接看汇编会更好
点赞 回复 分享
发布于 2023-06-18 15:31 湖南

相关推荐

服从性笔试吗,发这么多笔,现在还在发。
蟑螂恶霸zZ:傻 x 公司,发两次笔试,两次部门匹配挂,
投递金山WPS等公司10个岗位 >
点赞 评论 收藏
分享
11-14 16:13
已编辑
重庆科技大学 测试工程师
Amazarashi66:不进帖子我都知道🐮❤️网什么含金量
点赞 评论 收藏
分享
点赞 评论 收藏
分享
5 8 评论
分享
牛客网
牛客企业服务