如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
我们画图来看一下:
栈中的析构函数就代替了栈的销毁:
class Stack { public: Stack(int n = 4) { if (n == 0) { _a = nullptr; _top = -1; _capacity = 0; } else { int* tmp = (int*)realloc(_a, sizeof(int) * n); if (tmp == nullptr) { perror("realloc fail"); exit(-1); } _a = tmp; _top = -1; _capacity = n; } } ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; } //void Destort() //{ // free(_a); // _a = nullptr; // _top = _capacity = 0; //} void Push(int n) { if (_top + 1 == _capacity) { int newcapacity = _capacity == 0 ? 4 : 2 * _capacity; int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity); if (nullptr == tmp) { perror("realloc fail:"); exit(-1); } _a = tmp; _capacity = newcapacity; } _a[_top++] = n; } int Top() { return _a[_top]; } void Pop() { assert(_top > -1); _top--; } bool Empty() { return _top == -1; } private: int* _a; int _top; int _capacity; }; class MyQueue { private: Stack _pushst; Stack _popst; };
对于栈这样的,我们析构函数代替了销毁函数,析构函数会自动调用,以前我们需要手动调用销毁函数的接口,现在不用调用了。
因此,构造函数和析构函数最大的优势是自动调用。
4、拷贝构造函数
4.1 拷贝构造函数的概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器会引发无穷递归调用。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
拷贝构造就像是复制粘贴一样。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会引发无穷递归调用。
传值拷贝会发生无穷递归,我们来写一个拷贝构造函数。
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //拷贝构造 Date(Date d) { _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } ~Date() { _year = 0; _month = 0; _day = 0; } private: int _year; int _month; int _day; }; void func(Date d) { d.Print(); } int main() { Date d1(2023, 8, 2); func(d1); return 0; }
内置类型的拷贝是直接拷贝,自定义类型的拷贝要调用拷贝构造完成。
在vs2019中,传值传参编译器会报错:
因此,我们要是写拷贝构造函数,形参必须是同类型的引用:
引用是给变量起别名,析构自动调用的顺序是后定义先析构,拷贝的时候d1还没有析构,因此是可以使用引用的,这样就不会导致递归拷贝了。
我们将写的拷贝构造函数屏蔽掉,看看会如何:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //拷贝构造 //Date(Date& d) //{ // _year = d._year; // _month = d._month; // _day = d._day; //} void Print() { cout << _year << "/" << _month << "/" << _day << endl; } ~Date() { _year = 0; _month = 0; _day = 0; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 8, 2); Date d2(d1); d2.Print(); return 0; }
运行结果:
我们发现,如果我们不写,还是能实现拷贝,这是因为我们不写,编译器默认生成一个拷贝构造函数,对于日期类这样的浅拷贝,默认生成的构造函数是可以实现拷贝的。
我们再来看栈的拷贝构造:
typedef int DataType; class Stack { public: Stack(int n = 4) { if (n == 0) { _a = nullptr; _size = -1; _capacity = 0; } else { int* tmp = (int*)realloc(_a, sizeof(int) * n); if (tmp == nullptr) { perror("realloc fail"); exit(-1); } _a = tmp; _size = -1; _capacity = n; } } //拷贝构造 Stack(Stack& s) { _a = s._a; _size = s._size; _capacity = s._capacity; } ~Stack() { free(_a); _a = nullptr; _size = _capacity = 0; } void Push(int n) { if (_size + 1 == _capacity) { int newcapacity = _capacity == 0 ? 4 : 2 * _capacity; int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity); if (nullptr == tmp) { perror("realloc fail:"); exit(-1); } _a = tmp; _capacity = newcapacity; } _a[_size++] = n; } int Top() { return _a[_size]; } void Pop() { assert(_size > -1); _size--; } bool Empty() { return _size == -1; } private: int* _a; int _size; int _capacity; }; int main() { Stack s1; Stack s2(s1); return 0; }
我们这里为栈写的拷贝构造,我们来试一下拷贝构造:
这里为什么引发了异常呢?
我们调试看看:
这里我们可以看到,s1的_a与s2的_a地址是一样的,当s2拷贝完后就会析构,s2的_a被释放掉后,s1还会再调用一次析构函数,这时再去释放_a,_a的空间已经被释放过了,就会引发空指针异常的问题。
因此,对于有空间申请的对象,在写拷贝构造的时候必须要深拷贝。
我们来改正代码:
typedef int DataType; class Stack { public: Stack(int n = 4) { if (n == 0) { _a = nullptr; _size = -1; _capacity = 0; } else { int* tmp = (int*)realloc(_a, sizeof(int) * n); if (tmp == nullptr) { perror("realloc fail"); exit(-1); } _a = tmp; _size = -1; _capacity = n; } } //拷贝构造 Stack(Stack& s) { cout << "Stack(Stack& s)" << endl; //深拷贝 _a = (DataType*)malloc(sizeof(DataType) * s._capacity); if (nullptr == _a) { perror("malloc fail:"); exit(-1); } memcpy(_a, s._a, sizeof(DataType) * (s._size+1)); _size = s._size; _capacity = s._capacity; } ~Stack() { free(_a); _a = nullptr; _size = _capacity = 0; } void Push(int n) { if (_size + 1 == _capacity) { int newcapacity = _capacity == 0 ? 4 : 2 * _capacity; int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity); if (nullptr == tmp) { perror("realloc fail:"); exit(-1); } _a = tmp; _capacity = newcapacity; } _a[_size++] = n; } int Top() { return _a[_size]; } void Pop() { assert(_size > -1); _size--; } bool Empty() { return _size == -1; } private: int* _a; int _size; int _capacity; };
运行结果:
总结:像Date这样不需要我们实现拷贝构造,默认生成的就可以用;Stack需要我们自己实现深拷贝的拷贝构造,默认生成的会出问题;对于成员全是自定义类型的也不需要写拷贝构造,会调用自定义类型的拷贝构造函数。
引申: