《深度探索C++对象模型》学习笔记 — Data语义学(The Semantics of Data)
一、数据成员的绑定
1、全局变量与局部变量
float x = 5.0;
class CLS_Point
{
public:
CLS_Point(float _x)
{
x = _x;
}
float getX()
{
return x;
}
private:
float x;
};
对于上述代码中的getX方法,在最初的C++版本中,返回的将会是全局变量x的值,而非成员变量x。这是因为getX出现在类声明中,将会成为内联函数。内联函数中的符号解析在早期是按顺序进行的。这造成了两种不同的防御式编程风格:
(1)将数据成员放在类声明的最前面
(2)将类中的内联函数定义放在类声明之后
现在的C++采取rewrite rule,即一个内联函数在类的全部声明未被完全看见之前,是不会被评估求值的。
2、全局类型与局部类型
上述规则并不适用于成员函数的参数列表。也就是说,参数列表中的类型别名查找将按声明顺序发生:
#include <iostream>
using namespace std;
typedef char pos;
class CLS_Point
{
public:
CLS_Point(pos _x)
{
cout << (int)_x << endl;
cout << boolalpha << is_same_v<pos, char> << endl;
cout << boolalpha << is_same_v<pos, int> << endl;
}
private:
typedef int pos;
};
int main()
{
CLS_Point(135);
}
在打印变量 _x 时,其值为负数,因为在初始化列表中,别名pos被解析为char类型。在函数定义中我们进行类型判断时,发现pos被解析为int类型,因为定义中的符号裁定发生在完整的类声明被编译器看到之后。
针对这个问题,我们需要将类型别名的声明放在类声明的最前面。
二、Data Member 的存取
我们以下述代码为例,学习成员变量的存取成本和通过对象及指针访问成员变量的区别
CLS_Point point, *pPoint = &point;
point.x = 0.0;
pPoint->x = 0.0;
1、静态成员变量
对于静态成员变量的访问,两种访问方式并没有任何区别。因为通过类实例或指针访问静态数据并没有实际意义。在编译后,两种代码都将会被被转化为通过类访问静态数据。即使CLS_Point有很复杂的继承体系,且该静态数据位于其某个基类中,其存取成本仍然与继承深度与是否为虚继承无关。
如果我们尝试对静态成员变量取址,所获得的数据类型为指向其数据类型的指针,而不是一个指向其成员变量的指针。因为静态成员并不内含在类对象中:
#include <iostream>
using namespace std;
class CLS_Point
{
public:
static float x;
float y;
};
float CLS_Point::x;
int main()
{
CLS_Point point;
cout << boolalpha << is_same_v<decltype(&CLS_Point::x), float*> << endl;
cout << boolalpha << is_same_v<decltype(&CLS_Point::y), float*> << endl;
cout << boolalpha << is_same_v<decltype(&CLS_Point::y), float CLS_Point::*> << endl;
}
2、非静态数据成员
非静态数据成员的调用必须通过具体的对象进行。实际上,编译器是通过把对象的起始地址加上数据成员的偏移量获得数据成员的地址,以进行数据存取的。也就是说具体某个对象中成员变量的地址为:
&point.x = &point + &CLS_Point::x - 1;
这里我们最后要-1。因为指向数据成员的指针,其偏移量总是被加上1,用以区分一个指向数据成员的指针,用以指出类的第一个成员和一个指向数据成员的指针,没有指出任何成员。这种区别类似数据首元素的地址和数组的地址。
对于非静态数据成员,通过指针和对象存取的效率未必相同。如果该指针所属对象的继承体系中有虚继承,则对于偏移量的判断需要延迟到运行期进行;使用对象完全可以在编译期确定偏移量。
三、指向成员变量的指针
1、pointer to member 的本质
指向成员变量的指针本身实际上是保存了该成员变量的偏移量。为了前面所说的两种指针的区分,该偏移量需要+1。然而,在当前版本的C++中,并不存在pointer to member到整型的转换(即使使用reinterpret_cast)。因此,我们使用printf打印其值时可以看到:
#include <iostream>
using namespace std;
class CLS_Point
{
public:
virtual void func();
float y1;
float y2;
};
int main()
{
printf("the offset of y1 is %d, of y2 is %d\n", &CLS_Point::y1, &CLS_Point::y2);
}
vs中vptr保存在类对象的头处,因此偏移量从4开始。
2、继承中的 pointer to member
对于指向成员变量的指针,C++中支持从指向派生类成员变量的指针到指向基类成员变量的指针的隐式转换(多态的一部分)。因此下面的代码是合理的:
#include <iostream>
using namespace std;
class CLS_Base1
{
public:
int m_iMem1;
};
class CLS_Base2
{
public:
int m_iMem2;
};
class CLS_Derived : public CLS_Base1, public CLS_Base2
{
};
void test(int CLS_Derived::* pMem, CLS_Derived *pObj)
{
printf("the offset of pMem is %d\n", pMem);
pObj->*pMem;
}
int main()
{
auto pMem = &CLS_Base2::m_iMem2;
CLS_Derived derived;
printf("the offset of pMem is %d\n", pMem);
test(pMem, &derived);
}
为了支持我们上面所说的隐式转换,编译器将不得不介入以调整该偏移量实际的值,正如我们上面所打印的结果。否则,如果我们使用某成员变量在基类的偏移量以获取派生类对象中对应变量的值,无法得到正确的结果。
这种多态的背后同样会带来效率的降低。