inside the c++ object笔记(二)

chapter3

class的大小

1、语言本身的负担,如virtual function,virtual base class带来的虚指针,指向其虚函数表,或虚函数表偏移的位置。

2、编译器对特殊情况的优化,处理一个empty base object的基类时,是否会优化掉其用来占位的1bytes。

3.、alignment(齐位),可能补充在成员之间也可能补充在类的边界,32位补到4的倍数(不一定,具体需要其他的文章)

 

typedef

在类内使用的typedef最好在开始就声明,先使用类外的typedef,然后再使用类内的同名typedef覆盖是无法通过编译的。

 

 

成员顺序

non static member在obect内的排布为:相同access section中的成员按照声明顺序排布,不同access section理论上可以随意排布,其实同一access section中的成员只要符合先声明的成员地址小于后声明的成员即可(但是没有编译器会选择随意放置)。其间补上vptr或齐位。

通常来说access section和member的顺序都是按照声明顺序来排布的,vptr在开头/结尾,access section再多,也不会使class变大。

 

non static member

每个member的偏移值都在编译器就可知,计算一个data member的位置时总是减一。在不同深度的继承体系中存取成员的速度都是一样的。

 

调用

object.member

pointer->member

 

使用对象的速度在没有任何virtual时和指针一致,但出现virtual后需要等到执行期才能知道实际的offsest。

 

 

继承与data member

1、单一继承与无多态

注意:

需选择正确的inline函数,比如拷贝构造,赋值运算符。

把class分多层可能会使得class膨胀,因为class需要 在每个subobject上进行齐位,所以经过多重继承后,class可能会比原来大得多。这一点原因是c++保证"derived class“中的subobject保证其原有完整性。也就是说,如果不在每个subobject上都进行齐位,那么在把一个由base*指向的base class赋值给一个base*指向的derived class时,就会把derived class的某些成员覆盖,因为如果没有进行齐位,所以derived class的成员会放置在base class的成员后,而一个独立的base class将进行齐位,也就是说独立的base class的成员后为空白的齐位字节,把这些地址上的内容赋值给派生类时将把派生类的开始的某些成员覆盖(当然,只有在使用base*的时候才能把一个base对象赋值给一个derived class)。

 

2、加上多态

存取成员的额外开支

(1)导入virtual table来存放virtual function,并且其中的一个到两个slot来存放类的动态类型(runtime type)

(2)加强constructor去初始化vptr并加入到class中(每个constructor)

(3)加强destructor来除去vptr

 

vptr的位置

尾:可以兼容c

首:对于多重继承指向class member的指针访问virtual function有帮助(不需要计算vptr在subobject中的偏移,只需要计算其他base class的偏移),不兼容c。

 

出现virtual则根据vptr的放置改变地址计算方式,引用/指针需要指向相应的静态类型。

 

 

 

3、多重继承

没有virtual继承的时候。把一个derived class对象赋值给一个base class指针/引用不需要额外计算偏移(不论多少层继承效率都相等)。

 

出现virtual则根据vptr放置的位置去改变地址的计算方式,引用/指针需要指向相应的静态类型。

 

同样的,如果是把一个对象转换成其继承的第二个及以后的基类(指针),不能只是简单得把开始地址传入(基类指针),而是要把之前的所有基类的偏移量全部计算(还需要进行空指针的检测)。(第一个的vptr就在开头,与派生类共享)

base *bp = &derived ? (base *)( (char*)(&derived) + sizeof(subobject before) ) :0;

基类指针 *bp =  需转换的派生类地址是否为空 ?

不为空则(转换为基类指针) ( (转换为char*进行地址计算) (取派生类首地址)+ 这个基类之前所有嵌套基类部分的大小即偏移量 ) :

为空则等于0 即 空指针

(如果是reference则不需要空检测,因为引用不可能绑定无物)

也就是基类指针指向的是派生类中这个基类部分的开头

 

可能的优化

有些编译器发现继承列表中位置靠前的base class没有vptr而靠后的base class有vptr时,它们会选择把有vptr的base class调换到前面来减少一个vptr的产生(调到第一个就可以和当前的派生类共享一个虚指针)

 

 

 

3、虚拟继承

缺点:

1、每个base class中都要加入一个vptr指向共享的部分。

2、多层虚拟继承增加了多层间接性

 

实现:

