C++类与对象(new与delete初始化列表析构函数拷贝构造函数)
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;
return0;
}
[dyamo@~/code 17:08]$ g++ - character.cpp
[dyamo@~/code 17:08]$ ./
新建英雄成功!
⾓⾊被销毁!
程序结束
可见mario的析构函数在“程序结束”之前被调⽤了,也就是说函数内实例化的对象在离开定义域之后会⾃动被销毁。
new / delete
那有没有什么⽅法能让对象长时间存在内?那就是使⽤C++的关键字new,为对象分配堆上的内存。在对象的实例化上,关键字new做了以下两件事:
为对象分配堆上的内存空间,并返回对象地址;
调⽤该类的构造函数;
学过C的同学可能⽐较属性malloc这个函数,这个函数也是给变量或者结构体分配堆上内存的。当然malloc也可以给类的对象分配堆上内存,但是有⼀个问题,那就是malloc并不会调⽤类的构造函数,他只是单纯的划⼀⽚内存区域给你,划多少由你说了算;⽽且他返回的是void *指针,想要使⽤的话还得进⾏强制类型转化才能使⽤。所以要为对象分配堆上内存,还是⽤得⽤new。还是⽤上⾯的⾓⾊类例⼦:
void fun(){
//实例化对象并返回对象地址
Character *mario_ptr=new Character();
}
int main(){
fun();
cout <<"程序结束"<< endl;
return0;
}
[dyamo@~/code 17:08]$ g++ - character.cpp
[dyamo@~/code 17:08]$ ./
新建英雄成功!
程序结束
可见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++ - character.cpp
[dyamo@~/code 17:08]$ ./
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
⾓⾊被销毁!
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++ - character.cpp
[dyamo@~/code 17:08]$ ./
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
新建英雄成功!
⾓⾊被销毁!
⾓⾊被销毁!
⾓⾊被销毁!
⾓⾊被销毁!
⾓⾊被销毁!
构造函数的初始化列表
这⾥讲⼀下构造函数的初始化列表。之前说过声明了⼀定要定义才可以使⽤,初始化列表就是⽤于做定义的,也可以理解为给对象的变量进⾏初始化。
class Character {
private:
int hp;
int mp;
const string name;
public:
Character():hp(0),mp(0),name("马⾥奥")
{
cout <<"新建英雄成功!"<< endl;
}
~Character(){
cout <<"⾓⾊被销毁!"<< endl;
}
};
构造函数后⾯跟着的冒号就是初始列表了,他的优先级是⽐函数体更⾼的,也就是他会⽐构造函数更先执⾏。有的同学可能就很疑惑:这样的操作我也可以在构造函数内直接写hp = 0和mp = 0啊?是,确实可以,但是有⼀种情况就不可以,那就是引⽤的初始化。之前也说了引⽤必须在声明的时候就给他定义了,不然编译⽆法通过。还有⼀种情况就是上⾯代码的const string name的初始化,const的意思是常量(以后也会提到的),也就是name这个字符串是个常量,是不能够修改的,所以也就没有办法 name = “马⾥奥” 这样进⾏定义。⽽且和引⽤⼀样声明了就⼀定要定义,虽然不定义的话编译可以通过,但是在后⾯你也没有办法修改他啊,那声明他⼲嘛呢。
以上两种情况是常见的必须要使⽤初始化列表进⾏初始化的,还有后⾯会说到类的继承,⽗类的初始化也是要通过初始化列表进⾏初始化的。成员变量的初始化,还有如果成员变量⾥⾯如果有对象成员,也可以通过初始化列表进⾏对象的构造。这两种情况都不是必须要⽤初始化列表进⾏初始化的(也可以在函数体赋值),但是初始化列表还有⼀个好处,那就是效率上会⽐直接赋值要快那么⼀丢丢。所以经常⽤初始化列表进⾏成员变量的是个好习惯。
class A {
private:
int val;
public:
A(int v):val(v){}
};
public B {
private:
A a;
int val;
public://这⾥初始化列表会调⽤A的构造函数
B(int v):A(v),val(v){
//a = A(v)也可
}
实例化类和实例化对象}
析构函数
先前也将了,析构函数是在对象离开作⽤域或者被delete的时候调⽤的,⽤于做销毁⼯作的函数。可是有什么东西似要被销毁的呢?之前说不好好销毁对象会造成内存泄漏⼜是怎么造成的呢?那我们先来具体说说内存泄漏。
内存泄漏(Memory Leak)
这是C/C++程序猿⽼⽣常谈的问题。学C的同学肯定也是会接触的,⽐如⽼师或者教材会教你⽤了malloc函数就⼀定要⽤free去释放,其中的原因就是内存泄漏问题。
具体的定义还是引⽤⼀下百度百科吧:
内存泄漏(Memory Leak)是指程序中⼰动态分配的堆内存由于某种原因程序未释放或⽆法释放,造成系统内存的浪费,导致程序运⾏速度减慢甚⾄系统崩溃等严重后果。
说⽩了就是⽤new分配太多内存并且没有delete释放,程序越执⾏越卡呗。看起来似乎很好解决,只有new了记得delete就好了。如果真这么好解决,C++就不会引⼊智能指针了。很多时候就是不经意间,
内存分配出去就忘记回收了,你甚⾄不知道是什么东西占着内存不释放。其中最容易造成内存泄漏的,就属析构函数和拷贝构造函数了。析构函数还好,只要记得类⾥边那个成员变量是⽤new分配的,在析构函数⾥delete掉就好的。⽐较复杂的情况呢就是在继承的虚拟析构函数,这个在以后讲了继承和多态的时候我再详细讲。然后就是拷贝构造函数,这是最容易造成内存泄漏的,这个会在本篇详细讲。
析构函数造成的内存泄露
还是⽤⾓⾊类来举例⼦,看以下代码:
class Weapon;//前向声明
class Character {
private:
int hp;
int mp;
const string name;
Weapon *wp_ptr;
public:
Character(int hp,int mp, string name)
:hp(hp)
,mp(mp)
,name(name)
,wp_ptr(new Weapon)
{
cout <<"新建英雄成功!"<< endl;
}
~Character(){
cout <<"⾓⾊被销毁!"<< endl;
}
};
class Weapon {
//具体的定义
};
注意这⾥我⽤了个前向声明(forward declaration)来声明⼀个Weapon类,然后在最后才给出这个类的定义。这种⽤法⾮常常见,学过C的同学可能会⽐较清楚。C在声明函数的时候,有时候会在main函数前声明,然后在main函数后再给出定义。如果函数在main函数后进⾏声明和定义,那在main函数⾥⾯就没办法调⽤他。所以要先声明函数,先告诉编译器我是有这个函数的,只不过现在还没有给出定义。然后在使⽤该函数的时候,编译器就会⾃动取寻函数的定义,没到的话编译器会认为你在骗他,直接就报段错误。所以我⼀开始就说了,要养成声明了就⼀定要定义的好习惯。 前向声明的类也是这样⼀个道理。但是有⼀点要特别注意,那就是前向声明的类在声明为别的类的成员变量的时候,⼀定要声明成指针或者引⽤。 千万别声明成对象,声明成对象就相当于使⽤了⼀个未定义的类,⼀运
⾏就报段错误。看过C语⾔头⽂件的同学可能就知道,其实头⽂件⾥⾯就是⼀堆的声明,偶尔会有些定义但是不多。
回到正题,这时候我们的⾓⾊可以拿武器了。然后游戏上线运营,玩家们纷纷注册上线,玩了⼏天之后发现游戏越玩越卡。⼲运维的就去查看状况,⽤⼯具查内存⼀看,⾥⾯⼀⼤堆的武器数据。运维反馈给开发程序员,程序员只能连夜加班哪⾥出现了内存泄漏问题。最后没想到居然是在析构函数出了问题——玩家上线之后,就会给他的⾓⾊分配武器内存,下线之后析构函数调⽤了但是没有delete掉武器,导致武器数据在内存越堆越多。这样的低级错误是很难被察觉的,更何况真正的⼤型游戏代码量是多么的恐怖。
所以正确的代码应该是这样的:
~Character(){
delete wp_ptr;
cout <<"⾓⾊被销毁!"<< endl;
}
拷贝构造函数
什么时候拷贝构造函数被调⽤
拷贝构造函数,顾名思义就是拷贝的时候被调⽤的,⽽且他还是构造函数的⼀种。我们来看看拷贝构造函数的本体:
class Character {
public:
//构造函数
Character(){};
//拷贝构造函数,没有写的话编译器会给你补上,但不好
Character(Character &){};
};
我们补上函数体然后调⽤看看:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论