“迷人”的多态

    不得不说C++中的多态是又迷惑人,又吸引人;“迷人”又“迷人”。以下内容来自个人理解,不对请指正。

三个简单的例子(Three toy examples)

    在C++类继承中有三个指针:this指针、虚基表指针(vbptr)、虚表指针(vtptr)。这里先不讨论虚继承产生的虚基表指针,先看看this指针和虚表指针如何实现多态的。

示例一

    c++中类成员变量和类成员函数是分开的,多个类对象共享同一个类成员函数,this指针是在调用类成员函数时用以区分调用的是哪个类对象。this指针隐式的蕴含在成员函数的第一个位置。即

#include <iostream>
using namespace std;

class ex1{
private:
    int x;
public:
    ex1(int _x) : x(_x) {}

    void print(){
        cout << x << endl;
    }
};

int main(){
    ex1 e1(2);
    e1.print();

    ex1* e2 = new ex1(2);
    e2->print();

    return 0;
}

    上述例子在调用print时实际上隐式的传入了this指针,可以翻译成如下代码

#include <iostream>
using namespace std;

struct ex1{
    int x;
};

void create(ex1* const this){
    this->x = 0;
}

void create(ex1* const this, int _x){
    this->x = _x;
}

void print(ex1* const this){
    cout << this->x << endl;
}

int main(){
    ex1 e1;
    create(&e1, 2);
    print(&e1);

    ex1* e2;
    create(e2, 2);
    print(e2);

    return 0;
}

也就是说类的成员变量会先隐式的接收一个指向类成员数据的指针(从实例化对象的大小来看,类大小不包括函数的大小,只有数据成员对齐的大小)。

示例二

    看完this指针,再看看虚表指针实现多态的方式。

#include <iostream>
using namespace std;

class base{
public:
    base(){}
    virtual void func1() { cout << typeid(*this).name() << endl; }
};

class derived : public base{
public:
    int x;
    derived(int _x) : x(_x) {}
    void func1() { 
        cout << typeid(*this).name() << endl;
        cout << x << endl;
    }
};

int main(){
    base* b = new derived(2);
    b->func1();

    derived* d = new derived(2);
    d->func1();

    return 0;
}

输出结果如下

class derived
2
class derived
2

    这就是C++中的多态,虽然b是基类类型,但是调用的的却是派生类的函数。那是如何实现的呢?先从简单的指针d来看。

对象*d包括一个虚表指针,和一个数据。并且虚表指针在对象的起始位置,换句话说,d指向的内存的前8个字节就是虚表指针。然后再看看是如何调用fun1的

首先,将d放入rax寄存器,d是什么?d是我们new出来的对象地址;然后,从rax内存中读取8个字节,读入的是什么?就是上面0x00000243645b51f0内存中前8个字节,就是虚表指针;然后将d读入rcx,为什么?前面示例一说了,在调用类成员函数时隐式的传递参数this指针,而this指针指向这个对象,也就是与d的值相同,rcx就是调用函数的传参寄存器;最后,call [rax],因为*d只有一个虚函数func1,所以位于虚表的前8个字节,所以这句话就是调用func1。

    再看看b->func1()是如何调用的,首先b指向的内存如下:

这里图片没有放错,b指向的内存就是上面这个图,你会发现这个内存存储的东西和d指向内存的内容一样,然后看看如何调用func1

和d->func1()一样,b在调用func1的时候,也是先读取虚表指针,然后,再将this指针放入寄存器,调用function。所以,多态实现的根本原理是,当我们用派生类对象转换成基类对象指针的时候,可以理解成将派生类虚基表指针赋给基类虚基表指针,这样,当基类调用虚函数时实际上用的是派生类的虚表。

    有没有一个大胆的想法,既然基类的虚表指针指的是派生类的虚基表,那是不是基类可以直接调用派生类的虚函数,即修改派生类

class derived : public base{
public:
	int x;
	derived(int _x) : x(_x) {}
	void func1() final {
		cout << typeid(*this).name() << endl;
		cout << x << endl;
	}

	virtual void func2() { cout << "func2()" << endl; }
};

然后执行

int main() {
	base* b = new derived(2);
	b->func1();
	b->func2();

	return 0;
}

结果是不可以,因为编译器发现b是指向base的指针,而这个对象根本没有func2方法,自然不能调用。不过,秉持着虚表就在那,用户具有读入权限,编译器不让调用,那就内联汇编(你编译器说不让调用就不调用了,偏要调用)。

