来自深度探索C++对象模型的补充

- 每个class object存储空间,包含其数据成员.
对non-inline member function,只产生一个函数实例.
对inline function,在其每一个使用者身上产生一个函数实例.
- C++对象模型
Nonstatic data members被配置于每一个class object内
static data members被存储在个别class object外.
static和nonstatic function members也被放于个别的class object外.
对virtual functions:
1.每个class产生一堆指向virtual functions指针,放在表格中.
2.每个class object被安插一个指针,执行相关的virtual table.vptr的设定,重置由每一个class的constructor/destructor/copy assignment自动完成.
每一个class所关联的type_info object也放在表格中.
3.需要多少内存才能表现一个class object,一般要有:
其nonstatic data members的总和大小
加上由alignment的需求而填补上去的空间
加上为支持virtual而产生的负担

- C++对象的初始化/拷贝/赋值
对派生类,要考虑到派生类对象的基类部分
对类本身,要考虑到类自身的各个非static数据成员
对包含虚基类/含虚函数基类/自身有定义虚函数的类,每个类对象存储一个虚函数表指针.
继承体系每个类型有一个关联的虚函数表,
表中每个条目指向该类型的一个虚函数代码实例的起始地址.

- C++类型构造中,
对基类对象显式/隐式构造--
显式指定下,指定者负责
隐式构造下,编译器负责
对类自身每个非static数据成员显式/隐式构造--
显式指定下,指定者负责
隐式构造下,编译器负责
对含虚函数的类对象的虚表指针的初始化--
编译器负责
	
- C++拷贝构造
拷贝构造时,
要考虑到基类对象部分
要考虑到每个非静态数据成员
在类包含虚函数[类自己定义了虚函数/类的基类定义的虚函数/类有虚基类],要考虑到类的虚表,指向类的虚表的指针.

派生类对象赋值/拷贝赋值给基类对象是可以的.
但要注意,
1.一个是会发生切割.
即为
基类对象仅选取派生类对象中基类拥有的部分进行拷贝.
2.在基类和派生类有自己各自的虚表时,
基类和派生类对象有各自的虚表指针,分别执行类的虚表.
用派生类去赋值或拷贝构造得到的基类对象内的虚表指针,
仍然指向基类的虚表.

若成员+虚表指针连续存储于一个对象占据空间中
+
默认拷贝构造/拷贝赋值采用内存比特位简单复制方式,
则无法达成上述要求.

在类型继承或间接继承了某个虚基类时,
按虚基类的要求,即使多重继承不同继承各有一份相同虚基类下,
在派生类对象中,虚基类部分有且仅有一份.
为了避免歧义,
派生类对象中存在一个额外的偏移或指针字段,
用于指出虚基类部分位置.
这时如用派生类对象来拷贝构造/赋值它的一个基类对象
采用
若成员+虚表指针连续存储于一个对象占据空间中
+
默认拷贝构造/拷贝赋值采用内存比特位简单复制方式,
则无法达成上述要求.

一般,同类型对象间的拷贝构造/拷贝赋值,
采用内存比特位简单复制可完成要去.


- 需要记住的
C++的类,多态,...一系列特性
均可转化为C的形式.
类在C里,属于一个struct X和关联的函数集合.
1.C++的实参传递一种编译器用C实现的方式
将实参修改为调用前一个临时对象
将实参的传递,由值传递改为引用传递+调用前对临时对象用实参执行拷贝构造.
2.C++的函数返回值一种编译器用C实现的方式
将返回值,作为一个额外的引用参数
返回结果前,设置引用参数内容,
再以void返回

上述均是编译器以C++源码为输入,
执行转换,生成语义一致的C版本.

编译器的各种形式的优化,
本质也是一个C++源码,
可以通过不同C实现形式得到一致结果,
但不同实现形式下,效率可靠性不同.

- 可能的错误
读一个包含虚表指针的类对象执行memset(&obj, 0, sizeof(obj))
存在把保存在类对象内的虚表指针设为0的风险.
- 构造顺序
基类构造
按声明顺序对成员进行构造

出现在构造初始化列表中,
导致执行构造函数
对出现在构造函数体内的赋值,
实际执行赋值函数

执行顺序:
基类构造
依据构造初始化列表部分,执行对应构造函数
函数体部分,按赋值函数来

- Data语意学
class X{
    
    };
class Y:public virtual X
{
    
    };
class Z:public virtual X
{
    
    };
class A:public Y, public Z
{
    
    };
