C++类对象内存模型是一个比较抓狂的问题,主要是C++特性太多了,所以必须建立一个清晰的分析层次。一般而言,讲到C++对象,都比较容易反应到以下这个图表:
这篇文章,就以这个表格作为分析和行文的策略的纵向指导;横向上,兼以考虑无继承、单继承、多重继承及虚拟继承四方面情况,这样一来,思维层次应该算是比较清晰了。
1、C++类数据成员的内存模型
1.1 无继承情况
实验最能说明问题了,首先考虑下面一个简单的程序1:
#include<iostream>
class memtest
{
public:
memtest(int _a, double _b) : a(_a), b(_b) {}
inline void print_addr(){
std::cout<<"Address of a and b is:/n/t/t"<<&a<<"/n/t/t" <<&b<<"/n";
}
inline void print_sta_mem(){
std::cout<<"Address of static member c is:/n/t/t"<<&c<<"/n";
}
private:
int a;
double b;
static int c;
};
int memtest::c = 8;
int main()
{
memtest m(1,1.0);
std::cout<<"Address of m is : /n/t/t"<< &m<<"/n";
m.print_addr();
m.print_sta_mem();
return 0;
}
在GCC4.4.5下编译,运行,结果如下:
可以发现以下几点:
1. 非静态数据成员a的存储地址就是从类的实例在内存中的地址中(本例中均为0xbfadfc64)开始的,之后的double b也紧随其后,在内存中连续存储;
2. 对于静态数据成员c,则出现在了一个很“莫名其妙”的地址0x804a028上,与类的实例的地址看上去那是八竿子打不着;
其实不做这个测试,关于C++数据成员存储的问题也都是C++ Programmer的常识,对于非静态数据成员,一般编译器都是按其在类中声明的顺序存储,而且数据成员的起始地址就是类得实例在内存中的起始地址,这个在上面的测试中已经很明显了。对非静态数据成员的读写,我们可以这样想,其实C++程序完全可以转换成对应的C程序来编写,有一些C++编译器编译C++程序时就是这样做的。对非静态数据成员的读写也可以借助这个等价的C程序来理解。考虑下面代码段2:
// C++ code
struct foo{
public:
int get_data() const{ return data; }
void set_data(int _data){ data = _data;}
private:
int data;
};
foo f();
int d = f.get_data();
如果要你用C你会怎么实现呢?
// C code
struct foo{
int data;
};
int get_foo_data(const foo* pFoo){ return pFoo->data;}
void set_foo_data(foo* pFoo, int _data){ pFoo->data = _data;}
foo f;
f.data = 8;
foo* pF = &f;
int d = get_foo_data(pF);
在C程序中,我们要实现同样的功能,必须是要往函数的参数列表中压入一个指针作为实参。实际上C++在处理非静态数据成员的时候也是这样的,C++必须借助一个直接的或暗喻的实例指针来读写这些数据,这个指针,就是大名鼎鼎的 this指针。有了this指针,当我们要读写某个数据时,就可以借助一个简单的指针运算,即this指针的地址加上该数据成员的偏移量,就可以实现读写了。这个偏移量由C++编译器为我们计算出来。
对于静态数据成员,如果在static_mem.cpp中加入下面一条语句:
std::cout<<”Size of class memtest is : ”<<sizeof(memtest)<<”/n”;
我们得到的输出是:12。也就是说,class的大小仅仅是一个int 和一个double所占用的内存之和。这很简单,也很明显,静态数据成员没有存储在类实例的地址空间中,它被C++编译器弄到外面去了也就是程序的data segment中,因为静态数据成员不在类的实例当中,所以也就不需要this指针的帮忙了。
1.2 单继承与多重继承的情况
由于我们还没有讨论类函数成员的情况,尤其,虚函数,在这一部分我们不考虑继承中的多态问题,也就是说,这里的父类没有虚函数——虽然这在实际中几乎就是禁手。如此,我们的讨论简洁很多了。
在C++继承模型中,一个子类的内存模型可以看成就是父类的各数据成员与自己新添加的数据成员的总和。请看下面的程序段3。
class father
{
public:
// constructors destructor
// access functions
// operations
protected:
int age;
char sex;
std::string phone_number;
};
class child : public father
{
public:
// ...
protected:
std::string twitter_url; // 儿子时髦,有推号
};
这里sizeof(father)和sizeof(child)分别是12和16(GCC 4.4.5)。先看sizeof(father)吧,int占4 bytes,char占1byte,std::string再占4 bytes,系统再将char圆整到4的倍数个字节,所以一共就是12 bytes了,对于child类,由于它仅仅引入了一个std::string,所以在12的基础上加上std::string的4字节就是16字节了。
在单继承不考虑多态的情况下,数据成员的布局是很简单的。用一个图来说明,如下。
多重继承一般都被公认为C++复杂性的证据之一,但是就数据成员而言,其实也很简单,多重继承的复杂性主要是指针类型转换与环形继承链的问题,这些内容都将在第二部分讲述。
假设有下面三个类,如下面的程序段4所示,继承结构关系如图:
class A{
public:
// ...
protected:
int a;
double b;
};
class B{
public:
// ...
protected:
char c;
};
class C : public A, public B
public:
// ...
protected:
float f;
};
那么,对应的内存布局就是图4所示。
1.3 虚继承
多重继承的一个语意上的副作用就是它必须支持某种形式的共享子对象继承,所谓共享,其实就是环形继承链问题。最经典的例子就是标准库本身的iostream继承族。
class ios{...};
class istream : public ios {...};
class ostream : public ios {...};
class iostream : public istream, public ostream {...};
无论是istream还是ostream都含有一个ios类型的子对象。然而在iostream的对象布局中,我们只需要一个这样的ios子对象就可以了,由此,新语法虚拟继承就引入了。
虚拟继承中,关于对象的数据成员内存布局问题有多种策略,在Inside the C++ Object Model中提出了三种流行的策略,而且Lippman写此书的时候距今天已经很遥远了,现代编译器到底如何实现我也讲不太清楚,等哪天去翻翻GCC的实现手册再论,今天先前一笔债在这。
2、C++类函数成员的内存模型
2.1 关于C++指针类型
要理解好C++类的函数成员的内存模型,尤其是虚函数的实现机制,一定要对指针的概念非常清晰,指针是绝对的利器,无论是编写代码还是研究内部各种机制的实现机理,这是由计算机体系结构决定的。先给一段代码,标记为代码段5:
class foo{
//...
};
int a(1);
double b(2.0);
foo f = foo();
int* pa = &a;
double* pb = &b;
foo* pf = &f;
我们知道,int指针的内容是一个 表征int数据结构 的地址,foo指针的内容就是一个 表征foo数据结构 的地址。那么,系统是如何分别对待这些看上去就是0101的地址的呢?同样是一个 1000110100...10100,我怎么知道这个地址就一个int 数据结构的地址呢?它NN的拼什么就不是一个foo 数据结构的地址呢?我只有知道了它是int,我才知道应该取出从1000110100...10100开始的4个byte,对不对?
所以我就想——强调一下,我也只是在猜想——一定是指针的数据类型(比如int*,还是foo*?)里面保存了相关的信息,这些信息告诉系统,我要的是一个int,你给我取连续的4个byte出来;我要的是一个foo结构,你给我取XX个连续的byte出来…
简单地说,指针类型中包含了一个类似于 sizeof 的信息,或者其他的辅助信息——至少我们先这么来理解,至于系统到底怎么实现的,那是《编译原理》上艰深的理论和GCC浩繁的代码里黑客们的神迹了。这个sizeof的信息就告诉了系统你应该拿几个(连续)地址上的字节返回给我。例如,int* pInt的值为0xbfadfc64,那么系统根据int*这个指针的类型,就知道应该把从0xbfadfc64到0xbfadfc68的这一段内存上的数据取出来返回。
回到C++的话题上,假设下面的代码段6,其实就是前面代码段3,为了阅读的方便copy过来一下。
class father
{
public:
// constructors destructor
// access functions
// operations
protected:
int age;
char sex;
std::string phone_number;
};
class child : public father
{
public:
// ...
protected:
std::string twitter_url; // 儿子时髦,有推号
};
现在我进行下面的调用:
child c();
father* pF = &c;
child* pC = &c;
std::string tu;
tu = pF->twitter_url;// 这个调用是非法的,原因我们后面说,暂且将这一行标记为(*)
tu = pC->twitter_url;
if(child* pC1 = dynamic_cast<child*>(pF))
tu = pC1->twitter_url;
对于(*)行,其实原因就是我们前面所说的,指针类型中包含了一个类似于sizeof 的信息,或者其他的辅助信息,对比图5,我们可以这样子想,一个father类型object嵌套在了一个child类型的object里面,因为指针类型有一个sizeof的信息,这个信息决定了一个pF类型的指针只能取到12个连续字节的内容,(*)试图访问到这个12个字节之外的内容,当然也就要报错了。
我得说明一句,这样子想只是一种理解上的自由(而且我认为这样理解,从结论和效果上讲是靠谱的),到底是不是这样子,我还并没有调查清楚。
这里,我们先调查了一下指针访问类的数据成员,还没有涉及到函数成员,但其实这才是本部分的核心内容。OK,马不停蹄趁热打铁,接下来我们就说这个故事。
2.2 静态函数成员
与静态数据成员一样,静态函数成员从实现的角度上讲,最大的特点就是编译器在处理静态函数成员的时候不会讲一个this指针压入其参数列表,回顾代码段2,一般的成员函数都会压入一个this到参数列表的。这个实现的不同,决定了静态函数成员们许多不一样的特性。
如果取一个静态函数成员的地址,获得的就是其在内存中的地址,由于它们没有this指针,所以其地址类型并不是一个指向类成员函数的特别的指针。
也由于没有了this指针这一本质特点,静态函数成员有了以下的语法特点:
l 它不能直接读写class内的非静态成员,无论是数据成员还是函数成员;
l 它不能声明为const或是virtual;
l 它不是由类的实例来调用的,而是类作用域界定符;
这里,我想起了《大学》上一段话:物有本末,事有终始,知所先后,则近道矣”,这话太TMD妙了,凡事入乎其内,外面的什么东西都是浮云,就像《越狱》里的Micheal看到一面墙就想得到里面的钢筋螺丝,这时候这面墙已经不是一面墙了。如果只是生硬地去记忆上面那些东西,那是何其痛苦的事情,也几乎不可能,但是一旦“入乎其内”了,这些东西就真的很简单了。
静态函数成员的特点赋予了它一些有趣的应用场合,比如它可以成为一个回调函数,MFC大量应用了这一点;它也可以成功地应用线程函数身上。
2.3 非静态函数成员
还是可以回到代码段3,其实这个代码段已经给出了非静态成员函数的实现机制。
1. 改写非静态成员函数的函数原型,压入一个额外的this指针到成员函数的参数列表中,目的就是提供一个访问类的实例的非静态数据/函数成员的渠道;
2. 将每一个对非静态数据/函数成员的读写操作改为经由this指针来读写;
3. 最惊讶的一步是,将成员函数改写为一个外部函数——Gotcha!这就是为什么sizeof(Class)的时候不会将非虚函数地址指针计算进去的原因,因为(非静态)成员函数都被搬到类的外面去了,并借助Name Mangling算法将函数名转化为一个全局唯一的名字。
对于第3点,有一个明显的好处就是,对类成员函数的调用就和一般的函数调用几乎没任何开销上的差异,几乎从C++投胎开始,效率就成为了C++的极致追求之一。
分享到:
相关推荐
### 深度探索C++对象模型:理解与解析 #### C++对象模型概览 C++对象模型是C++编程语言中一个核心且复杂的概念,它定义了如何在内存中表示类、对象以及它们之间的关系。理解C++对象模型对于深入掌握C++语言特性、...
《深度探索C++对象模型》专注于C++面向对象程序设计的底层机制,包括结构式语意、临时性对象的生成、封装、继承,以及虚拟——虚拟函数和虚拟继承。这本书让你知道:一旦你能够了解底层实现模型,你的程序代码将获得...
《深度探索C++对象模型》是一本专注于C++底层机制的专著,主要针对2012年的标准进行深入解析。C++是一种多范式、静态类型、编译型、并发型、通用程序设计语言,它以其强大的功能和灵活性而闻名。这本书的目标是帮助...
《深度探索C++对象模型》是一本专门为C++程序员量身打造的专业书籍,它深入剖析了C++语言的核心——对象模型。这本书旨在帮助开发者更好地理解C++中的内存管理、类型系统、类层次结构以及对象生命周期等关键概念。...
《深度探索C++对象模型》是侯捷翻译的一本关于C++对象模型深入探讨的专业书籍,原著作者为Stanley B. Lippman。这本书详细解析了C++中对象的内部表示、构造和析构的过程以及运行时行为等关键概念。它不仅仅是一本...
在探索C++对象模型之前,首先需要理解对象模型(Object Model)的含义。对象模型是面向对象编程(Object-Oriented Programming,OOP)中的一个核心概念,它描述了对象的结构、属性、方法、以及对象之间的关系。C++...
《深入探索C++对象模型》是一本深度剖析C++编程语言内部机制的著作,而设计模式则是软件工程中的一种最佳实践,是解决常见问题的模板。这两者结合在一起,为开发者提供了理解C++如何实现面向对象特性以及如何高效地...
深度探索C++对象模型 超高清
深度探索c++对象模型.pdf Inside The C++ Object Model专注于C++对象导向程序设计的底层机制,包括结构式语意、暂时性对象的生成、封装、继承,以及虚拟——虚拟函数和虚拟继承。这本书让你知道:一旦你能够了解...
C++对象模型 第1章 关于对象 第2章 构造函数语意学 第3章 Data语意学 第4章 Function语意学 第5章 构造、析构、拷贝语意学 第6章 执行期语意学 第7章 站在对象模型的尖端 第8章 C++对象模型总结 8.1 C++对象模型 8.2...
深度探索C++对象模型
C++ 对象内存模型 C++ 对象内存模型是 C++ 编程语言中一个重要的概念, 它描述了 C++ 对象在内存中的存储结构。这个模型是 C++ 编程语言的基础之一,对于理解 C++ 编程语言的工作机理具有重要的意义。 在 C++ 中,...
深度探索C++对象模型 中文图片影印版pdf,比较清晰,不是那种模糊的版本,和文字版差别不大 英文清晰文字版chm 第一代C++编译器开发主管所写。如果你想成为真正的C++高手,看这本书,他为你讲述了编译器在处理各种...
"C++对象模型详解[收集].pdf" 这是一个关于C++对象模型的详细解释,涵盖了C++类继承内存布局、成员变量和成员函数的访问、虚继承、虚函数调用、强制转换到基类、异常处理等知识点。 首先,文章介绍了为什么需要...
《侯捷讲座:C++对象模型》是一份深入解析C++内部对象模型的高质量学习资料。这份PDF高清版教程由知名C++专家侯捷主讲,旨在帮助开发者深入理解C++语言的核心机制,特别是对象模型的构建和运作原理。通过这份资源,...
深度探索C++对象模型 第0章 导读(译者的话) 第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple...
《深度探索C++对象模型》这本书,由Stanley B. Lippman撰写,侯捷翻译,由华中科技大学出版社出版。本书致力于深入解析C++编译器在处理C++代码时所采取的复杂对象模型及其背后的底层机制,特别是针对构造函数、解构...