承接上一篇博客中内容,讲述完类和对象中构造函数内容之后,这篇博客我们来讲述类和对象中,析构函数的内容。
目录
1.析构函数
2.拷贝构造函数
3.浅拷贝与深拷贝
1.析构函数
在类和对象的构建当中,类中的对象会通过构造函数来完成初始化,与之相应的存在析构函数来对类中的对象进行销毁(释放),它的特点是:
- 只能被声明为公有(public);
- 析构函数同类名,不过前加会加上~;
- 析构函数无任何参数,不能被重载;
- 一个类中只能存在一个析构函数;
- 无相应的返回类型;
- 析构函数在释放一个对象中会被自动调用;
同样的,当一个类中不存在析构函数,系统会默认生成一个析构函数来完成对类中对象的释放。
#include<iostream>
class Point {
private:
int x;
int y;
public:
Point() {
};
Point(int xvalue, int yvalue) :x(xvalue), y(yvalue) {
}
void Print() {
std::cout << "(" << x << "," << y << ")" << std::endl;
}
~Point() {
std::cout << "析构函数被调用\n";
}
};
int main() {
Point a(1, 1);
a.Print();
return 0;
}
当我们执行上述代码,得到结果如下:
我们可以很明显的看出,当我们主函数退出之前,会释放程序创建资源。那么对于类而言,便会调用它的析构函数来释放它所创建的对象,于是我们便在结果上观察到“析构函数被调用”的打印内容。
2.拷贝构造函数
拷贝构造函数也是为我们提供的一种初始化类中对象的方式,它可以用一个已经存在的对象去初始化另外一个对象,并且拷贝构造函数必须为构造函数的一个重载类型,其-【-特点如下:(正如它的名字,通过拷贝来构造对象。)
- 拷贝构造函数名字与类相同,不能指定返回类型;
- 拷贝构造函数只能由一个参数,并且该参数是该类中其他某一对象引用;
- 拷贝构造函数不能被显示调用。
我们照例在Point类中进行书写:
编译并执行得到结果如下:
可以很明显看出,当创建对象a时,会调用构造函数;当通过a来初始化b时,会调用拷贝构造函数。最后在函数退出时,会释放资源,调用两次析构函数,对a和b全部进行释放。
最后,当出现以下三种情况时,拷贝构造函数会被自动调用来初始化对象:
- 当用类中一个已经存在的对象去初始化另外一个对象时;
- 当函数的形参时类的对象,进行形参和实参结合时;
- 当函数的返回值是类的对象,函数执行完成返回调用者时。
对于上述第3条,我们进行一个格外补充说明,当我们设计程序如下这种形式:
对于不同的编译器会存在不同的打印结果,使用vs一些较老版本会将”拷贝构造函数被调用“打印三次。这是因为函数返回值是对象,需要返回给调用者时,程序会默认生成一个临时对象来调用拷贝构造函数,并完成对象的初始化。
对于gcc编译器而言,它会将函数返回对象进行优化,即函数return时并不会创建额外的临时对象和调用拷贝构造函数,所以我们只能在结果中看到”拷贝构造函数被调用“只打印了两次。分别是函数调用之前,和函数执行语句中的两次。
3.浅拷贝与深拷贝
对于类的构造函数,析构函数和拷贝构造函数,当我们不去主动设计它们,系统也会默认为我们生成一份。对于前两者的函数参数列表和函数体都是空的,并不存在内容;不过对于系统默认生成的拷贝构造函数,其中参数列表会具备const引用的类,函数体中会存在对参数类的对象初始化。
对于系统默认生成的拷贝构造函数,在对数据成员进行逐一初始化时,通常并不会存在问题。但是出现一些系统难以完成拷贝赋值的数据类型(如指针……)时,系统默认生成的拷贝构造函数执行时就会发生错误。
于是我们首先对拷贝进行分为,将系统默认生成的拷贝构造函数对成员数据进行逐一初始化的过程,称为“浅拷贝”;对于需要进行我们自己设计拷贝构造函数,来对额外数据成员进行拷贝初始化时,称为“深拷贝”。
我们编写代码如下:
#include<iostream>
typedef int DataType;
class Stack {
private:
DataType*_array;
size_t _size;
size_t _capacity;
public:
Stack(size_t capacity = 10) {
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (_array == nullptr) {
std::cout << "malloc failed\n";
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data) {
_array[_size] = data;
_size++;
}
~Stack() {
if (_array) {
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
};
int main() {
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1);
return 0;
}
通过执行上述代码,编译器便会报错,因为我们通过对象s2来对对象s1进行拷贝,s2和s1中的内容便会完全一致。于是当函数运行结束退出时,会调用两次析构函数来分别对s1和s2进行释放,可是s1和s2所指向的内存空间是同一块,所以会导致同一块内存空间被销毁两次,程序崩溃。
总结:当我们类中对象涉及到资源的申请时,使用系统默认的拷贝构造函数函数进行拷贝过后,在析构函数进行资源释放时会造成同一块内存销毁多次,导致程序崩溃。浅拷贝是行不通的,那么一旦类中涉及到资源申请,我们一定要设计自己的拷贝构造函数,进行深拷贝。