zoukankan      html  css  js  c++  java
  • C++对象模型简析

    最近在看林锐的《高质量程序设计指南——C/C++语言》,在阅读的过程中重温了C++对象的内存分布,在这篇文章里予以总结。

    非多态类的内存映像

    先看看最基本的非多态类的内存分布,我们这里展示了一个Rectangle类:

    class Rectangle {
    public:
    	Rectangle(): m_length(1), m_width(1) {}
    	~Rectangle() {}
    	float GetLength()const {return m_length;}
    	void SetLength(float length) {m_length = length;}
    	float GetWidth()const {return m_width;}
    	void SetWidth(float width) { m_width = width;}
    	void Draw() {...}
    	static unsigned int GetCount() {return m_count;}
    protected:
    	Rectangle(const Rectangle& copy) {...}
    	Rectangle& operator=(const Rectangle& assign) {...}
    private:
    	float m_length;
    	float m_width;
    	static unsigned int m_count;
    };
    

    我们知道,一段程序的内存大致分为代码段、堆栈和静态数据段等区域。这个Rectangle类的所有函数均存储在代码段中。m_count是一个静态变量,因此存储在静态数据段中。用户创建的Rectangle对象,包括它们的m_length,m_width等成员则存储在堆栈段中(书中称之为用户内存区)。
    Rectangle的内存映像如下所示:

    多态类的内存映像

    我们假设Rectangle派生自抽象基类Shape,Shape有一个属性m_color,并且把Draw()移植到Shape中作为纯虚函数,代码如下所示:

    class Shape {
    public:
    	Shape():m_color(0) {}
    	virtual ~Shape() {}
    	float GetColor()const {return m_color;}
    	void SetColor(float color) {m_color = color;}
    	virtual void Draw() = 0;
    private:
    	float m_color;
    };
    
    class Rectangle: public Shape {
    	...
    };
    

    Rectangle作为派生类,它会继承基类的非静态成员,并作为自己对象的专用数据成员。同时,编译器会为每一个多态类创建一个虚函数指针数组vtable,该类的所有虚函数地址都保存在这张表里。
    多态类的每一个对象中会有一个指针成员vptr,它是一个指向函数指针的指针,指向当前类型所属的vtable。一般来说,vptr会放置在所有数据成员的最前面。
    为了支持RTTI,编译器会为每一个多态类创建一个type_info对象,记录该类的类型信息,并把其地址保存在vtable的固定位置(一般是第一个位置)。
    按照上面的论述,Rectangle的内存映像如下图所示:

    从上图可以看到,派生类的内存构造顺序是按照基类的继承顺序,统一新增在基类子对象的前面的,因此从一个派生对象入手,就可以直接访问到基类的数据成员(因为基类的数据成员被直接嵌入到了派生类对象中)。
    访问虚函数的时候,因为要通过vptr间接寻址,因此增加了一层间接性,由此带来了一些额外的运行开销。

    虚函数表的运行时访问实现

    不同于调用普通的函数,如果一个虚函数被调用,那么我们要到运行时才能知道运行的到底是哪一个类的虚函数。具体是怎么实现这样的效果的呢?书中给出了一种猜测。
    因为C++中的数组只能存储一种类型,而虚函数指针的类型是各种各样的,因此编译器定义了一个统一的函数指针类型作为vtable的元素类型:

    typedef void (*PVFN) (void);
    typedef struct{
    type_info * _pTypeInfo;
    PVFN        _arrayOfPvfn[];
    };
    

    所有的虚函数指针都会被强制转换成PVFN后存储在vtable中。
    同时,记录下vtable中每一个元素中虚函数指针的实际类型:

    typedef void (*PVFN_Draw)(void);
    typedef void (*PVFN_~Shape)(void);
    

    真正在调用虚函数时,从vtable中取出的类型只是PVFN,还需要做一个反向的强制转换:

    (*(PVFN_Draw)(pShape->_vptr[2]))(pShape);   //pShape->Draw();
    (*(PVFN_~Shape)(pShape->_vptr[1]))(pShape);   //delete pShape;
    

    在这个过程中并没有直接在代码中调用Rectangle::Draw,因为pShape的静态类型虽然是Shape*,但是它实际上指向一个Rectangle对象,而该对象的vptr成员指向Rectangle::_vtable,这个vtable中存储的是Rectangle改写过的虚函数或新加的虚函数的地址,而不是Shape的虚函数的地址,因此实现了对Rectangle::Draw的调用。

    对于vtable中虚函数指针的排列顺序,大致遵循的规律是:
    1.一个虚函数如果再当前类中是第一次出现,则将其地址插入到该类的每一个vtable的底部
    2.如果派生类改写了基类的虚函数,则这个函数的地址在派生类vtable中的位置与它在基类vtable中的位置一致,与它在派生类中声明的顺序无关
    3.派生类没有改写的基类虚函数被继承下来并插入派生类的vtable中,因此派生类的vtable布局是和基类的兼容的

    比如下面的代码,我们这么定义Shape和Rectangle类:

    class Shape {
    public:
    	Shape():m_color(0) {}
    	virtual ~Shape() {}
    	float GetColor()const {return m_color;}
    	void SetColor(float color) {m_color = color;}
    	virtual void Draw() = 0;
    private:
    	float m_color;
    };
    
    class Rectangle {
    public:
    	Rectangle(): m_length(1), m_width(1) {}
    	~Rectangle() {}
    	virtual int ObliqueAngle() {return 90;}
    	virtual void Draw() {...}
    	virtual ~Rectangle() {}
    	static unsigned int GetCount() {return m_count;}
    protected:
    	Rectangle(const Rectangle& copy) {...}
    	Rectangle& operator=(const Rectangle& assign) {...}
    private:
    	float m_length;
    	float m_width;
    	static unsigned int m_count;
    };
    

    那么它们的虚函数表如下所示:

  • 相关阅读:
    爬虫基础简介
    父子分类与无限分类
    Flask路由层
    Flask基础简介
    DRF之JWT签发,认证,群查
    DRF之JWT简介
    DRF之认证组件
    软件众包外包平台汇总
    Python与机器视觉(x)windows下import cv2报错dll
    【今日CV 视觉论文速览】Fri, 8 Feb 2019
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/13959791.html
Copyright © 2011-2022 走看看