C++多态(下)
Posted 风起、风落
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多态(下)相关的知识,希望对你有一定的参考价值。
文章目录
1.单继承中的虚函数表
整体代码
#include<iostream>
using namespace std;
class Base
public:
virtual void Func1()
cout << "Base::Func1()" << endl;
virtual void Func2()
cout << "Base::Func2()" << endl;
void Func3()
cout << "Base::Func3()" << endl;
private:
int _b = 1;
;
class Derive : public Base
public:
virtual void Func1()
cout << "Derive::Func1()" << endl;
virtual void Func4()
cout << "Base::Func4()" << endl;
private:
int _d = 2;
;
typedef void(*VF_PTR)();
// typedef void(*)() VF_PTR;
void PrintVFTable(VF_PTR table[])//函数指针数组
int i = 0;
for (i = 0; table[i] != nullptr; i++)
printf("[%d]:%p\\n",i, table[i]);
VF_PTR f = table[i];
f();
int main()
Base b;
Derive d;
PrintVFTable((VF_PTR*)*(int*)&b);
cout << endl;
PrintVFTable((VF_PTR*)*(int*)&d);
return 0;
在子类中实现一个虚函数Func4,但是不构成重写
![](https://image.cha138.com/20230406/0a939ce3753b46d9b7ba13ad23624017.jpg)
![](https://image.cha138.com/20230406/1455a284eed54f8aaa9bec458d8f0feb.jpg)
Func4函数并没有进入虚表中
![](https://image.cha138.com/20230406/191ebdddac8a4e7fb578b2d08498d463.jpg)
通过查询内存发现,虚表指针中存在三个地址,而其中两个正好为监视中的两个地址
猜测 0x00c4146a 就是Func4的地址
用程序打印虚表
虚表本质是一个函数指针数组
![](https://image.cha138.com/20230406/e706487819d1422f981f1c526966b2a6.jpg)
VS中在数组最后放了一个nullptr,这样就可以解决在不同虚表中的个数不同的问题
![](https://image.cha138.com/20230406/c26d0219f8ad44a19d130a6bc064d426.jpg)
typedef一个函数指针 为VF_PTR
正常来说 要写成将VF_PTR放在后面
![](https://image.cha138.com/20230406/1b8000744a564863a5bb53bf71415b31.jpg)
但是由于函数指针的特殊性,定义变量都要放在中间
如何寻找到虚表地址
想要虚表的地址,就可以通过找到虚表的指针
而这个虚表指针在对象中,这个指针在对象的前4个(32位)或者8个字节(64位)上面
![](https://image.cha138.com/20230406/55f5b55bc9af4646abf96a5f4d46cdac.jpg)
以32位为例,如何取对象的前4个字节
强制转换为int*
![](https://image.cha138.com/20230406/fb6e7a29793149ed8e8086e06bd10713.jpg)
* (int* )&b
首先取到Base* ,将其强制转换为int*,代表前四个字节的地址,再解引用是int类型,把前四个字节取出来
但是由于PrintVFTable函数参数是 函数指针数组
(VF_PTR*) * (int *)&b
如果这个数组是int类型,就需要 一个int * 指针去指向
同理 ,该数组为 VF_PTR类型,需要一个VF_PTR *指针去指向
所以需将 int 类型 再次强制转换为 VF_PTR * ,使其指向这个数组
缺陷
但是这种写法具有一定的局限性,只能在32位跑,因为32位指针大小为4个字节
而64位下就不行了,64位下指针大小为8个字节
运行程序打印虚表,确实了解到多了一个地址
![](https://image.cha138.com/20230406/3f0b8a42dddb47219a6256c98f0b8fe3.jpg)
![](https://image.cha138.com/20230406/2cb74000494a4c66946e75986c5e1cd5.jpg)
把虚表的地址拿出来赋给函数指针,用函数指针去调用函数
这里发现 监视中没有出现的地址确实是Func4函数的地址
虚表存在哪里?
![](https://image.cha138.com/20230406/6778503e23c1433f81368823dac6b0dc.jpg)
由于常量区地址与虚表的地址最为接近,所以说明虚表在常量区/代码段上
2.多继承中的虚函数表
整体代码
class Base1
public:
virtual void func1() cout << "Base1::func1" << endl;
virtual void func2() cout << "Base1::func2" << endl;
private:
int b1;
;
class Base2
public:
virtual void func1() cout << "Base2::func1" << endl;
virtual void func2() cout << "Base2::func2" << endl;
private:
int b2;
;
class Derive : public Base1, public Base2
public:
virtual void func1() cout << "Derive::func1" << endl;
virtual void func3() cout << "Derive::func3" << endl;
private:
int d1;
;
typedef void(*VF_PTR)();
// typedef void(*)() VF_PTR;
void PrintVFTable(VF_PTR table[])//函数指针数组
int i = 0;
for (i = 0; table[i] != nullptr; i++)
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];
f();
int main()
Derive d;
PrintVFTable( (VF_PTR*) *(int*) &d);
cout << endl;
/*PrintVFTable((VF_PTR*)*(int*)( (char*)&d+ sizeof(Base1) ) );*/
Base2* ptr2 = &d;
PrintVFTable((VF_PTR*)*(int*)(ptr2));
return 0;
寻找虚表地址
![](https://image.cha138.com/20230406/8e9f90405bd344f7b41211cfca22e04f.jpg)
Derive 作为Base1 和Base2的子类,所以Derive内部有两张虚表
![](https://image.cha138.com/20230406/de14b2effb414f0abf88dacc059961bb.jpg)
正常来说,Derive内部还存在一个func3函数,这个函数放在哪里了呢?
借助打印虚表来查看,这里的打印虚表依旧可以使用单继承中的那个
![](https://image.cha138.com/20230406/115dc1ef63fc4fae9e4a03052cb57543.jpg)
![](https://image.cha138.com/20230406/424593fe873e46a2a468d00ef171ed3e.jpg)
base1的虚表指针 正好在对象的前4个字节处,直接可以使用求出虚表指针 去指向base1的虚表
方法1 : base2的虚表指针 需要加上base1的大小
![](https://image.cha138.com/20230406/65a9061b446d442cbfa9accaafdd9796.jpg)
但是这里要注意一个问题,若写成 PrintVFTable((VF_PTR*)(int)( &d+ sizeof(Base1) ) )
写的并不对,d本身是一个Derive类型,&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小
而该设计想要每次+1跳转1个字节,所以需要强制转换char*
方法2 :切片自动偏移
![](https://image.cha138.com/20230406/ba1711a26eda4b6282a37acb64738840.jpg)
![](https://image.cha138.com/20230406/9bd771fc0a9443ecb05b00474c38a2f7.jpg)
两种方法的结果都是一样的
注意事项
![](https://image.cha138.com/20230406/50edc6a758d3408cb76ae957be83dfb0.jpg)
多继承派生类增加的的虚函数在第一个虚表中
多继承重写后的func1的地为什么地址不同?
![](https://image.cha138.com/20230406/0ac722308fdd4e9fa96337eacfde711d.jpg)
ptr1调用函数——一次jmp
![](https://image.cha138.com/20230406/368c134b874a47ec9dd44d5999d72ff3.jpg)
找到 Base1虚表里的地址 0x00e21230 ,再call这个地址
只需要jmp一次 就可以找到实际真正执行的函数地址
ptr1调用地址属于正常调用
ptr2 调用函数——多次jmp
![](https://image.cha138.com/20230406/7586fc15d46b44f99a4763c275569739.jpg)
ptr2调用地址,需要 多次jmp 才能找到真正的函数地址
![](https://image.cha138.com/20230406/b5d43703d7cb4e9182b2c9946968cbc2.jpg)
ecx存的是this指针
ecx,8 目的是修正this指针的位置
最终Base1和Base2都是执行同一个函数的指令
![](https://image.cha138.com/20230406/35dc9a641af244ceb0b264a18c74fd2f.jpg)
- ptr1->func1 调用的是子类的func1函数,ptr1指向调用对象的开始
- ptr2并没有指向子类对象的开始,此时调用子类对象的func1函数,this指针指向中间的位置不对了,所以需要修正this指针,使之指向子类对象开始的地方
是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?
【中文标题】是否可以在不手动将重写的克隆方法添加到 C++ 中的每个派生类的情况下克隆多态对象?【英文标题】:Is it possible to clone a polymorphic object without manually adding overridden clone method into each derived class in C++? 【发布时间】:2019-03-09 10:36:45 【问题描述】:当您想要复制多态类时,典型的模式是添加一个虚拟克隆方法并在每个派生类中实现它,如下所示:
Base* Derived::clone()
return new Derived(*this);
然后在调用代码中你可以:
Base *x = new Derived();
Base *y = x->clone();
但是,如果您有 50 多个派生类并意识到您需要多态复制,那么将克隆方法复制粘贴到每个派生类中会很繁琐。它本质上是一个样板文件,可以解决语言限制,您必须拼出实际名称才能调用构造函数。
我没有跟踪最近 C++ 标准中的新特性...有没有办法在现代 C++ 中避免这种情况?
【问题讨论】:
我不认为 c++ 支持,但也许你可以写一个简单的宏,比如GEN_CLONE(Derived)
。
我想这样的框架很难想象,即使对于地球上一些最聪明的 C++ ISO 团队来说也是如此。一种体面的克隆模式尚未出现在任何语言中。看看爪哇的狗晚餐。 CRTP 解决方案可能是最好的解决方案。这不是一个“XY”问题吗?为什么你真的需要克隆东西?
据我所知,这里的正常做法是对复制构造函数as std::function
does 进行类型擦除。这当然是可能的,而且这种技术肯定在使用。
我个人认为任何解决方案至少与实际问题一样复杂。所以我会硬着头皮实现你的clone()
函数以及你所有的其他强制覆盖。此外,您的 clone()
函数应该返回 std::unique_ptr<Base>
。
@Bathsheba 对于多态对象,如果你想完整地复制它们(而不是切片),你不能真正避免克隆。
【参考方案1】:
你可以使用这个通用的 CRTP 代码
template <class Derived, class Base>
struct Clonable : Base
virtual Base* do_clone()
return new Derived(*static_cast<Derived*>(this));
Derived* clone() // not virtual
return static_cast<Derived*>(do_clone());
using Base::Base;
;
struct empty ;
struct A : Clonable<A, empty> ;
struct B : Clonable<B, A> ;
如果需要,它可以推广到智能指针和多个基数。
【讨论】:
哇,我从未听说过这种模式。我会检查我是否可以用它来解决我的问题。 @Calmarius CRTP 主要是 C++ 模式。难怪你是否习惯了其他语言。 @Red.Wave:在 C++ 之外,它被称为“F-bounded polymorphism”。您将主要在 Java 的Comparable<T>
接口的上下文中听到这一点。由于 C++ 模板与类型擦除有根本的不同,CRTP 和 F 有界多态性显然不是一回事,但在实践中它们倾向于解决相同类型的问题。
@Kevin 认为它在 C++ 之外并不常见。??
do_clone()
方法返回Base
的目的是什么? πάντα ῥεῖ 的答案对我来说似乎更明显正确【参考方案2】:
您可以使用 CRTP 方法,但它还有其他缺点:
struct Base
virtual Base* clone() const = 0;
;
template <typename Derived>
class BaseT : public Base
// ...
public:
Base* clone() const override
return new Derived(*static_cast<Derived*>(this));
;
用法:
class DerivedA : public BaseT<DerivedA>
;
Base *x = new DerivedA();
Base *y = x->clone();
我没有跟踪最近 C++ 标准中的新特性...有没有办法在现代 C++ 中避免这种情况?
这个技巧从 c++98 标准开始就可以使用了。
【讨论】:
此技巧从 C++98 开始可用,如果省略了override
说明符(直到 C++11 才引入)。
Cloneable
可能是比Base
更好的名字【参考方案3】:
如果您可以控制传递多态类型的方式,请使用类型擦除。特别是,proposed std::polymorphic_value
在复制时会调用派生的复制构造函数。你可以把它想象成这样:
template <typename B>
class polymorphic_value
public:
template <typename D,
std::enable_if_t<
std::is_base_of<B, std::decay_t<D>>::value, int> = 0>
explicit polymorphic_value(D&& value)
: ptrstd::make_unique<derived_t<std::decay_t<D>>>(std::forward<D>(value))
polymorphic_value(polymorphic_value const& rhs)
: ptrrhs.ptr->clone()
B const& get() const return ptr->get();
B& get()
// Safe usage of const_cast, since the actual object is not const:
return const_cast<B&>(ptr->get());
private:
struct base_t
virtual ~base_t() = default;
virtual std::unique_ptr<B> clone() const = 0;
// With more effort, this doesn't have to be a virtual function.
// For example, rolling our own vtables would make that possible.
virtual B const& get() const = 0;
;
template <typename D>
struct derived_t final : public base_t
explicit derived_t(D const& d)
: valued
explicit derived_t(D&& d)
: valuestd::move(d)
std::unique_ptr<B> clone() const override
return std::make_unique<D>(value);
B const& get() const override
return value;
D value;
;
std::unique_ptr<base_t> ptr;
;
如需遵循提案的彻底实施,请参阅jbcoe's github repository。
示例用法:
class Base
public:
virtual ~Base() = default;
;
class Derived : public Base
public:
Derived() = default;
Derived(Derived const&);
;
int main()
polymorphic_value<Base> itDerived;
auto const copy = it;
Live on Godbolt
【讨论】:
【参考方案4】:您可能有一个类来存储多态对象以及要克隆的位置?连同你的多态对象,你可以存储一个函数指针来进行克隆:
template<class Derived>
Base* clone(const Base* b)
return new Derived(static_cast<const Derived*>(b));
void SampleUsage()
Base* b = new Derived;
Base*(*cloner)(const Base*) = clone<Derived>;
Base* copy = cloner(b);
cloner
的类型与 Derived 无关。它就像一个简化的 std::function。
【讨论】:
这其实是个好主意,也是类型擦除的基础。为了提高可用性,最好将Base*
和Base*(*)(const Base*)
函数指针包装在一个可以自动正确执行此操作的类中。【参考方案5】:
您至少可以通过从类本身的类型中获取类名来避免编写类名:
struct A: public Base
Base* Clone() return new std::remove_reference_t<decltype(*this)>(*this);
;
使用 CRTP 将无助于避免再次重复类名,因为您必须在 CRTP 基础的模板参数中写入类名。
【讨论】:
【参考方案6】:您可以使用CRTP
在派生类和实现克隆方法的基类之间添加一个附加层。
struct Base
virtual ~Base() = default;
virtual Base* clone() = 0;
;
template <typename T>
struct Base_with_clone : Base
Base* clone()
return new T(*this);
;
struct Derived : Base_with_clone<Derived> ;
【讨论】:
将类的命名从“新”转移到 CRTP 基类...有帮助吗? 您不必写“new ClassName”...而是写class Derived<CRTP_BASE<Derived>>
所以您只需将编写类名的需要从“new”语句转移到CRTP 一。有帮助吗? ;) 我提到,正如 OP 在他的问题中提到的那样,他希望避免在每个新的派生类类型中命名派生类名称。
@Klaus 可以说添加<Derived>
比在类中添加整个克隆方法要少得多。确实是品味问题,但我会说它可能会很有帮助。
同意! only 到了 OP 无法避免使用 CRTP 再次给出派生类的名称的地步。仅此而已,因为它被OP特别提及。
@Klaus 没有特别提到,OP唯一提到的是他发现在每个派生类中添加克隆方法很乏味。这提供了另一种选择。以上是关于C++多态(下)的主要内容,如果未能解决你的问题,请参考以下文章