sizeof(X)// 1
sizeof(Y)// 8
sizeof(Z) // 8
sizeof(A) // 12
解释:
a.对sizeof(X)占据1字节解释
这使得此类型实例,可拥有一独一无二地址.
大小为0对象,无法拥有地址
每个不含任何非静态数据成员的类型,被编译器安插一个char隐藏成员
b.Y和Z大小
b-1.语言本身额外负担
类型含虚函数,则类型有一个虚表
每个类型实例对象存储一个指向类型虚表的指针
类型有虚基类时,
每个类型实例对象需存储一个虚基类部分在对象中的偏移
/或指向虚基类对象指针
b-2.Alignment

- C++如何实现多态
1.多台指的是通过基类指针或引用调基类虚函数
执行时依据指针实际指向类型[动态类型]调用调用与其实例类型一致的函数.
实现上,可在每个多态的类实例对象上加两个成员.
一个用于标识对象的实际类型
一个指向对象虚表的指针
每个虚函数,在表格中占据一项,位置用一个索引表示.
一个class只会有一个virtual table
每个table内含其对应之class object中所有active virtual functions函数实例的地址.
active virtual functions包括:
a.class自身新定义或重载的虚函数
b.继承自base class的虚函数
c.一个pure_virtual_called函数实例
单一继承下一个虚表实例

point
包含一个
虚析构函数
一个名为mult的纯虚函数
一个名为y的虚函数
一个名为z的虚函数
包含一个名为x的数据成员
则一个point实例内存布局:
x
虚表指针

指向的虚表含五项
项0为point类型信息
项1为虚析构Point::~Point函数地址
项2为名为mult的纯虚函数地址
项3为Point::y虚函数地址
项4为Point::z虚函数地址

point2d继承自point,对虚函数析构/y/mult有自己的实现
包含数据成员y
则一个point2d实例内存布局:
x
虚表指针
y

指向的虚表含五项
项0为point2d类型信息
项1为虚析构Point2d::~Point2d函数地址
项2为名为Point2d::mult的虚函数地址
项3为Point2d::y虚函数地址
项4为Point::z虚函数地址

point3d继承自point2d,对虚函数析构/mult/z有自己的实现
包含数据成员z
则一个point3d实例内存布局:
x
虚表指针
y
z

指向的虚表含五项
项0为point3d类型信息
项1为虚析构Point3d::~Point3d函数地址
项2为名为Point3d::mult的虚函数地址
项3为Point2d::y虚函数地址
项4为Point3d::z虚函数地址

一个class派生自一个有虚函数的基类时,
1.它可继承base class所声明的virtual functions函数实例
2.可对继承的虚函数提供自己的版本
3.可新增自己定义的虚函数[导致虚表扩展,还是只有一个虚表]

在上述背景下,
通过基类point指针ptr调z()时,
1.每次调z()时,不知道ptr所指对象的真正类型
但通过ptr可存取到该对象的virtual table
因为每个基类和派生类对象中vptr成员距离对象起始的偏移是一样的
2.虽然不知道哪一个z()实例被调用,
但知道每个z()实例都被放在虚表槽4中
因为每个派生类无论是否重载z(),z在虚表中位置不变.

这样,无论基类指针指向的动态类型是啥
都可通过
(*ptr->vptr[4])(ptr)
这一同样的调用形式来表达.
这样就没做什么额外工作,然而却实现了多态.
- 多重继承下多态的一个可能实现说明
一个基类Base1
有一个int数据成员a
有虚析构函数,虚函数SpeakClearly,clone
则每个Base1类的实例成员内存布局:
一个int成员a
一个指向虚表的指针

类型Base1的虚表含4项,
项0放类型信息
项1放Base1::~Base1析构函数
项2放Base1::SpeakClearly
项3放Base1::clone

一个基类Base2
有一个int数据成员b
有虚析构函数,虚函数mumble,虚函数clone
则每个Base2类的实例成员内存布局:
一个int成员b
一个指向虚表的指针

类型Base2的虚表含4项,
项0放类型信息
项1放Base2::~Base2析构函数
项2放Base2::mumble
项3放Base2::clone

一个派生了基类Base1,基类Base2的类型Derived
有一个int数据成员c
有自己定义的虚析构~Derived/虚clone函数
则每个Derived类的实例成员内存布局:
Base1类型子对象
但此子类对象中虚表指针指向的虚表属于Derived结合Base1得到的
Base2类型子对象
但此子类对象中虚表指针指向的虚表属于Derived结合Base2得到的

