类的实例化对象的内存存储方式与内存对齐
- 对于类当中定义的成员函数,是放在公共代码区的,在公共代码区有类成员函数表。
- 对于不同的实例化对象而言,它里面的各个成员变量都是不一样的,但是如果他们分别调用相同名字的成员函数,这些成员函数都是一样的,实际上会专门划一块公共区域存放这些成员函数,而不是到当中去每个对象去找。
- 因此在类的实例化对象当中,是没有为该类的成员函数开辟空间,只有为该类的成员变量开辟空间。
- 当对类的某个实例化对象的成员变量进行访问的时候,是在访问这个实例化对象所占的内存空间,当对某个实例化对象.成员函数的时候,不会到这个实例化对象里面的内存空间去。
- C++为了和c保持统一性,也需要进行内存对齐。没有这些内存对齐的话,去访问数据会有效率的损失。对于不同的类的实例化对象,他们的成员变量是不一样的,它们有自己各自的内存空间。但是在类当中的成员函数是针对本类的专属套餐,所以说不同的实例化对象是可以进行共用的,如果说在每个实例化对象里面都放一个成员函数的话,因为本来这些成员函数都是可以在该类当中去专属使用,这样就会造成浪费,但每一个实例化对象里面去存放专属的成员变量,这是应该的。
- 反正成员函数不存在实例化对里面。
- 结构体内存对齐规则:然后由于在C++当中类是对c语言当中结构体的升级,学语言的发展必须是要向前兼容的。据说c++当中的类的各个成员变量也是需要进行内存对齐,至于具体的内存对齐的规则与c语言当中结构体内存对齐一模一样:
- 为什么结构体内存对齐?计算机在内存当中读取数据的时候,并不是你想访问哪个字节就去访问哪个字节,一般一次固定访问四个字节一次读4个字节,32个位(这个就跟具体硬件有关系),反正就是由于硬件的具体设计,一次的话必须得读四个字节,不能你想读多少就读多少。比如说:
- 内存对齐肯定都是以空间换时间。如果不想以空间换时间,就是宁愿程序跑的慢一点,但是空间十分珍贵,这时候就会出现什么修改默认对齐数啊等等,以时间换空间。
sizeof计算类的实例化对象大小
- 注意:sizeof(类)与sizeof(该类的实例化对象)两者的结果是一样的,一个道理,然后在具体计算的时候是不考虑成员函数,只考虑成员变量。
- 因为对象里面只存储成员变量而不存储成员函数,所以对象的大小只计算成员变量,不计算成员函数
- 4,成员函数是放在公共代码区的,并不是放在类的实例化对象当中,因此成员函数是不占任何空间的,sizeof在计算类的实例化对象的时候,只计算成员变量的大小
class A1
{
public:
void f1()
{
}
private:
int _a;
};
int main()
{
cout << sizeof(A1) << endl;
return 0;
}
- 16
class A
{
void func()
{
cout << "Hello" << endl;
}
int b;
char a;
double c;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
5. 24
class A
{
void func()
{
cout << "Hello" << endl;
}
int b;
char a;
double c;
char d;
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
6. 如果一个类没有成员变量(仅有成员函数或者说类中什么都没有,也就是空类),那么这个类的实例化对象也需要一个字节的空间,这一个字节的空间是为了占个位置,表示对象的存在,由于他没有成员变量,所以说不存储任何有效数据。但反正他就是有一个字节,取地址的话也是可以取得到的。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象
this 指针 【 类名* const this 】
- 在类当中的成员函数都是放在公共数据区当中,所以说如果说某一个类的两个不同实例化对象去调用相同名字的成员函数,实际上他们在调用的是同一个函数。
- 这时候问题就来了,比如说他们都调用Print函数,由于这两个类的实例化对象当中的成员变量不一样,所以就打印出了不同的结果,问题是他们两个调用的都是同一个函数,为什么会出现不同的结果呢?
- 类当中的这些成员函数他们都是放在公共数据区当中,只要是这个类的,不管多少实例化对象去调用这个函数,你从汇编指令的角度去看,无非都是一模一样的call 一模一样的地址,我就去公共数据区当中去找到这个函数,执行函数里面的汇编指令,所以说调用的函数是完全一模一样的。
- C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
- 原因在于成员函数的参数这边有我们看不见的一个隐藏起来的this指针,一旦某个类的实例化对象去调用类的成员函数,对于成员函数需要传入参数,这个参数就会有一个隐藏的this指针,你就可以理解成编译器编译了之后会把成员函数给它改掉了,参数里面反正有一个this指针指针的类型是(类名*),然后对于这个成员函数里面的各个成员变量前面也都会用this指针->访问获取。
上面讲的这是一方面,然后在外面类的实例化对象,具体去调用成员函数的时候也会把实例化对象的地址传过去,这些都是隐藏起来的,悄悄搞得,暗箱操作,编译器搞得。所以说调用的就是同一个函数,只不过它们的形参不同,所以说结果也不同。
- this指针是一个暗箱操作的罪魁祸首,他不能在成员函数的形参和实参当中去显示传递。但却能够在成员函数内部去显示使用。以后会有很大用途
- 并且这个this指针还不能去修改,它的类型实际上是这样的 (类名)* const 。所以说他自己不能被改,但是他指向的东西是可以被修改的。
- this指针形参与实参不能显示传递
从汇编与函数栈帧底层角度的一些认识
- This指针是隐藏的,所以说对于类的实例化对象在调用类的成员函数当中,比如说你的形参有三个,但实际上他是有四个。这些形参在所属函数被调用之前都会被压参压到栈里面去。但实际上对于this指针的话,编译器会进行优化,如果你频繁的去调用成员函数的话,那么这个this指针就需要频繁的去传入,在vs下面对this指针传递进行优化。
- 从函数栈帧底层的角度去理解的话,如果你要调用一个函数,那么在函数栈帧建立之前,先要把他的参数先压到栈区里面,然后等到具体函数栈帧建立完了,然后里面函数具体执行汇编代码的时候,会回过头来去找你这些已经事先压在栈里面的参数。当类的实例化对象调用成员函数的时候,这个this指针压参是通过寄存器,也就是说把类的对象的地址放在寄存器当中,通过寄存器去传参,因为寄存器相比于内存快太多了。
问:this指针存在哪边?
- 首先,这个指针不可能存在对象里面,假设他存在对象里面,那为什么用sizeof计算它大小的时候没有把这个指针给他算进去。
- this指针相当于是类当中成员函数的一个形参(被隐藏掉了,你看不见而已),当然不是实参,因为实参是对象的地址。
- this是指针成员函数的形参,根据函数栈帧的知识,刚说你形参要传入到函数的时候,需要去压栈,这个形参是作为函数栈帧的一部分,就是跟普通参数一样存在函数调用的函数栈帧里面。
- 但并不是在实例化对象的内存区域当中。然后当函数调用结束的时候,这个形参this它自然也会被销毁掉。
- 所以在内存栈区。
问:下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
C
- 易错点:因为类当中的成员函数它并不是在类的实例化对象当中,而是在公共数据段,也就是公共代码区当中。所以说如果说对象指针去->成员函数,这个指针是不需要去解引用的。因为print的地址根本就不在对象当中,而是在公共代码块当中,所以说在编译的时候的地址很容易找的到的。既然在公共代码段当中很容易找到这个函数的地址,那么在编译的时候直接汇编call xxxxxxx。
- 然后这个实例化对象指针p会作为实参去传递给成员函数。
- 如果说一个是实例化对象类型指针p去用一个箭头去访问一个成员变量的话,是需要到这个对象当中的内存空间里面去寻找的,这时候就需要对这个指针进行解引用操作,然后如果说这时候这个指针是空指针,那就麻烦了。
- 但如果说一个对象指针需要去访问一个成员函数,但成员函数是在内存当中的数据区的,所以说在编译的时候转化为汇编指令的时候是不需要到对象的内存空间当中去寻找,直接在内存的公共代码区中就可以找得到,所以就算是空指针也不会报错。
问:下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
B
问:为什么这样写不可以 : 类名::成员函数();
- 虽然这个符号它确实是域作用限定符,并且类名确实是类域的名称。
- 但是当你这样写的时候,你这个成员函数的this指针传什么参数,根本就没有一个实例化对象,也就意味着根本就没有这个对象的地址,所以说这个函数就传不了参数,所以就会报错。