1. C++程序设计模型支持的三种程序设计模型:
1.1. 程序模型(procedural model、OP)可以理解为过程化模型就像C一样
char boy[] = "Tom";
char* p_son;
...
p_son = new char[strlen(boy) + 1];
strcpy(p_son,boy);
...
if(!strcmp(p_son,boy))
{
take_to_disneyland(boy);
}
可以看出面向过程模型就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,如上图代码所示,对字符串的处理分为申请内存、复制数据,比较数据差异几个步骤,每个步骤都需要手动调用C库函数,且步骤的顺序是固定的,可以把面向过程理解为自顶向下的编程,其特点为代码容易理解,主要是算法+数据结构,确定也显而易见,对于复杂系统,难以复用
1.2. 抽象数据类型模型(abstract data type model、ADT) 可以理解为非多态的数据封装模型,和OO相比拥有更快的速度而且空间更紧凑(因为不需要virtual)。例如CString ,string 类
string boy = "Tom";
string son;
...
//string::operator=()
son = boy;
...
//string::operator==()
if(son == boy)
take_to_disneyland(boy);
可以看到ADT模型这里对字符串进行了封装,并对外提供了一系列接口,这便是抽象的过程,对字符串的处理不再需要手动地去使用C库函数,取而代之的是一系列诸如“=” ,“==”的针对字符串类的抽象符号重载,这便是高度实物的抽象化,可复用性明显增强了
1.3. 面向对象模型(object-oriented model、OO)
void check_in(Library_materials* p_mat)
{
if(p_mat->late())
p_mat->fine();
p_mat->check_in();
if(Lender* p_lend = p_mat->reserved())
p_mat->notify(p_lend);
}
Library_materials即为抽象的base class(用以提供共通接口),真正的subtype例如Book、Video、Laptop等等都可以从它派生而来,其特点为集齐了封装、抽象、继承、多态特点,适用大型复杂系统且易复用
2. C++对于程序模型的设计
早期的C++对象模型设计并不是一蹴而就的,而是经过一系列的对比选择、优化等流程,首先我们来看下面一个例子来看几种可能的模型,对于class Point类
class Point
{
public:
Point(float val);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream& os) const;
float m_x;
static int m_iPointCount;
}
Point存在static 、nonstatic data member;static 、nonstatic 、virtual function member,其在运行时会是怎样的表现呢,编译器如何模塑出这个类的data member呢和function members呢,首先是一种较为简单的实现
2.1. 简单对象模型(A Simple Object Model)
简单对象模型在内存上的结构十分简洁,一个类的成员函数和成员变量都有自己的一个slot(可以理解为槽),每一个slot相当于一个指针,指向对应的函数或变量,内存连续分布,这种设计可以让编译器做更少的事,且类的内存存放的都是同一种类型,因此也较容易管理(类中的成员可以用slot的索引值来寻址,指针大小*索引),但是显而易见的是,空间和执行期的效率较低,毕竟函数在内存布局中也占有一个槽,且成员变量的每次访问都需要经由slot去跳转地址访问,这个模型没有应用到C++实际的编译器产品中,但是其根据slot索引去寻址的观念,被应用到C++的"指向成员的指针"观念中。
class CDerive{
public :
int m_iBase, m_iDervie;
}
//pmInt指针为数据成员指针,可以指向CDerive对象的所有int数据成
//员 这里会给指针赋值4,即m_iDervie在class中的偏移量,这其实跟slot索引去寻址如出一辙
int (CDerive::*pmInt) = &CDerive::m_iDervie;
002A1730 mov dword ptr [pmInt],4
CDerive Dervie;
002A1737 lea ecx,[Dervie]
002A173A call CDerive::CDerive (02A10A0h)
002A173F mov dword ptr [ebp-4],0
Dervie.*pmInt = 8;
002A1746 mov eax,dword ptr [pmInt]
002A1749 mov dword ptr Dervie[eax],8
2.2. 表格驱动对象模型(A Table-driven Object Model)
简单对象模型实际上每个类的大小是不固定的(每个类的成员变量跟函数数量不确定,slot数量不确定),为了将所有的类的所有对象都有一致的内存表达方式,表格驱动对象模型出现了,类本身只含有两个指针(可以理解为slot),一个指针指向数据表,一个指向函数表,数据表本身包含数据,函数表则包含多个slot,每个slot指向对应的成员函数,这个模型也没有实际应用到C++编译器上(成员变量访问效率还是低,且对于类的成员函数界定不明确),但是成员函数表这个观念却成为C++实现虚函数机制的一个有效方案。
2.3. C++对象模型(The C++ Object Model)
Stroustrup(C++之父)当初设计的C++对象模型是从简单对象模型派生而来的,空间上采取了折中的方法,非静态成员变量直接存储在类对象内存中,静态成员变量及静态、非静态非虚函数放在类对象内存之外,即不会在内存布局中存放指向函数的slot指针,这样就可以使得非静态成员变量访问效率提高的同时内存空间也在一个可以接受的范围,对于剩下的虚函数部分,则由两个步骤支持:
1.每一个类有多少虚函数,就产生多少个指针,指向对应的虚函数地址,这些指针顺序放置于表格之中,这个表格即virtual table
2.每一个类对象会在内存中添加相应的指针,指向相关的virtual table,这个指针通常被叫做vptr
3. 解释各种情况下的C++模型构建
上述只是简单介绍了C++对象模型的选择依据及大体结构,事实上,对于不同的情况,模型的结构也会有差异,大体可以分为单继承、多继承、菱形继承,单一虚继承、虚拟菱形继承。
3.1 单继承
class CBase
{
public:
CBase()
{
printf("CBase\r\n");
}
virtual ~CBase()
{
printf("~CBase\r\n");
}
void SetNumber(int iNumber)
{
m_iBase = iNumber;
}
int GetNumber()
{
return m_iBase;
}
virtual void Show()
{
printf("CBase Show\r\n");
}
static void Print()
{
printf("~CBase Print\r\n");
}
public:
int m_iBase;
};
class CDerive : public CBase
{
public:
virtual ~CDerive()
{
printf("~CDerive\r\n");
}
virtual void say(){};
void ShowNumber(int iNumber)
{
SetNumber(iNumber);
m_iDervie = iNumber + 1;
printf("%d\r\n",GetNumber());
printf("%d\r\n",m_iDervie);
}
public:
int m_iDervie;
};
int main(int argc,char* argv[])
{
CDerive Dervie;
}
可以看到CBase有一个非静态成员变量,两个虚函数,三个非静态成员函数,一个静态成员函数,若不考虑继承关系,单独的CBase内存结构如下图所示
如前面所讲的一致,单一类若存在虚函数,编译器便会创建一个虚函数表,表中指针分别指向对应的虚函数地址(按照虚函数声明顺序依次对应放置),使用一个指针(Vptr)存储虚函数表地址,其余的函数处于代码段且不会占用到虚函数表的slot。
对于单继承类CDerive ,其内存结构如下所示
CDerive 在CBase的基础上多了一个m_iDerive变量(置于尾部),对于虚函数表,我们可以认为编译器先复制了一份CBase的虚函数表,在它的基础上找到那些被当前类重写的函数,把对应slot指向的地址改为重写的函数地址,也即上面的CDerive ::~CDerive ,此时编译器发现当前类还有一个自己的虚函数,将虚函数表添加一项slot,指向该虚函数,然后把对象的Vptr指针指向新的虚函数表地址处,值得注意的是,类的所有对象共享一份虚函数表,如上面所展示CBase虚函数表跟CDerive虚函数表,可以该类的所有对象共用,即同一类所有对象Vptr指向同一个虚函数表地址,虚函数表在msvc编译环境下放置于常量段中,可以如下验证:
可以看到对于同一个类产生的CDerive对象,Vptr指向的虚函数表地址是一致的,我们通过地址找到对应内存,可以看到,虚函数表地址处开始的12个字节存储的正是3个虚函数的地址(每个四字节),通过虚函数表记录的虚函数地址我们可以通过反汇编工具找到对应的代码段位置。
3.2 多继承(非菱形继承)
class CSofa
{
public:
CSofa()
{
m_iColor = 2;
}
virtual ~CSofa()
{
printf("~CSofa\r\n");
}
virtual int GetColor()
{
return m_iColor;
}
virtual int SitDown()
{
return printf("CSofa SitDown\r\n");
}
protected:
int m_iColor;;
};
class CBed
{
public:
CBed()
{
m_iLength = 4;
m_iWidth = 5;
}
virtual ~CBed()
{
printf("~CBed\r\n");
}
virtual int GetArea()
{
return m_iLength * m_iWidth;
}
virtual int Sleep()
{
return printf("CBed Sleep\r\n");
}
protected:
int m_iLength;
int m_iWidth;
};
class CSofaBed : public CSofa, public CBed
{
public:
CSofaBed()
{
m_iHeight = 6;
}
virtual ~CSofaBed()
{
printf("~CSofaBed\r\n");
}
virtual int SitDown()
{
return printf("CSofaBed SitDown\r\n");
}
virtual int Sleep()
{
return printf("CSofaBed Sleep\r\n");
}
virtual int GetHeight()
{
return m_iHeight;
}
protected:
int m_iHeight;
};
int main(int argc, char* argv[])
{
CSofaBed sofaBed;
}
多继承一个首要的问题是类的虚函数表需要几个,虚函数指针(Vptr)需要几个,内存如何布局?
- 直接继承的有虚函数的父类有多少个,便有多少个虚函数表,也会有相应数量的虚函数指针,对于本子类新增的虚函数,则统一放在第一个虚函数表中
-
内存布局中,父类按照声明顺序排列
其对象模型如下图所示:
可以看到非菱形多继承在单继承的基础上对多个父类进行布局,对父类虚函数表‘’拷贝‘后‘’重写’的步骤其实是一样的,只是需要考虑本子类新增的虚函数放置的位置,目前的编译器大多将其放于第一个基类(存在虚函数表)的虚函数表中
3.3 菱形继承
菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例
#include "stdio.h"
class CFurniture
{
public:
CFurniture()
{
m_iPrice = 0;
}
virtual ~CFurniture()
{
printf("~CFurniture()\r\n");
}
virtual int GetPrice()
{
return printf("CFurniture GetPrice\r\n");
}
public:
int m_iPrice;
};
class CSofa : public CFurniture
{
public:
CSofa()
{
m_iPrice = 1;
m_iColor = 2;
}
virtual ~CSofa()
{
printf("~CSofa\r\n");
}
virtual int GetColor()
{
return printf("CSofa GetColor\r\n");
}
virtual int SitDown()
{
return printf("CSofa SitDown\r\n");
}
protected:
int m_iColor;
};
class CBed : public CFurniture
{
public:
CBed()
{
m_iPrice = 3;
m_iLength = 4;
m_iWidth = 5;
}
virtual ~CBed()
{
printf("~CBed\r\n");
}
virtual int GetArea()
{
return printf("CBed GetArea\r\n");
}
virtual int Sleep()
{
return printf("CBed Sleep\r\n");
}
protected:
int m_iLength;
int m_iWidth;
};
class CSofaBed : public CSofa, public CBed
{
public:
CSofaBed()
{
m_iHeight = 6;
}
virtual ~CSofaBed()
{
printf("~CSofaBed\r\n");
}
virtual int SitDown()
{
return printf("CSofaBed SitDown\r\n");
}
virtual int Sleep()
{
return printf("CSofaBed Sleep\r\n");
}
virtual int GetHeight()
{
return printf("CSofaBed GetHeight\r\n");
}
protected:
int m_iHeight;
};
int main(int argc, char* argv[])
{
CSofaBed sofaBed;
sofaBed.m_iPrice = 1;//error
sofaBed.CSofa::m_iPrice = 1;//success
sofaBed.CBed::m_iPrice = 1;//success
}
其内存布局如下图所示:
可以看到由于CSofaBed 间接继承了CFurniture两次,导致内存中有两个m_iPrice数据成员,增大了空间,也易引发歧义
3.4 简单虚继承
为了解决上述菱形继承所带来的空间及歧义问题,虚继承出现了,在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。
class CFurniture
{
public:
CFurniture()
{
m_iPrice = 0;
}
virtual int GetPrice()
{
return printf("CFurniture GetPrice\r\n");;
}
public:
int m_iPrice;
};
class CSofaVirtual : virtual public CFurniture
{
public:
CSofaVirtual()
{
m_iPrice = 1;
m_iColor = 2;
}
virtual int GetColor()
{
return printf("CSofaVirtual GetColor\r\n");;
}
virtual int SitDown()
{
return printf("CSofaVirtual SitDown\r\n");
}
protected:
int m_iColor;
};
此时CSofaVirtual 的内存模型如下图所示:
我们使用指针对内存进行访问及打印输出,以便更好地理解模型
int main(int argc, char* argv[])
{
typedef void(*Fun)(void);
CSofaVirtual sofa;
cout << "CSofaVirtual对象内存大小为:" << sizeof(CSofaVirtual) << endl;
//取得CSofaVirtual的虚函数表
cout << "[0]CSofaVirtual::vptr";
cout << "\t地址:" << (int *)(&sofa) << endl;
//输出虚表CSofaVirtual::vptr中的函数
for (int i = 0; i<2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*(int *)(&sofa) + i);
fun1();
cout << "\t地址:\t" << *((int *)*(int *)(&sofa) + i) << endl;
}
cout << "[1]vbptr ";
cout << "\t地址:" << (int *)(&sofa) + 1 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&sofa) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]CSofaVirtual::m_iColor=" << *(int*)((int *)(&sofa) + 2);
cout << "\t地址:" << (int *)(&sofa) + 2;
cout << endl;
cout << "-------------------------------------------------" << endl;
//[3]
cout << "[3]CFurniture::vptr";
cout << "\t地址:" << (int *)(&sofa) + 3 << endl;
//输出CFurniture::vptr中的虚函数
for (int i = 0; i<1; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&sofa) + 3) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&sofa) + 3) + i) << endl;
}
//[5]
cout << "[4]CFurniture::m_iPrice=" << *(int*)((int *)(&sofa) + 4);
cout << "\t地址: " << (int *)(&sofa) + 4;
cout << endl;
}
分析后我们可以得出,vbptr存储的第二个字段(本例中为8)的数值即为基类CFurniture相对于vbptr的偏移值,当需要访问CFurniture的数据时,通过vbptr存储的偏移值去寻址。
3.5 虚拟菱形继承
好了,现在我们使用虚继承来优化菱形继承
class CFurniture
{
public:
CFurniture()
{
m_iPrice = 0;
}
virtual int GetPrice()
{
return printf("CFurniture GetPrice\r\n");
}
protected:
int m_iPrice;
};
class CSofa :virtual public CFurniture
{
public:
CSofa()
{
m_iPrice = 1;
m_iColor = 2;
}
virtual int GetColor()
{
return printf("CSofa GetColor\r\n");
}
virtual int SitDown()
{
return printf("CSofa SitDown\r\n");
}
protected:
int m_iColor;;
};
class CBed :virtual public CFurniture
{
public:
CBed()
{
m_iPrice = 3;
m_iLength = 4;
m_iWidth = 5;
}
virtual int GetArea()
{
return printf("CBed GetArea\r\n");
}
virtual int Sleep()
{
return printf("CBed Sleep\r\n");
}
protected:
int m_iLength;
int m_iWidth;
};
class CSofaBed : public CSofa, public CBed
{
public:
CSofaBed()
{
m_iHeight = 6;
}
virtual int SitDown()
{
return printf("CSofaBed SitDown\r\n");
}
virtual int Sleep()
{
return printf("CSofaBed Sleep\r\n");
}
virtual int GetHeight()
{
return printf("CSofaBed GetHeight\r\n");
}
protected:
int m_iHeight;
};
其内存布局如下图所示:
对比单一虚继承
可以看到与单一虚继承相比,此例的虚拟菱形继承多了次父类CBed跟最派生类CSofaBed,需要考虑的主要有两点,一是次父类CBed跟CSofaBed内存如何布局,二是CSofaBed类新增的虚函数应该放置于哪张虚函数表
- 次父类按照声明的顺序顺序放置,最派生类放置于所有次父类之后,最后是虚祖父类
- CSofaBed新增的虚函数放置于第一张虚函数表处
我们使用指针对内存进行访问及打印输出,以便更好地理解模型
int main(int argc,char* argv[])
{
typedef void(*Fun)(void);
CSofaBed sofaBed;
cout << "sofaBed对象内存大小为:" << sizeof(sofaBed) << endl;
//取得CSofa的虚函数表
cout << "[0]CSofa::vptr";
cout << "\t地址:" << (int *)(&sofaBed) << endl;
//输出虚表CSofa::vptr中的函数
for (int i = 0; i<3; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*(int *)(&sofaBed) + i);
fun1();
cout << "\t地址:\t" << *((int *)*(int *)(&sofaBed) + i) << endl;
}
//[1]
cout << "[1]CSofa::vbptr ";
cout << "\t地址:" << (int *)(&sofaBed) + 1 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&sofaBed) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]CSofa::m_iColor=" << *(int*)((int *)(&sofaBed) + 2);
cout << "\t地址:" << (int *)(&sofaBed) + 2;
cout << endl;
cout << "-------------------------------------------------"<<endl;
//[3]
cout << "[3]CBed::vptr";
cout << "\t地址:" << (int *)(&sofaBed) + 3 << endl;
//输出CBed::vptr中的虚函数
for (int i = 0; i<2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&sofaBed) + 3) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&sofaBed) + 3) + i) << endl;
}
//[4]
cout << "[4]CBed::vbptr ";
cout << "\t地址:" << (int *)(&sofaBed) + 4 << endl; //虚表指针的地址
//输出虚基类指针条目所指的内容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&sofaBed) + 4) + i);
cout << endl;
}
//[5]
cout << "[5]CBed::m_iLength=" << *(int*)((int *)(&sofaBed) + 5);
cout << "\t地址: " << (int *)(&sofaBed) + 5;
cout << endl;
//[6]
cout << "[6]CBed::m_iWidth=" << *(int*)((int *)(&sofaBed) + 6);
cout << "\t地址: " << (int *)(&sofaBed) + 6;
cout << endl;
cout << "-------------------------------------------------" << endl;
//[7]
cout << "[6]CSofaBed::m_iHeight=" << *(int*)((int *)(&sofaBed) + 7);
cout << "\t地址: " << (int *)(&sofaBed) + 7;
cout << endl;
cout << "-------------------------------------------------" << endl;
//[8]
cout << "[8]CFurniture::vptr";
cout << "\t地址:" << (int *)(&sofaBed) + 8 << endl;
//输出CFurniture::vptr中的虚函数
for (int i = 0; i<1; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&sofaBed) + 8) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&sofaBed) + 8) + i) << endl;
}
//[9]
cout << "[9]CFurniture::m_iPrice=" << *(int*)((int *)(&sofaBed) + 9);
cout << "\t地址: " << (int *)(&sofaBed) + 9;
cout << endl;
getchar();
}
可以看到,虚拟菱形继承主要解决的便是class包含多个虚基类派生类产生的数据二义性及冗余的问题,msvc编译器采用的是虚基表存储虚基类偏移的方式,虚基类成为一个共享部分,虚基类派生类访问都通过虚基表指针去间接访问,当然,不同的编译器为了支持虚拟菱形继承所采用的方法也不尽相同,如Sun编译器采用了虚函数表存储虚基类偏移量的方式,通过对虚函数表负索引得到偏移的方式进行间接访问虚基类