Derived结合Base1得打的虚表包含5项
项0放Derived的类型信息
项1放Derived::~Derived虚函数
项2放Base1::SpeakClearly
项3放Derived::clone
项4放Base2::mumble

Derived结合Base2得到的虚表包含4项
项0放Derived类型信息
项1放Derived::~Derived
项2放Base2::mumble
项3放Derived::clone

C++里用指向Derived对象指针赋值给指向Base2对象指针,
编译器要作一个指针偏移.
C++里用指向Derived对象指针调Base2虚函数,编译器也要作一个指针偏移.
总之,由于多重继承导致上述有多个虚表,
且涉及派生类指针和多个子类指针间转换时,
编译器要对指针进行偏移,以保证C语义下表现正确.
- C++针对虚继承支持不稳定,尽量避免用虚拟继承/多重继承

- 考虑一个如下继承关系
Point3d virtual 继承Point
Vertex virtual 继承Point
Vertex3d分别public继承Point3d和Vertex
PVertex public继承Vertex3d

对类,如其包含一个或多个虚基类,
则类的实例对象中虚基类部分只有一个,且类的实例有一个指针/偏移::指向虚基类部分位置/在对象内偏移.
考虑
PVertex::PVertex(float x, float y, float z)
	: _next(0), Vertex3d(x, y, z), Point(x, y)
{
    
    
	if(spyOn)
	{
    
    
		cerr << "PVertex::PVertex()"
			<< "size:"
			<< size()
			<< endl;
	}
}
可能被扩展为
PVertex* PVertex::PVertex(
	PVertex* this,
	bool __most__derived,
	float x,
	float y,
	float z)
{
    
    
	// 纯基类对象,只在最终派生类中构造一次
	if(__most__derived)
	{
    
    
		this->Point::Point(x, y);
	}
	
	this->Vertex3d::Vertex3d(x, y, z);
	// 虚表指针
	this->__vptr_PVertex = __vtbl_PVertex;
	// 指向虚基类子对象指针
	this->__vptr_Point__PVertex = __vtbl_Point__PVertex;
	
	// 程序员写的代码
	if(spyOn)
	{
    
    
		cerr << ....
		// (*this->__vptr__PVertex[3].faddr)(this)
	}
	
	return this;
}
在类构造过程中,
若基类构造时,在基类构造函数体执行了虚函数,
此时this的动态类型是基类类型,按此前提执行虚函数.

在类构造完毕后,
通过类指针执行虚函数,
即使遇到,
类指针执行类未重载的基类的虚函数
且基类虚函数中又调了另一虚函数的情形,
一个持续保持的现象是整个过程this指针指向的动态类型
始终是派生类类型.
多重继承下,this的隐式偏移,
也是在派生类对象内部多个虚表指针间偏移.


- 类大小
计算下面几个类的大小:	
class A {
    
    };
int main()
{
    
    
  cout<<sizeof(A)<<endl;// 输出 1;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 1;
  return 0;
}
空类的大小是1, 
在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。
具体来说,空类同样可以被实例化,
并且每个实例在内存中都有独一无二的地址,
因此,编译器会给空类隐含加上一个字节,
这样空类实例化之后就会拥有独一无二的内存地址。
当该空白类作为基类时,
该类的大小就优化为0了,子类的大小就是子类本身的大小。
这就是所谓的空白基类最优化。

空类的实例大小就是类的大小,
所以sizeof(a)=1字节,
如果a是指针,则sizeof(a)就是指针的大小,即4字节。
class A 
{
    
     
	virtual Fun(){
    
    } 
};

int main()
{
    
    
  cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器);
  A a; 
  cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器);
  return 0;
}
因为有虚函数的类对象中都有一个虚函数表指针 __vptr,
其大小是4字节	
class A 
{
    
     
	static int a; 
};

int main()
{
    
    
  cout<<sizeof(A)<<endl;// 输出 1;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 1;
  return 0;
}
静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类大小

class A 
{
    
     
	int a; 
};
int main()
{
    
    
  cout<<sizeof(A)<<endl;// 输出 4;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 4;
  return 0;
}

class A 
{
    
     
	static int a; 
	int b; 
};

int main()
{
    
    
  cout<<sizeof(A)<<endl;// 输出 4;
  A a; 
  cout<<sizeof(a)<<endl;// 输出 4;
  return 0;
}
静态成员a不占用类的大小,所以类的大小就是b变量的大小 即4个字节

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/113139966