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,但是不构成重写



Func4函数并没有进入虚表中


通过查询内存发现,虚表指针中存在三个地址,而其中两个正好为监视中的两个地址
猜测 0x00c4146a 就是Func4的地址

用程序打印虚表

虚表本质是一个函数指针数组

VS中在数组最后放了一个nullptr,这样就可以解决在不同虚表中的个数不同的问题


typedef一个函数指针 为VF_PTR


正常来说 要写成将VF_PTR放在后面

但是由于函数指针的特殊性,定义变量都要放在中间


如何寻找到虚表地址

想要虚表的地址,就可以通过找到虚表的指针
而这个虚表指针在对象中,这个指针在对象的前4个(32位)或者8个字节(64位)上面


以32位为例,如何取对象的前4个字节

强制转换为int*


* (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个字节


运行程序打印虚表,确实了解到多了一个地址



把虚表的地址拿出来赋给函数指针,用函数指针去调用函数
这里发现 监视中没有出现的地址确实是Func4函数的地址

虚表存在哪里?

由于常量区地址与虚表的地址最为接近,所以说明虚表在常量区/代码段上

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;

寻找虚表地址

Derive 作为Base1 和Base2的子类,所以Derive内部有两张虚表


正常来说,Derive内部还存在一个func3函数,这个函数放在哪里了呢?
借助打印虚表来查看,这里的打印虚表依旧可以使用单继承中的那个



base1的虚表指针 正好在对象的前4个字节处,直接可以使用求出虚表指针 去指向base1的虚表


方法1 : base2的虚表指针 需要加上base1的大小

但是这里要注意一个问题,若写成 PrintVFTable((VF_PTR*)(int)( &d+ sizeof(Base1) ) )
写的并不对,d本身是一个Derive类型,&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小
而该设计想要每次+1跳转1个字节,所以需要强制转换char*


方法2 :切片自动偏移




两种方法的结果都是一样的

注意事项

多继承派生类增加的的虚函数在第一个虚表中

多继承重写后的func1的地为什么地址不同?


ptr1调用函数——一次jmp

找到 Base1虚表里的地址 0x00e21230 ,再call这个地址
只需要jmp一次 就可以找到实际真正执行的函数地址
ptr1调用地址属于正常调用


ptr2 调用函数——多次jmp

ptr2调用地址,需要 多次jmp 才能找到真正的函数地址

ecx存的是this指针
ecx,8 目的是修正this指针的位置
最终Base1和Base2都是执行同一个函数的指令


  • 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&lt;Base&gt; @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&lt;T&gt; 接口的上下文中听到这一点。由于 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&lt;CRTP_BASE&lt;Derived&gt;&gt; 所以您只需将编写类名的需要从“new”语句转移到CRTP 一。有帮助吗? ;) 我提到,正如 OP 在他的问题中提到的那样,他希望避免在每个新的派生类类型中命名派生类名称。 @Klaus 可以说添加&lt;Derived&gt; 比在类中添加整个克隆方法要少得多。确实是品味问题,但我会说它可能会很有帮助。 同意! only 到了 OP 无法避免使用 CRTP 再次给出派生类的名称的地步。仅此而已,因为它被OP特别提及。 @Klaus 没有特别提到,OP唯一提到的是他发现在每个派生类中添加克隆方法很乏味。这提供了另一种选择。

以上是关于C++多态(下)的主要内容,如果未能解决你的问题,请参考以下文章

多态性? C++ 与 Java

Visual C++和C++的区别

接口讲解

JAVA和C++区别

类 C 语言中的返回类型多态性

C++编译期多态与运行期多态