C++类的底层实现

类的优点是能将“数据”(数据成员)和“操作”(函数成员)封装起来,大大提高程序的可维护性和扩展性。

对于非静态数据成员,每个对象应持有自己独立的数据,因此不同对象的数据成员应存储在不同的内存区域中,一般存储在堆栈区。

对于静态数据成员,它不属于类的某个对象,存储在全局变量区。

函数成员是对数据进行的操作,即使数据源不同,数据操作方式是相同的,因此没有必要为每个对象单独开辟内存空间来存储函数成员,同一个类的不同对象的函数成员地址是完全相同的。


非静态成员的储存方式

类的所有非静态数据成员以连续存储的方式存储在内存中,如下图所示。我们可以将一个对象看作一个存储了不同类型数据的”数组”,该”数组”的首地址是对象本身的地址(即this指针)。对象的数据成员是根据this指针以及该数据成员与this的偏移量来获取的。
这里写图片描述
我们通过以下例子来加深理解。假设我们定义了一个AClass类,它包含两个私有数据成员a和b,分别是int和char类型。此外,还定义了一个公有函数成员setParams,对这两个数据成员进行赋值。

class AClass {
private:
    int a;
    char b; 
public:
    void setParams(int a, char b) {
        this->a = a;
        this->b = b;
    }
}; 

测试代码如下:

AClass obj;
void *pObj = &obj;
int *pA = (int*)((char*)pObj + 0); // obj.a的地址
char *pB = (char*)pObj + sizeof(int); // obj.b的地址

obj.setParams(1,'a');
cout << *pA << ',' << *pB << endl; // 输出1,a

obj.setParams(2,'b');
cout << *pA << ',' << *pB << endl; // 输出2,b

输出结果与setParams的实参对应,验证了数据成员的存储方式。

上述例子说明了一个事实:类的私有(或保护)成员并非不可访问!把类的成员设置为private(或protected)的用途仅仅是提醒编译器在编译的时候不能通过obj.a或pObj->a的方式来访问类的私有(或保护)成员。但不排除可以通过别的方式读取或修改类的私有(或保护)成员。当然,类的设计初衷是实现数据封装、访问限制等功能,通过上述例子的方式访问类的私有成员是不安全的,也是不推荐的,上述例子仅用于说明数据成员在内存中的存储方式。


关于对象的存储空间大小问题

既然对象是由它的非静态数据成员构成的,理应对象的存储空间大小等于各个数据成员的存储空间总和。然而,这这两者往往是不相等的。对象的存储空间大小还与以下因素有关:
1. 虚函数
2. 虚继承
3. 空类
4. Data alignment

含有虚函数的类多一个虚函数指针__vptr,因此存储空间应多出sizeof(__vptr)。

虚基类往往多一个指向基类对象的指针,因此存储空间也应多出sizeof((void*)0)。

空类是指没有任何数据成员的类,为了标识它的不同对象,编译器往往会给空类对象分配1个字节的内存空间。有趣的是,继承自空类的空类,编译器也往往只给它分配1个字节的空间,而不是2个字节,以避免空间的浪费;继承自空类的非空类,编译器往往会忽略空类1个字节的空间,只计算非空类自身数据成员所占的空间。

Data alignment是指编译器会在数据成员之间自动填充一些字节,以保证数据成员字节对其。我们知道,对象的数据成员是根据this指针以及该数据成员与this指针的偏移量来获取的。CPU在读取内存时,往往不是只读取一个内存单元的数据,而是一片。例如,32位机器往往一次读取4个内存单元数据;在64位机器往往一次读取8个内存单元数据。如果数据成员与this的偏移量是4或8的整数倍,往往能提高内存读取性能。Data alignment的目的是在数据成员之间填充一些字节,使得每个数据成员与this的偏移量均是4(32位机器)或8(64位机器)的整数倍。

例如:

class A {
    int a;
    char b;
};

class B {
    char b;
    int a;
};

类A和B的数据成员完全相同,但它们的存储空间确是不同的。

在32位机器上,对于类A,第一个数据成员a与this的偏移是0,是4的整数倍,不必填充;第二个数据成员b与this的偏移是4,也是4的整数倍,不必填充。因此sizeof(A) == sizeof(int) + sizeof(char)。

对于类B,第一个数据成员b与this的偏移是0,是4的整数倍,不必填充;第二个数据成员a与this的偏移是1,不是4的整数倍,因此编译器会在两个数据成员之间填充3个字节的数据,以保证第二个数据成员a与this的偏移是4。因此sizeof(B) == sizeof(int) + sizeof(char) + 3。


函数成员

我们可以通俗地认为,函数成员在编译完成后都会转换为一个C风格的函数。例如,对于非静态的函数成员,如上例中的AClass::setParams(int a,char b)在编译后会转换为以下函数

void AClass::setParams(AClass *this, int a, int b);

其中,函数的第一个形参实际上就是对象的this指针,指向对象的内存地址。每个非静态成员都隐含this这一参数,this指针是隐式传递到函数中去的。对于大多数编译器而言,this指针的传递方式与其他参数不同,this指针往往是通过寄存器来传递的。

对于非静态成员函数,它属于类不属于某个对象,因此调用静态成员函数时不需要传递this指针,编译后静态成员函数的形参个数不变。

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值