引言

本节我们将学习C++中类的存储模型,据此更好地讲解this指针及其作用。由于C++的类和C语言中的结构体大致相当,所以我们先回顾C中结构体的存储方式,即字节对齐

先看个例子

学习C语言时,我们可能了解过结构体的字节对齐数据结构老师讲的合理吗……它是结构体的成员进行存储时采用的方式。比如,对于下面这样一个结构体:

1
2
3
4
5
6
7
typedef struct MyStruct{
int a;
char b;
int c;
short int d;
char e;
}MyStruct;

可否想过,如果我们对其进行取大小运算,即sizeof(MyStruct),结果将会是多少?

我们可能会直接将各个成员的大小加起来,所以结果将会是4+1+4+2+1=12 。我们不妨去程序里面验证一下:

结果和我们之前计算的竟然不一样我不演了,这个问题就出在结构体的字节对齐

字节对齐的含义

字节对齐可以表述为:各种类型的数据按照一定的规则在空间上排列,而不是按顺序一个接一个地排放。 就上面的例子而言,结构体的中每四个字节分一段,所以储存的结果为:

如果知道字节对齐就先想想应该是怎么排放的吧

图中黑色部分表示间隔,没有存放数据。因为c占了4个字节,所以不能和b在一段里面,被挤到了下一段;相反地,d占2个字节,e占一个字节,所以它们就可以放在一段里面。

字节对齐的原因

C语言是一门注重程序运行速度的语言,字节对齐其实是C语言以空间来换时间的一种方式

因为从计算机的角度来讲,它通常是以2,4或8的字节块来对程序进行读写的。C语言尽可能地使一个成员的读写在一次扫描中完成,就可以加快程序运行的速度。

字节对齐的应用

学习字节对齐是有实际意义的!它在面试中也经常被问到,我们应该重视。

如果我们将上面例子中结构体成员的顺序改一下:

1
2
3
4
5
6
7
typedef struct MyStruct{
int a;
char b;
char e;
short int d;
int c;
}MyStruct;

再按照字节对齐的方式算一下,发现它的大小为12字节,恰好没有间隔!这样空间利用率就达到了最高

所以,合理地安排结构体中的元素顺序,可以节省空间 。这应该作为程序员写代码时的自觉。

上面的例子都是四字节对齐。除此之外,还有二字节和八字节对齐的方式,这和计算机有关。读者不妨去验证一下自己的计算机是哪种对齐方式(大多数都是四字节对齐)。


类的存储模型

类的大小

明白了结构体的字节对齐,我们再对下面的类进行取大小运算

1
2
3
4
5
6
7
8
class MyClass{
public:
int a;
char b;
int add(){
return (a+b);
}
};

类相比于C语言中的结构体多了成员函数。我们先来分析可能的结果。

先想想成员函数的大小可能会怎么计算

int占4字节,char占1字节,函数如果以指针的形式存储,占4字节。那么,按照字节对齐,该类的大小应该是12字节。是这样吗?我们接下来去程序中验证一下。

最终的结果是8字节,与预想的不同!我们可以验证,函数在类中是不占空间的

值得注意的是,C++为空类分配了1个字节的空间,表示它是存在的。

C++的内存模型

在讲类的存储模型之前,我们先来研究一下C++的内存模型

  1. 栈是我们经常用到的。调用一个函数,就要在栈上开辟一个新的空间,栈从高地址向下生长。
  2. 动态内存分配的空间就在堆上,堆向上生长。
  3. 数据段包括全局区,静态区和常量区,是程序运行前事先分配好的
  4. 代码段,顾名思义存的就是代码。不过,它不是将我们的程序原封不动地存储,而是以二进制指令的形式进行存储。

类的存储模型

类的成员变量和结构体的存储一样,我们关注的是类中函数的存储

如上图,对于一个类的所有对象,它们的成员变量可以不同,但是成员函数都是一样的。那么为了节省存储空间,我们应该让它们共用这个函数。即一个类的成员函数存放在公共的代码段

我们可以通过程序来验证这一点:

myclassyourclass的add函数的地址都是相同的,可以说明类的成员函数存放在公共的代码段。

总结类的存储模型:

  1. 成员函数在类中不占空间,它储存在公共的代码段。
  2. 类中的成员变量按照字节对齐的方式进行存储。
  3. 空类占一个字节表示存在。

This指针

问题

通过上面类的存储模型,我们知道了成员函数都存储在公共的代码段中。那么问题来了,对于下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
class MyClass{
public:
int _a;
char _b;
int add(){
return (_a+_b);
}
};
int main(){
MyClass m,n;
m._a=1;
m._b=2;
n._a=3;
n._b=4;
std::cout<<m.add();
}
//输出3,即1+2

这个程序中MyClass的实例有mn两个。我们调用m中的add()函数时,通过add这个函数名找到对应函数在代码段中的地址。可是,m.add()n.add()都指向的是同一个函数的地址,那么,↓

这个函数如何知道是m这个实例在调用它而不是n呢?

this指针的含义

C++通过This指针来解决上面的问题。

This指针的定义

C++编译器给每个给每个非静态的成员函数增加了一个隐藏的指针参数 ,让该指针指向调用当前函数的对象 。该函数体中的所有对成员变量的操作,都是通过这个指针来访问的。

调用成员函数的地方,编译器会自动增加了对象的地址作为隐藏的实参

1
std::cout<<m.add(&m);

而类中成员函数的定义,就会自动添加this指针作为形参,同时this指针会指向函数里面的成员变量:

1
2
3
int add(MyClass* this){
return (this->_a+this->_b);
}

这样成员函数就知道是哪个实例在调用它了,同时也能正确地对该实例中的成员变量进行操作。

我们通过下面的程序对this指针的存在 及其指向这个对象blue 进行证明:

this指针在哪?

这是一个频率极高的面试题,但是,如果我们明白什么是this指针,这个问题也就迎刃而解了。

this指针是成员函数的一个形参。由C++的内存模型知,当我们调用这个成员函数的时候,就会使用到栈空间。this指针就存储在栈空间里面。