kuikuitage

  博客园  ::  :: 新随笔  :: 联系 ::  :: 管理
#include <cstdio>
#include <string>

using namespace std;

struct X
{};

struct Y : virtual public X
{};

struct Z : virtual public X
{};

struct A : public Y, public Z 
{};

int main(void)
{
	X x;
	Y y;
	Z z;
	A a;
	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d\n", 
		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
	getchar();
    return 0;
}




来信者编译器:
1.考虑语言的特性,虚表,是否需要在子类中分配额外的一个指针存放虚基类表地址。来信者的编译器好像看图示好像没有
2.考虑编译器优化,空虚基类的1字节在子类有成员变量的时候是否被优化掉,来信者编译器没有优化掉。
3.考虑字节对齐补齐。和系统位数和编译器设定的对齐字节数有关。
即在来信者的编译器上,上述class A的大小由下列几点决定。
(1)X被大家共享的唯一个实体,空类大小为1。
(2)基类Y和Z的大小为减去因虚继承而配置的大小,因Y和Z都为空,依然占用1各字节。
(3)A自己的大小
(4)考虑对齐
VS编译器:
1.考虑语言的特性,虚继承后子类存在虚基类表
2.编译器优化,基类为空时,子类非空(不存在虚表指针或者成员变量),则基类占位1字节被优化掉
3.B,C成员变量和虚函数表的大小
4.X的成员变量和虚函数表的大小
5.公共的A成员变量和虚函数表的大小

回到这个钻石继承示例,在书中示例所谓来信者的编译器和VS两种编译器情况做说明。

来信者编译器,看起来这个编译器没有考虑虚基类表指针,只考虑了对齐补齐的内存开销。X空类本身1个字节,Y和Z依然为空故而依然各存在一个字节,虚基类X独立一份实体。三个空类各占一个字节,但单独对齐补齐后占用了12个字节。问题是A为空,A为何不占用一个字节,作者也没提。


VS编译器,应用了虚基类表指针,同时优化掉了空类的1个字节,X空类,Y和Z虚继承X后各存在一个虚基类表指针各占用4字节,虚基类X本身空,A存放按继承顺序存放Y和Z后,这里因为A本身没有成员变量和虚函数,直接附加虚基类A,因为A已经非空了,即使X为空,基类X也不再占用空间,故而A大小其实只占用8字节,即两个虚基类表指针的大小。

另外需要注意到另外一个编译器导致的差异:不同编译器的虚基类在子类存放位置差异。来信者编译器是虚基类是放在子类的开始位置,而VS的虚基类对象是放在子类的结尾位置。

VS作为主流编译器的一种,且不断更新C++标准,学习以VS为准。

一个虚基类只会在其子类中存在一份实体,不管它在继承体系中出现多少次

(1)数据成员的绑定###

暂且把绑定改成决议。。。比较好理解。


外部引入的全局x和成员x,这里编译器需要考虑编译过程中使用的x到底是this.x还是extern的global x。
为什么存在调用全局x的情况,因为编译器在从上往下进行编译Point3D的函数的时候还看不到自己定义的x,使用了全局的x,为此出现了两种策略。
(1)把Point3D的x声明放在类的开始位置,这样在编译X()的时候就会取就近作用域的x即this.x
(2)内联函数声明和定义分开,在整个类声明完了再进行定义,规避inline内联函数在声明时就定义时内部的x还没有被声明的情况。
考虑如下代码

#include <cstdio> 
#include <string> 

using namespace std; 

int m_x = 10;

struct Point3D
{
	inline int X(){return m_x;}
	void X(int x){m_x = x;}
	int m_x;
};

int main(void) 
{ 
	Point3D* point3d = new Point3D;
	int x = point3d->X();
	printf("x = %d\n", x);

	getchar(); 
	return 0; 
}


实际依然为this.x,VS并没有考虑将m_x定义在前,或者将inline的定义延后。按照作者的说法编译器在策略上对类成员函数的分析是在整个类声明完毕了才开始。
规避了编译函数时使用的内部成员还没有被完全决议的问题。

另外一个是成员函数的参数列表的决议

#include <cstdio> 
#include <string> 

using namespace std; 

typedef int length;

struct Point3D
{
	length X(){return _val;}
	void X(length val)
	{
		_val = val;
		printf("sizeof(val)= %d, sizeof(_val) = %d\n", sizeof(val), sizeof(_val));
	}

	typedef double length;
	length _val;
};

int main(void) 
{ 
	Point3D* point3d = new Point3D;
	point3d->X(10.0f);

	getchar(); 
	return 0; 
}


可以看出参数中的length被决议成了全局的int,而内部的length被决议成了期望的double。
但是当我们把内部的声明放到声明的开始位置就符合预期了。

#include <cstdio> 
#include <string> 

using namespace std; 

typedef int length;

struct Point3D
{
	typedef double length;

	length X(){return _val;}
	void X(length val)
	{
		_val = val;
		printf("sizeof(val)= %d, sizeof(_val) = %d\n", sizeof(val), sizeof(_val));
	}

	length _val;
};

int main(void) 
{ 
	Point3D* point3d = new Point3D;
	point3d->X(10.0f);

	getchar(); 
	return 0; 
}


此时length均被决议成了类内部的double类型定义。
至于为何定义位置差异会出现两种不同的决议,以及参数列表和函数内部的决议策略不再深入研究。

基于以上,需要注意两点:
1.成员变量命名尽量加m_,减小和全局重名的概率。
2.类中的typedef放在类的开始位置。至少要放在内部成员有使用到之前。

(2)数据成员的布局###