整了半天,想要内联汇编,一定要注意环境,作者用的是vs2022 community编译器,在window环境下,首先,要设置工程build dependecies

然后选择

再然后

选择x86平台,好像64位不支持内联汇编

最后,把上面看到的调用func1的方式迁移,修改一下call的位置

int main() {
	bbase* b = new dderived(2);

	__asm mov eax, [b];
	__asm mov eax, [eax];
	__asm mov ecx, [b];
	__asm call[eax+4];

	return 0;
}

编译运行就能看到打印出

func2()

所以,当然可以骗过编译器用基类指针调用派生类虚表中的函数,但是千万不要这么做!!偷偷写着玩就行了。

示例三

    再看看第三个例子

#include <iostream>
using namespace std;

class base{
public:
    base(){}
    virtual void func1() { cout << typeid(*this).name() << endl; }
};

class derived : public base{
public:
    int x;
    derived(int _x) : x(_x) {}
    void func1() { 
        cout << typeid(*this).name() << endl;
        cout << x << endl;
    }
};

int main(){
    base* b = new derived(2);
    derived* d = static_cast<derived*>(b);
    cout << d->x << endl;

    return 0;
}

输出结果为

2

 这个似乎没啥说的,但是仔细想想,难道只要把指针类型转换了就可以,那如果是这样的呢

int main(){
    base* b = new base();
    derived* d = static_cast<derived*>(b);
    cout << d->x << endl;

    return 0;
}

你会发现这个代码也能执行,而且不报错,只是读出的结果很抽象。

总结

    上述三个例子已经跑完了,那么关于多态为什么只能用指针实现?在c++中,指针存储一个对象的地址,同时指针还有指向的类型,但是如果我们从cpu执行的汇编指令来看,汇编中可以操作比特、一个字、两个字、四个字、甚至八个字,但是一个对象可能占用十几甚至更多的内存,cpu处理的时候依然是一个字节或者等等处理,指针的类型是编译器在将高级语言翻译成机器语言的时候判断要读取4字节还是8字节,类型是对编译器而言的。上面那个例子

base* b = new derived(2);
cout << b->x << endl;

编译器不允许,因为b指针是base类型,class base中没有x这个对象,所以这个不被编译器允许,但是如果你看b的内存指向,前8个字节是虚表指针,紧随的后4个字节是x数值。x就在b指向的内存里,却不能读出来,因为编译器确定class base只占8个字节,这8个字节存储的是虚表指针,编译器知道这8个字节是虚表指针,但是并不知道这个虚表指针指向的是基类还是派生类,所以多态,就是因为派生类把自己的虚表地址给了基类的虚表指针,编译器调用虚函数,通过虚表指针寻找位置(这里你一定会有这样的疑问,派生了的虚表顺序万一不和基类一样,如何确定函数的相对偏移量,实际上,派生类的虚表是拷贝基类的虚表,然后派生类重写的函数会替换原虚表的函数地址,派生类新增的虚函数会添加到虚表尾部,这成了派生类的虚表)。

二、一些例子

示例一

  可是只有虚表指针还不够,还记得前面示例一的this指针,下面这个示例来自C++中的this指针 | 基类与派生类中隐含的指针,仔细分析一下this指针是如何确定是哪个对象的。

#include<iostream>

using namespace std;

class Parent{
public:
    int x;
    Parent *p;
public:
    Parent(){}
    Parent(int x){
		this->x=x; 
		p=this;
	}
	virtual void f(){
        cout << typeid(*this).name() << endl;
		cout<<"Parent::f()"<<endl; 
	}
	void g(){
        cout << typeid(*this).name() << endl;
		cout<<"Parent::g()"<<endl;
	}
    
	void h(){
        cout << typeid(*this).name() << endl;
		cout<<"Parent::h()"<<endl;
		f();
		g();
		y();
		cout<<"LOOK HERE: "<<x<<endl;
	}
        
private:
	void y(){
        cout << typeid(*this).name() << endl;
		cout<<"Parent::y()"<<endl;
	}
};

class Child : public Parent{
public:
    int x;
        
public:
    Child(){}
    Child(int x) : Parent(x+5){//正确的调用父类构造函数要写在初始化型参列表中 
            
        //这样企图调用父类的构造函数是错误的,因为这是另外创造了一个临时对象,函数结束之后就什么都没有了! 
        //Parent(x+5);
        this->x=x;
    }
    void f(){
        cout << typeid(*this).name() << endl;
        cout<<"Child f()"<<endl;
    }
    void g(){
        cout << typeid(*this).name() << endl;
        cout<<"Child g()"<<endl; 
    }
};