1、经典模型把所有base class的vptr都赋值一份到derived class,可以解决间接性的问题,即可以实现固定时间的存取(从object -> vptr(virtual base)->vptr(another vortual base) 到 object -> vptr(any base class) )

2、microsoft的virtual base class table,为derived class生成一个vptr指向virtual base class table,每个虚拟基类占一行,解决缺点2的对象膨胀问题。

3、在virtual function table里放入virtual base class的指针,也就是增加额外的间接性

object->vptr[n]->member来访问基类member。正索引为function,负索引为base class的pointer

 

 

 

指向datamember的指针调整

取一个没有绑定实例的对象的成员的地址会得到其偏移值,取实际对象的成员则会得到实际的地址

&class:member ->得到偏移值              &object.member  -> 得到地址值

为了区分“没有指向任何成员的成员指针” 和 “指向第一个成员的成员指针”,所以才为所有的成员指针的偏移加上了1,因为

if( empty pointer == pointer to first member )需要这么一个偏移量来区分(否则两个取地址得到的偏移量都是0,判断为true)

不过事实上vs的sizeof计算偏移值的时候不会算上这个1.实际的地址没有这个1的偏移。但是,取一个成员的地址后需要减去某数才能得到对象的起始地址,当对象拥有虚函数指针的时候-1得到对象地址,当对象同时拥有虚指针和虚基类指针时则需要-2(到底减去多少取决于对象拥有的vptr数量),没有的话则不需要减,可以看出vs进行了某种处理。另外vs的vptr放在首位,v base pointer也放在开始。

//仅虚函数指针
class A
{
	virtual void fun() {}
public:
	int x, y, z;
};

int main()
{
	A a;
	A *ap;
	ap = (A*)(&(a.x) - 1);//A具有虚指针,故需要-1
	cout << &A::x << endl;
	cout << &A::y << endl;
	cout << &A::z << endl;//cout无法输出正确偏移值
	cout << "________________________" << endl;

	cout << &a << endl;
	cout << ap << endl;//等于&a
	cout << &(ap->x) << endl;
	cout << (A*)(&(a.x)) << endl;//不减去1时等于&a.x,说明虚指针在开头
	cout << "________________________" << endl;
	cout << &a.x << endl;
	cout << &a.y << endl;
	cout << &a.z << endl;
	printf("%p\n", &A::x);
	printf("%p\n", &A::y);
	printf("%p\n", &A::z);

	system("pause");
}
//加上虚继承指针
class A
{
	virtual void fun() {}
public:
	int x, y, z;
};

class D :virtual public A {};//

int main()
{
	D a;
	D *ap;
	a.x = 5, a.y = 5;
	ap = (D*)(&(a.x)-2);
	cout << &D::x << endl;
	cout << &D::y << endl;
	cout << &D::z << endl;
	cout << "-------------------------" << endl;
	cout << (D*)(&(a.x)) << endl;//不减等于&a.x,说明虚指针在开头
	cout << (D*)(&(a.x) - 1) << endl;//减一
	cout << &a << endl;//减二后等于对象起始地址,说明有两个虚拟指针(可能更多)
	cout << ap << endl;
	cout << "-------------------------" << endl;
	cout << &(ap->x) << endl;
	cout << ap->x << endl;
	cout << &a.x << endl;
	cout << a.x << endl;
	cout << "-------------------------" << endl;
	cout << &a.y << endl;
	cout << &a.z << endl;

	system("pause");
}

offset会因为多重继承而改变,把指向基类成员的指针传递给派生类成员的指针是ok的(因为派生类包含了基类成员,取出的是派生类中基类部分的成员),所以这个时候编译器需要额外计算在这个基类之前的subobject的偏移并检测指针是否为空

class A
{
	virtual void fun() {}//虚拟也不会影响
public:
	int x, y;
};

class B :public A {
public:
	int z;
	//int x;若这里覆盖了从A继承来的x,则指向基类成员的指针无法得到正确值,因为指向的是基类嵌套部分的值
};

void fun(int B::*p, B b)
{
	cout << b.*p << endl;
}

int main()
{
	A a;
	B b;
	b.x = 100;
	int A::*p = &A::x;//指向基类A中的x成员的指针
	fun(p, b);//成功把p传入到了要求 指向B中整型成员的指针 中

	system("pause");
}

 

效率影响

指针会带来一层间接性(vptr的负载和一般指针一样),普通继承对效率几乎没有影响,间接性会使得存取效率几乎低一半,因为间接性阻止了“把所有处理都搬到寄存器”的优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值