C++标准只要求在同一个access section(private,public,protected)中,符合"较晚出现的成员在类对象的高地址中"这一条件即可。表明,不管分多少个段,声明多少个访控属性的的段,只要同一个访控属性的段晚声明的在高地址即可。
类的静态成员不占用类对象的存储空间,存储在数据段,其属于类本身,可以直接通过类访问,就像类的成员函数可以通过类的空指针对象访问一样(尽管成员函数使用了类的成员变量的时候有风险)。

作者为了进一步说明,拿模板实现了一个通用的取成员变量地址并进行比较的模板函数。
应该只是比较了Point3D对象中的成员z,y和地址用来说明,z比y的声明晚,故而y的地址低。
上图中声明类的成员变量指针操作没有遇到过,但int X:😗 temp = &X::m_x确实有这种写法返回的temp表示偏移量。

(3)数据成员的存取###

1.静态成成员变量
继承过程中的静态成员变量

#include <cstdio> 
#include <string> 

using namespace std; 

struct Point3D
{
	int x;
	static double d;
};

struct Point3D2 : public Point3D
{

};

double Point3D::d = 1.1f;

int main(void) 
{ 
	printf("%lf, %lf, 0x%x, 0x%x\n", Point3D::d, Point3D2::d, &Point3D::d, &Point3D2::d);
	getchar(); 
	return 0; 
}

静态成员属于类,存储于数据段。

如果有两个类有同名的静态变量,编译器会根据所属的名字空间进行区分。

2.非静态成员变量
非静态成员通过对象指针访问或者直接对象访问,在虚继承的时候会因为虚继承的机制导致存在间接寻址,有存取效率的差异。

3.没有虚函数情况下非虚继承的成员变量
这种情况下没有虚表指针基类存储于子类开始的位置。但基类需要考虑内存对齐和补齐。
一个类拆分成多个基类后,可能因为各个基类都要内存对齐导致继承后整体空间变大。比如只要一个char类型的两个基类,如何只写一个类,则只占4字节,如果分开写则占用8字节,这样多占用了内存,但在子类中可以直接分割出两个基类,不会存在两个基类的内存交叉。

4.加上多态的单一继承
这种继承只是增加了虚函数表。对比下

#include <cstdio> 
#include <string> 

using namespace std; 

class A 
{ 
       virtual int fun(){} 
public: 
       int dataA; 
}; 

class B : public A 
{ 
       virtual int fun(){} 
       virtual int foo(){} 
public: 
       int dataB; 
}; 

class X 
{ 
public: 
	int dataX; 
}; 

class Y : public X 
{ 
	virtual int fun(){} 
	virtual int foo(){} 
public: 
	int dataY; 
};

int main(void) 
{ 
       printf("sizeof(int) = %d, sizeof(A) = %d, sizeof(B) = %d, sizeof(X) = %d, sizeof(Y) = %d\n", 
               sizeof(int), sizeof(A), sizeof(B), sizeof(X), sizeof(Y)); 
       getchar(); 
       return 0; 
}


对应的B和Y的结构如下

至于虚表指针有的编译器存放于对像开始位置,有的编译器存放在对象结束的位置。

4.加上多态的多重继承

代码就不贴了,简单说明下,Point2D和Vertex有虚函数,都是普通继承,继承关系如下

作者所用的编译器结构如下

对于VS编译器,只需要把虚函数表指针由基类对象的结束位置放到开始位置即可。

至于下述情况

现有VS编译器存在这种优化。

#include <cstdio> 
#include <string> 

using namespace std; 

class A 
{ 
       virtual int fun(){} 
public: 
       int dataA; 
}; 

class X 
{ 
public: 
	int dataX; 
}; 

class Y : public X ,public A
{ 
	virtual int fun(){} 
	virtual int foo(){} 
public: 
	int dataY; 
};

int main(void) 
{ 
       getchar(); 
       return 0; 
}


暂时发现少产生一个虚函数表指针的情况是普通继承复用了第一个基类的虚函数表,或者子类没有新增虚函数时直接使用基类的虚函数表。

5.虚继承

涉及到虚基类的基类在子类的存储形式,有编译器在子类中直接存放各个基类的地址,有的则存放一个虚表的地址,虚表中再存放各个虚基类的地址,VS等主流为第二种

这里书中所用的编译器似乎是把虚基类表和虚函数表放在一起了,使用上下偏移来区分的。。。好像不符合主流策略,先不管了。

6.对象成员的效率
这个作者做了几个测试。涉及到编译器可能做的优化,似乎也并不能说明什么问题。不过多态或者多继承肯定会影响效率。

7.成员变量地址

#include <cstdio> 
#include <string> 

using namespace std; 

class X 
{ 
	virtual int fun(){} 
public: 
	int dataX;
	int dataX2;
}; 

int main(void) 
{ 
	printf("%p",&X::dataX2);
	getchar(); 
	return 0; 
}


确认对类的成员变量直接取地址得到的是该成员相对类起始位置的偏移。存在其他编译器这个地址是实际偏移+1的情况,可能是VS编译器做了处理。

而对类对象的成员变量取地址得到的是当前成员变量的实际地址。

作者主要讲了这种成员变量的偏移地址在被多继承后子类中需要重新调整偏移地址,否则找不到对应的成员。但如下又没有作解释

#include <cstdio> 
#include <string> 

using namespace std; 

struct X 
{ 
	int dataX; 
}; 

struct Y 
{ 
	int dataY; 
}; 

struct A :public X, Y
{ 
}; 

int main(void) 
{ 
	printf("&A::dataX = %p, &A::dataY = %p\n", &A::dataX, &A::dataX); 
	getchar(); 
	return 0; 
}

另外关于

关于成员指针可参考 指向成员的指针

posted on 2020-02-16 21:44  kuikuitage  阅读(158)  评论(0编辑  收藏  举报