C++对象模型可以概括为以下2部分:
1. 语言中直接支持面向对象程序设计的部分
2. 对于各种支持的底层实现机制
语言中直接支持面向对象程序设计的部分,如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等第一部分这里我简单过一下,重点在底层实现机制。
在c语言中,“数据”和“处理数据的 *** 作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。在c++中,通过抽象数据类型(abstract data type,ADT),在类中定义数据和函数,来实现数据和函数直接的绑定。
概括来说,在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。
class base { public: base(int); virtual ~base(void); int getIbase() const; static int instanceCount(); virtual void print() const; protected: int ibase; static int count; };
基本C++对象模型:
在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。
简单对象模型(a simple object model):
所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。
表格对象模型(a table-driven object model)
这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。
二、C++对象模型这个模型从结合上面2中模型的特点,并对内存存取和空间进行了优化。在此模型中,non static 数据成员被放置到对象内部,static数据成员, static and nonstatic 函数成员均被放到对象之外。对于虚函数的支持则分两步完成:
1. 每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。
2. 每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。
这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。
使用vs 查看c++类的内存布局:CSDN
可以看出,base类的对象一共占用八个字节,偏移地址0开始的四个字节存放的是vfptr指针,它指向的虚函数表,表的内容即base::$vftable@,其中:
0 | &base::{dtor}
1 | &base::print
表示虚函数的地址。
偏移地址4开始的四个字节存放 int类型的ibase;类中的静态成员变量static int 不占用类对象的内存空间;成员函数也不占用类对象的空间;
空类#includeclass Empty { }; int main() { std::cout << "sizeof(Empty) = " << sizeof(Empty) << std::endl; return 0; }
输出:
通过vs查看内存布局:
可以看出不存在虚函数的空类在window msvc编译器中占用一个字节:C++规定空类对象大小至少为1字节,只是为了区分示例化对象。例如创建了多个空间的对象,可以通过对象的内存空间区分。
影响C++对象大小的三个要素:非静态数据成员、虚函数和字节对齐一个实例对象中包含非静态数据成员、虚表指针以及为对齐而必需的填充。静态成员变量、成员函数以及静态成员函数独立于单个实例化对象。
其中非静态数据成员占用C++类对象的空间,如果存在虚函数,在C++类对象中则存在虚函数指针,字节对齐也会影响C++对象的大小:
class base { public: base(int); virtual ~base(void); int getIbase() const; static int instanceCount(); virtual void print() const; protected: int ibase; char ch; static int count; };
在刚刚的base类中增加了一个char类型之后,msvc编译器32位系统下占用的内存空间为12:
关于字节对齐的相关知识:C++中字节对齐问题_Benjamin的博客-CSDN博客
数据成员的声明顺序和内存布局class base { public: protected: private: int a; int b; int c; };
在相同访问控制级别(public protected private)的非静态数据成员在类对象的内存中是按照声明的先后顺序分布的:
不同访问控制级别的非静态数据成员,为规定内存分配顺序;实际上,编译器是按照声明顺序来安排内存的:
class base { public: int a; protected: int c; private: int b; };单继承的对象布局(非多态)
class base { public: protected: private: int a; int b; int c; }; class Derive:public base { private: int d; };
可以看出:Derive继承base,在Derive类的实例化对象中,从偏移地址0开始先存放继承的基类base的数据成员:a b c,然后再存放自己的数据成员:d ;
当然,在上面的例子中不存在内存对齐,如果存在内存对齐的情况下:
class base { public: protected: private: int a; int b; short c; }; class Derive:public base { private: short d; };
其中,short占两个字节。
可以看书,首先存放基类base的数据成员,当时base类存在内存对齐,需要填充两个字节;在base类完成内存对齐后,再存放Derive的数据成员d,然后再完成内存对齐;
为什么不将两个short类型的c,d以此存放,这样就不存在内存对齐;而是先存放基类的数据成员在完成基类的数据成员字节对齐后再存放派生类的数据成员呢?
将一个“大”的对象赋值给“小”的对象时,会引起对象的切割,将“大”的对象的中小对象部分赋值给小的对象。c++语言保证出现在derived class中的base class suboject有其完整原样性;
即:
int main() { base * b1 = new base; base * b2 = new Derive; b1 = b2; return 0; }
在main中,将b2赋值给b1 ,只有Derive中的base部分和base类对象实例内存排放方式相同时,才能安全的赋值,如果是下面的情况,将导致意想不到的情况发生:
多继承与多继承的对象布局(非多态)class base { public: protected: private: int a; int b; short c; }; class Interface { public: char name; }; class Derive:public base,public Interface { private: short d; };
内存排布按照继承的顺序,且需要满足字节对齐
多态下的内存布局C++虚函数与静态、动态绑定_LIJIWEI0611的博客-CSDN博客
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)