类和对象(中)
主要是对于类对象大小的计算,以及复习结构体内存对齐规则,还有对于this指针的理解和使用,最后是关于构造函数和析构函数的讲解
如何计算类对象的大小
对于计算一个类型的大小,我们首先想到的是sizeof关键字,我们知道class或者struct都是自定义类型,所以我们根据sizeof也是可以计算类对象的大小
class Person{ int data; double name; }; int main() { int ans1=sizeof(Person); Person p; int ans2=sizeof(p); cout<<ans1<<" "<<ans2<<endl; return 0; }
根据上述程序可知,我们知道了,C++中对于类的大小的计算是根据类中的成员变量在内存中的大小决定的,反应了,C++是对于C语言的延申,保留了C语言中对于结构体的内存的计算方式,类的内存大小计算方式与结构体类似,但是有一点不同,下面代码讲解
struct A{}; class B{}; int main() { int ans1=sizeof(A); int ans2=sizeof(B); return 0; }
如果类中没没有成员变量,或者就是为空,默认空间大小为1,这是因为,我们要对于这个类预留一个字节的空间,毕竟是一个自定义类型,怎么能没有空间呢?(这样讲好理解)
类中的成员变量和函数是如何保存的呢
每一个对象中的成员变量是不一样的,但是带哦用同一个函数,如果按照这个方法来存储,当类创建多个对象的时候,每一个对象中都会保存一部分相同的代码,这样浪费空间,所以让我们来看看类中的成员是如何保存的?
- 只保留成员变量,成员函数放在公共区的代码段
所以我们sizeof得到类的大小的时候,实际上就是成员变量的在内存中的大小,函数是对象调用的时候,才去公共区寻找- 一个类的大小实际上就是该类中成员变量之和(内存对齐之后),当为空类时候,编译器给了空类一个字节来唯一标识这个类的对象
结构体内存对齐的规则
结构体内存对齐,详情在C语言篇章的结构体部分已经讲解过了,这里就用代码一笔带过
struct Person{ int data; double name; };//输出结果为 16 int main() { cout<<sizeof(Person)<<endl; return 0; }
内存对齐步骤
1.第一个成员与结构体偏移量的0地址处对齐
2.其他成员变量一套对齐某个数字(对齐数)的整数倍的地址处
对齐数:编译器默认的一个对齐数与这个成员大小的较小值,VS中默认对齐数为8(当然可以用代码来规定对齐数是几)
3.结构体的总大小为:最大对齐数的整数倍
最大对齐数:所有变量类型的最大者与默认对齐数取最小
4.如果嵌套了结构体的情况下,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍
上述已经对于内存对齐问题做了详细讲解,接下来是相关的面试题
- 结构体怎么对齐? 为什么要进行内存对齐?
上文已经很详细了,是有他设置的内存对齐规则,内存对齐是为了编译器读取数值的时候方便,且更多的是因为
1.平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
是可以通过宏指令来改变默认对齐数,改变成2的n次方的指定数值,#pragma pack(任意值),所以3,5不可以,4可以。
- 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
大小端是编译器的存储方式
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
- 通过运用指针的知识可以测试大小端
int main() { int a=1; char* i=(char*)&a; if(*i==1){ cout<<"大端"<<endl; }else{ cout<<"小端"<<endl; } return 0; }
内存对齐实际上是用空间来换取时间的做法,为了更加节省空间,我们应该在写结构体的时候,将占用空间小的成员尽量放在一起
this指针
this指针的使用,我们来看下面的代码
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class Person { public: int add(int x, int y) { return x + y; } void getName() { cout << name << endl; } private: int data; string name; }; int main() { Person p1, p2;//我们创建了两个对象,分别调用了两个方法,都要输出对象中的成员变量name p1.getName(); p2.getName();//我们知道这样的输出结果是不同的,那么我们是怎么使得编译器来区分不同的对象使用相同的函数得到不同的结果呢 return 0; }
这就引出了this指针的概念:C++编译器给每个“非静态的成员函数”增加了一个this指针,这是一个隐藏的指针参数,让该指针指向当前对象(调用函数方法的那个对象),在函数体中所有的成员变量的操作(比如输出name),都是通过该this指针来访问的,只不过所有操作对于用户来讲是观察不到的,即编译器会自动完成
this指针的特性
1.this指针的类型:类名类型* const,即成员函数中,不能给this种子很赋值。比如this=nullptr这是会报错的
2.只能在成员函数的内部使用,在类域外或者是成员变量处是无法使用的
3.this指针的本质是成员函数的形参,当对象调用成员函数的时候,将对象地址作为实参传递给this形参。所以对象中不存储this指针
4.this指针是成员函数的第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递
//对于上面四条this的总结,我们来看下面的代码 //1.对于指针类型 class Person{ public: void Print(Person* const this)//实际上是这样 { cout<<this._name<<endl; }//这就是 类名* const this private: string _name; }; //2. class Person{ public: void Print() { cout<<_name<<endl; } private: string _name; //不能在这里使用如 this._name=.....; }; //当然也不能在类域外使用 //3.对象中并不存储this指针,只是在调用成员函数的时候隐式使用this形参,将对象的信息(地址)给这个指针
面试题:
- this指针存在哪里?
this指针是形参,所以是跟普通参数一样存在函数调用的栈帧里面
- this指针可以为空吗?
this指针语法上可以为空的,但是也要根据是否对this解引用来判断,如果需要调用成员变量(this解引用),那就不能为空,反之可以,this指针是否为空,是看对象的,因为this指针的值实际上是对象在ecx寄存器中的地址值,也就是说,如果对象地址为空,this也为空,只要不用到类中的相关变量的信息就没有影响,程序正常运行,反之,会运行崩溃
this为空时,程序正常运行代码,不需要对this解引用
class Person{ void Print(){ cout<<"Print()"<<endl;//只是输出Print(),没有进行调用类中成员变量,所以就算this为空,也会正常运行程序 } int name; }; int main() { Person* p=nullptr;//对象地址赋值为nullptr p.Print();//这个输出结果为 Print() return 0; } //p调用Print,不会发生解引用,因为Print函数地址不在对象中,p会作为实参传递给this指针
this为空时,程序崩溃,需要对this解引用
class Person{ void Print(){ cout<<name<<endl;//这里我们要调用类的成员变量,但是因为,this为空(对象为空),所以无法调用,即程序崩溃 } int name; }; int main() { Person* p=nullptr;//对象地址赋值为nullptr p.Print();//调用类的成员函数 return 0; } //p调用Print,不会发生解引用,因为Print函数地址不在对象中,p会作为实参传递给this指针
类的6个默认成员函数
空类:类中什么成员都没有
空类中没有成员,但是会有编译器自动生成的6个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
构造函数
构造函数:就是对于类的成员变量进行初始化的函数,当没有自定义构造函数的时,编译器会使用默认构造函数,所以自己写,实际上算是函数重载
//下面是对于构造函数的使用 class Person{ int data; //只有变量,没有自定义构造函数 }; int main() { //实例化类Person Person p;//这个时候,我们创建了p这个对象,也就直接调用了默认构造函数 return 0; }
自定义构造函数的方法:不需要返回值,构造函数的函数名为类名,括号内写形参
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
特点如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
//下面是代码演示 class Person{ public: Person(){}//无参构造函数 Person(int data){//有参构造函数 this.data=data; } private: int data; }; int main() { Person p;//这是调用无参构造函数 Person d(10);//调用有参构造函数 return 0; }
调用无参构造的时候,不需要加上括号(),如果加上的话,和函数声明有逻辑冲突,,因为这个时候Person是自定义类型。
如果没有显示定义构造函数,那么编译器就会隐式定义默认构造函数,(有显示用显示,无显示用隐式)
默认构造函数有没有用
- 我们使用默认构造函数之后,发现得到的数值都是随机值,这样看起来编译器生成的默认构造函数并没有什么用?
- 但是,我们知道C++将类型分成内置类型(基本类型,int,double,int*……),和自定义类型。自定义类型就是我们使用class/struct/union等自己定义的类型。
- 下面的程序,会发现,编译器对于自定义类型变量会调用他的构造函数
using namespace std; class Person{ private: int data; string name; Student _std; }; class Student{ public: Student() { cout<<"Stduent()"<<endl; data=10; } private: int data; }; int main() { Person p; return 0; }
当类中有自定义函数的时候,调用构造函数的顺序为:按照自定义类型在成员变量顺序调用,最后是类的构造函数
类中的自定义类型调用构造函数的时候,不能是有参构造,应该是无参构造
如果是调用默认构造的时候,成员变量都是随机值,可能有些编译器在存在自定义变量的时候,初始化会进行优化,不再是随机值,但是我们默认这个,调用默认构造,成员变量就是随机值
C++11的特性:加上了,对于类的内置类型可以在声明的时候进行赋值
class Person{ public: int data=10; //基本类型(内置类型) }; int main() { Person p; cout<<p.data<<endl; return 0; }
无参构造函数和全缺省的构造函数都为默认构造函数,且默认构造函数只能有一个
注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都可以认为是默认构造函数
class Person{ public: Person(){ _data=10;//无参构造函数 } Person(int data=10){ _data=data; }//全缺省构造函数 //这两个不能同时存在,语法没有问题,在编译的时候有问题 private: int _data; }; int main() { Person p; return 0; }
这两个不能同时存在,语法没有问题,在编译的时候有问题。
析构函数
我们都知道C语言中使用指针的时候,结束使用的时候,使用free来释放空间,指向NULL来防止野指针,在C++中我们只需要一个函数就可以完成这个事情,那就是析构函数
析构函数的特征
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
class Person{ public: Person() { cout<<"Person()构造函数"<<endl; } ~Person() { cout<<"~Person()析构函数"<<endl; } private: int data; Student s; }; class Student{ public: Student(){ cout<<"Student()构造函数"<<endl; } ~Student() { cout<<"~Student()析构函数"<<endl; } }; int main() { Person p; return 0; }
总结:
- 当类中有自定义类型时,先调用该自定义类型的构造函数(多个自定义函数,就按照顺序来依次调用)
- 当类中有自定义类型的时候,先调用这个类的析构函数,然后调用自定义类型的析构函数
- 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可