C++类与对象 (new与delete / 初始化列表 / 析构函数 / 拷贝构造函数 )
类的构造与析构
上篇博文我简单地介绍了一下什么是类的构造函数,什么时候构造函数被调用,并通过代码例子介绍了类的实例化。
那既然有构造,那肯定是有销毁的啦,那就是析构函数干的事情。析构函数在对象被销毁的时候调用,被销毁有两种情况:
- 离开作用域
- 调用delete
这两种情况我都会具体说一下,先说说最简单的“离开作用域”。学过C的同学都知道,局部变量在离开作用域的时候会被内存回收,除非用malloc函数为其分配内存。这里涉及到栈区和堆区的知识,简单说明一下栈区是由编译器管理的,主要是为函数的调用分配空间,函数结束以后被回收;堆区是由程序员管理的,主要通过new / delete关键字分配和回收,他在程序运行过程中一直存在,程序结束后由OS进行回收,想要知道细节的同学可以百度一下。
这里用代码简单地感受一下析构函数,还是用上次的角色类:
#include <iostream>
using namespace std;
class Character {
private:
int hp;
int mp;
public:
Character() {
cout << "新建英雄成功!" << endl;
}
~Character() {
//在构造函数前加个~就是析构函数,析构函数不能带任何参数
cout << "角色被销毁!" << endl;
}
};
void fun(){
Character mario;
}
int main(){
fun();
cout << "程序结束" << endl;
return 0;
}
[dyamo@~/code 17:08]$ g++ -o character.exe character.cpp
[dyamo@~/code 17:08]$ ./character.exe
新建英雄成功!
角色被销毁!
程序结束
可见mario的析构函数在“程序结束”之前被调用了,也就是说函数内实例化的对象在离开定义域之后会自动被销毁。
new / delete
那有没有什么方法能让对象长时间存在内?那就是使用C++的关键字new,为对象分配堆上的内存。在对象的实例化上,关键字new做了以下两件事:
- 为对象分配堆上的内存空间,并返回对象地址;
- 调用该类的构造函数;
学过C的同学可能比较属性malloc这个函数,这个函数也是给变量或者结构体分配堆上内存的。当然malloc也可以给类的对象分配堆上内存,但是有一个问题,那就是malloc并不会调用类的构造函数,他只是单纯的划一片内存区域给你,划多少由你说了算;而且他返回的是void *指针,想要使用的话还得进行强制类型转化才能使用。所以要为对象分配堆上内存,还是用得用new。还是用上面的角色类例子:
void fun(){
//实例化对象并返回对象地址
Character *mario_ptr= new Character();
}
int main(){
fun();
cout << "程序结束" << endl;
return 0;
}
[dyamo@~/code 17:08]$ g++ -o character.exe character.cpp
[dyamo@~/code 17:08]$ ./character.exe
新建英雄成功!
程序结束
可见mario的析构函数并没有被调用程序就已经结束了,所以我们也可以知道OS回收程序内存就是单纯的回收,并不会做什么收尾工作。
那么如果我们想销毁用new实例化的对象,就得用C++的另一个关键字delete。上面也说了对象被销毁只有两种情况,第二种就是调用delete来销毁。我们来销毁mario:
void fun(){
Character *mario_ptr = new Character();
delete mario_ptr;
}
要注意两点问题:
- delete后跟着的一定是该对象的指针
- new和delete一定是成双成对使用的。用了new就一定是用delete来回收内存,不是用new分配的内存就千万不要用delete回收。
Character mario;
delete mario; //error!不能这样使用
delete &mario; //error!mario不是用new实例化的
再额外说一下批量分配内存。如果我们要一次实例化多个对象,就必须回收同样的内存,不然会出大问题。看以下代码:
void fun(){
Character *heros = new Character[5]();
delete mario;
}
[dyamo@~/code 17:08]$ g++ -o character.exe character.cpp
[dyamo@~/code 17:08]$ ./character.exe
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
角色被销毁!
Segmentation fault
我们会发现,只有一个hero被销毁了,然后就报段错误了。为什么会这样子呢?有同学就想:解决这个问题还不简单,写个循环一个个delete掉呗。
void fun(){
Character *heros = new Character[5]();
for(int i = 0; i < 5; ++i){
delete heros + i;
}
}
运行之后还是一样的结果。原因是这样的:在第一个delete之后,其实后面的都已经被内存回收了。注意这里我说的是“内存回收”,并不是“销毁”,也就是说对象构造函数还没调用就已经被内存回收掉了。这时候你再用delete去销毁已经被内存回收的对象,就会报段错误。而且这会造成另外一个严重的问题——内存泄漏(Memory Leak),这个在后面析构函数和拷贝构造函数会具体讲。
那么怎么正确回收内存呢?就和上面说的用了new就一定要用delete回收一样,用了new[]就一定要用delete[]来回收。
void fun(){
Character *heros = new Character[5]();
delete[] heros;
}
[dyamo@~/code 17:08]$ g++ -o character.exe character.cpp
[dyamo@~/code 17:08]$ ./character.exe
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
角色被销毁!
角色被销毁!
角色被销毁!
角色被销毁!
角色被销毁!
构造函数的初始化列表
这里讲一下构造函数的初始化列表。之前说过声明了一定要定义才可以使用,初始化列表就是用于做定义的,也可以理解为给对象的变量进行初始化。
class Character {
private:
int hp