二、类和对象
7. 类对象的大小(补)
7.2 类的内存对齐规则
类的对其规则和结构体的内存对齐规则是一样的。
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整>数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
让我们来看看例子:
#include<iostream> using namespace std; class A { int _a; char _b; }; class B { char _b; int _a; }; int main() { cout << sizeof A << endl; cout << sizeof B << endl; return 0; }
结果是:
在类 A 中,_a 变量从偏移量为0开始存储,一共四个字节, _b 变量的对齐数为1,说明在1的倍数地方可以存储这个变量,总共占据了5个字节的空间,而类的总大小要是8的倍数,所以结果是8。
在类 B 中,_b变量从0开始,占1个字节,而 _a 变量的对其数为4,需要从4的倍数处才能存储,所以_a变量从4开始,存到7为止,这个类空间要是8的倍数,所以结果就是8。
我们会发现,内存对齐会导致内存浪费,为什么要内存对齐?
因为不是所有的硬件平台都能够访问任意地址的任意数据的,一般只能在某些地址处访问特定类型的数据,这回导致访问未对齐的数据时,需要从从前面访问一次,再从后面访问一次,需要两次访问,而对齐则仅需一次访问。本质上就是空间换时间的做法。
8. this指针
8.1 this指针的引出
我们来先定义一个日期类:
#include<iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1, d2; d1.Init(1111, 1, 11); d2.Init(2222, 2, 22); d1.Print(); d2.Print(); return 0; }
对于上面的类,我有这样一个问题:d1和d2调用的明明是同一个printf()函数,为啥输出的结果不一样呢,函数也不带参数,它是怎么区分d1的数据和d2的数据呢?
这下不得不牵扯出 this指针 了,C++的类中,所有的成员函数都隐含了一个参数,这个参数就是this指针。成员函数是通过this指针来访问相应对象的成员变量的。形式上就像:
// 成员函数 void print(Date* const this) { cout << this->_year << "-" << this->_month << "-" << this->_day << endl; } // 调用 d1.print(&d1); d2.print(&d2);
this指针跟普通的参数没什么区别,只是C++将其给隐藏了。
8.2 this指针的特点
我们不能主动去将this指针给写出来,也不能修改this指针的指向,但是在函数内部是可以使用this指针的。
那么,this指针存放在哪里?因为this指针是形参,所以this指针存在栈区。this指针可以为nullptr吗?我们来看看下面两段程序。
程序1:
#include<iostream> using namespace std; class A { public: void Print() { // ↓ ↓ ↓ cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
程序2:
#include<iostream> using namespace std; class A { public: void Print() { // ↓ ↓ ↓ cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
程序1和程序2分别运行后的结果是什么呢?答案是:程序1正常运行,程序2运行崩溃。为什么呢?因为尽管p指针是nullptr,但是在调用成员函数的时候,本质上是把p值传递给this指针,首先这一步肯定是没有问题的,在程序1中,函数仅仅打印了一段字符串,并没有使用this指针,所以this指针是空也可以正常运行,反观程序2,我们要去this指针的指向的空间里面找成员变量,但是this指针指向空,根本找不到成员变量,于是程序产生了崩溃。(注意: p->Print() 与 (*p).Print() 是一样的,结果相同。)this 指针可以为空。
9. 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数就是你不写,编译器会自动生成的成员函数。
10. 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。(不是void空返回值,是无返回值,不需要写返回值)
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
示例:
#include<iostream> using namespace std; class Date { public: // 构造函数 Date() { _year = 1111; _month = 1; _day = 1; } // 重载构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // 注意,这里d1的后面不能加(),否则难以跟函数声明区分开 Date d1; d1.Print(); Date d2(2222, 2, 2); d2.Print(); return 0; }
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
#include<iostream> using namespace std; class Date { public: // 没有显式定义构造函数 void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }
此时编译期生成的默认构造函数会做什么呢?
结果是啥也没做。那默认构造函数又有什么用呢,有何意义?其实是:默认构造函数对于内置类型(语言自身定义的类型,如:int, char)不做处理,自定义类型(自己定义的结构体或类)则调用自定义类型的默认构造函数。
内置类型不做处理其实属于缺陷,如果也能处理则会更加方便,于是在C++11版本,C++委员会对这个语法打了补丁,允许内置类型在声明处给缺省值。
#include<iostream> using namespace std; class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: // 声明处给缺省值 int _year = 1111; int _month = 1; int _day = 1; }; int main() { Date d1; d1.Print(); return 0; }
默认构造函数就是编译器自动生成的函数,这句话对吗?其实不对,编译器自动生成的构造函数确实是默认构造函数,但是无参的构造函数和全缺省的构造函数都属于默认构造函数。总结一下:不需要传参的构造函数就是默认构造函数。这三种函数只需要写出一个,建议是全缺省的构造函数。
11. 析构函数
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include<iostream> using namespace std; class Date { public: // 析构函数 ~Date() { // ... } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year = 1111; int _month = 1; int _day = 1; }; int main() { Date d1; d1.Print(); return 0; }
日期类的析构函数没啥好写的,毕竟都是栈上的空间,函数结束后空间自动销毁。而在某些类里,有些变量在堆上开辟了一片空间,变量的生命周期到了后自动调用析构函数来将堆上的空间释放。在C语言里,空间是需要手动释放的,忘记释放则会造成内存泄露的问题,C++则会自动调用析构函数来释放空间。
构造函数是先定义的先构造,而析构函数相反,最先定义的最后析构,就跟括号匹配一样。编译器默认生成的析构函数与构造函数一样,内置类型不做处理,自定义类型调用自定义类型的析构函数。