int main(){
    //例一 
    Child *ch=new Child();
    ch->h(); 
    cout<<endl;
    //例二: 
    ch=new Child(5);
    ch->h();
    cout<<endl;
    
    //例三: 
    ch->p->h();
	delete ch;

	system("pause");
    return 0;
} 

示例结果为

为什么会是这个结果,示例一,

ch->h(); 

相当于在调用

h(ch);

而h()是基类里定义的,其参数如下

void h(Parent* const this)

就是说,传入的ch指针隐式的转换成了Parent* const类型,接着执行

f();  等价于执行this->f(this)

而f函数是虚函数,是通过虚表调用的,此时this指针内的虚表是Child的,所以,根据虚表调用,调用的是child的f()函数,而f函数原型是

void f(Child* const this)

指针又隐式的转换为child类型,f函数执行完,继续执行g(),而这个翻译过来是

this->g(this)

这个this依然是Parent类型的this,并且g函数不是虚函数,所以直接根据this类型调用g函数,所以调用的是parent的g函数,而y函数是private函数,this指针直接调用(这里是不是类外那种不能访问private函数,关于private权限可以搜搜其他资料)。最后输出的x是

this->x

这个this依然是parent类型,所以访问的是Parent的x。

所以看到了吗,this在示例三中,我们强制做类型换去访问x,而实际在继承过程中,this指针在基类和派生类隐式互相转换。所以,派生类调用基类方法是通过this指针隐式转换成基类类型,而之所以可以这么转换,是因为派生类包括整个基类(即使基类private派生类无法访问,但是派生类是包含基类的这些东西),而基类类型调用派生类方法是通过派生类虚表指针传递给基类虚表指针实现的。

示例二

    到这里,已经知道this指针和虚表指针,那么看看一个经典的框架,装饰器模式,代码来自装饰者模式

// 定义一个抽象组件,表示饮料
class Beverage {
public:
    // 返回饮料的描述
    virtual std::string getDescription() = 0;
    // 返回饮料的价格
    virtual double cost() = 0;
    // 虚析构函数,方便子类析构
    virtual ~Beverage() {}
};

// 定义一个具体组件,表示咖啡
class Coffee : public Beverage {
public:
    std::string getDescription() override {
        return "咖啡";
    }

    double cost() override {
        return 10.0;
    }
};

// 定义一个具体组件,表示茶
class Tea : public Beverage {
public:
    std::string getDescription() override {
        return "茶";
    }

    double cost() override {
        return 5.0;
    }
};

// 定义一个抽象装饰者,表示调料
class CondimentDecorator : public Beverage {
protected:
    // 持有一个被装饰者的指针
    Beverage* beverage;

public:
    CondimentDecorator(Beverage* beverage) : beverage(beverage) {}

    // 返回调料和饮料的描述
    std::string getDescription() override {
        return beverage->getDescription() + " + " + typeid(*this).name();
    }

    // 虚析构函数,释放被装饰者的内存
    virtual ~CondimentDecorator() {
        delete beverage;
    }
};

// 定义一个具体装饰者,表示糖
class Sugar : public CondimentDecorator {
public:
    Sugar(Beverage* beverage) : CondimentDecorator(beverage) {}

    // 返回糖和饮料的价格
    double cost() override {
        return beverage->cost() + 1.0;
    }
};

// 定义一个具体装饰者,表示奶油
class Cream : public CondimentDecorator {
public:
    Cream(Beverage* beverage) : CondimentDecorator(beverage) {}

    // 返回奶油和饮料的价格
    double cost() override {
        return beverage->cost() + 2.0;
    }
};
// 测试代码
int main() {
    // 创建一个咖啡对象
    Beverage* coffee = new Coffee();
    std::cout << coffee->getDescription() << " = " << coffee->cost() << std::endl;

    // 给咖啡加糖和奶油
    coffee = new Sugar(coffee);
    coffee = new Cream(coffee);
    std::cout << coffee->getDescription() << " = " << coffee->cost() << std::endl;

    // 创建一个茶对象
    Beverage* tea = new Tea();
    std::cout << tea->getDescription() << " = " << tea->cost() << std::endl;

    // 给茶加糖
    tea = new Sugar(tea);
    std::cout << tea->getDescription() << " = " << tea->cost() << std::endl;

    // 释放内存
    delete coffee;
    delete tea;

    return 0;
}

看看真正的多态!!!!

以上内容来自个人理解,不对请指正

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值