C++类与对象 (new与delete初始化列表析构函数拷贝构造函数 )

    技术2022-07-17  97

    C++类与对象 (new与delete / 初始化列表 / 析构函数 / 拷贝构造函数 )

    类的构造与析构new / delete构造函数的初始化列表析构函数内存泄漏(Memory Leak)析构函数造成的内存泄露 拷贝构造函数什么时候拷贝构造函数被调用拷贝构造函数的禁忌深拷贝与浅拷贝浅拷贝带来的内存泄露 总结

    类的构造与析构

    上篇博文我简单地介绍了一下什么是类的构造函数,什么时候构造函数被调用,并通过代码例子介绍了类的实例化。

    那既然有构造,那肯定是有销毁的啦,那就是析构函数干的事情。析构函数在对象被销毁的时候调用,被销毁有两种情况:

    离开作用域调用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; 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 &) {}; };

    我们补上函数体然后调用看看:

    #include <iostream> using namespace std; class Character { private: int hp; int mp; const string name; public: Character(int hp, int mp, string name) : hp(hp), mp(mp), name(name) { cout << "新建角色成功!" << endl; }; Character(const Character &another) //这里一般要加上const,因为我不希望拷贝的过程中原版被改变 : hp(another.hp) , mp(another.mp) , name(another.name) { cout << "拷贝角色成功!" << endl; cout << "name: " << name << endl; cout << "hp: " << hp << endl; cout << "mp: " << mp << endl; }; //因为没有分配内存,所以析构函数里面可以不写任何东西 ~Character() {} }; int main(){ Character mario(100, 50, "马里奥"); Character copy = mario; //Character *copy = new Character(mario); 也是可以的 return 0; } [dyamo@~/code 20:28]$ g++ -o copy.exe copy.cpp [dyamo@~/code 20:28]$ ./copy.exe 新建角色成功! 拷贝角色成功! name: 马里奥 hp: 100 mp: 50

    可以看到,copy这个对象并没有调用构造函数,而是调用了拷贝构造函数,这是拷贝构造函数的调用地方之一。看到这里眼尖的小伙伴可能就要问了:欸这些成员变量不都是是private吗,为什么能直接访问?我只能说:盲生,你发现了华点!对没错,他们确实是private的,但是就是那么不讲道理,就是能访问,而且也只能在拷贝构造函数里面访问。 你可以理解为自家兄弟害羞啥呢,快给我康康[滑稽] !

    还有一个经常要用到拷贝构造函数的地方,也是最容易出问题的地方,那就是对象作为函数参数传递时。之前也将了,C语言的函数参数传递是通过拷贝实现的,然后还介绍了什么似实参什么是形参,具体请查看我的一篇博客。对象作为函数参数也是一个道理,也是要通过拷贝实现的。看以下代码:

    #include <iostream> using namespace std; class Character { private: int hp; int mp; const string name; public: Character(int hp, int mp, string name) : hp(hp), mp(mp), name(name) { cout << "新建角色成功!" << endl; }; Character(const Character &another) //这里一般要加上const,因为我不希望拷贝的过程中原版被改变 : hp(another.hp) , mp(another.mp) , name(another.name) { cout << "拷贝角色成功!" << endl; }; //因为没有分配内存,所以析构函数里面可以不写任何东西 ~Character() {} void print() { cout << "name: " << name << endl; cout << "hp: " << hp << endl; cout << "mp: " << mp << endl; } }; void fun(Character hero); //声明函数 int main(){ Character mario(100, 50, "马里奥"); fun(mario); return 0; } void fun(Character hero){ //定义函数 cout << "fun()" << endl; hero.print(); } [dyamo@~/code 20:40]$ g++ -o copy.exe copy.cpp [dyamo@~/code 20:40]$ ./copy.exe 拷贝角色成功! fun() name: 马里奥 hp: 100 mp: 50

    可以看到函数参数的传递的的确确是靠拷贝实现的。之前也说过,如果对象很大,而且函数不停被调用,就会消耗一部分系统资源在无关紧要的拷贝上面。要解决这个问题,函数参数要么用引用,要么用指针,虽然指针也会被拷贝,但指针本身才4个字节,最多就8个字节,拷贝一下小问题。

    拷贝构造函数的禁忌

    之前说到拷贝构造函数的原型是:

    Character(Character &) {};

    这里有一个必须要时刻记住的点,那就是参数一定要用该类的引用! 引用! 引用!(重要的事情说三遍)

    那为什么要用引用呢?之前讲过,C/C++的函数参数传递靠的是拷贝。那么传递的是对象的话,那势必会调用该类的拷贝构造函数。现在假设拷贝构造函数的参数不是引用,看以下代码:

    int main(){ Character mario; Character luigi = mario; //调用拷贝构造函数 return 0; }

    构造luigi对象时调用Character的拷贝构造函数,给其传递的参数是mario。然而传递mario需要拷贝一份mario,所以又调用Character的拷贝构造函数,给其传递的参数是mario。然而传递mario需要拷贝一份mario,所以又调用… … 如此往复,无止限地调用拷贝构造函数,势必会爆栈,造成程序崩溃。

    所以拷贝构造函数的参数一定是该类的引用, 因为引用是别名嘛,传递参数时就不用拷贝了,也就不会造成无限调用拷贝构造函数的情况了。

    深拷贝与浅拷贝

    深拷贝与浅拷贝的定义参考百度,这里我自己简单概括一下:

    深拷贝:按照程序员自己的意愿来拷贝,就是自己写代码来控制拷贝过程,就像上面写的代码一样。浅拷贝:按字节拷贝,就是无脑复制。原对和拷贝对象在字节上是一模一样的。

    如果自己写拷贝构造函数,编译器就会自动给我们生成一个拷贝构造函数,这个拷贝构造函数就是浅拷贝的,使用起来会有一定的风险。主要风险在于指针,还有就是内存泄漏问题。这里简单说说指针问题。

    如果说在一个类中有一个数组指针,你希望这个类在拷贝的时候,给新的对象一个新的数组但是与原数组的值相同。如果用编译器生成的拷贝构造函数,也就是浅拷贝,或者说是按字节拷贝,就会出现 copy.ptr = a.ptr,就是完完全全将指针赋值,两个数组指针指向同一个数组。copy对象修改了数组里边的值,a对象的数组里边的值也跟着改变,这不是我希望看到的。如果要把浅拷贝改成深拷贝,就要这么改:

    A(const A &anothoer){ this->ptr = new int[len]; //新开一个数组 for(int i = 0; i < len; ++i){ *(this->ptr + i) = *another.ptr; } }

    这样两个对象的数组就不会冲突了,虽然有些麻烦,但不得不这么做。

    浅拷贝带来的内存泄露

    好了又是老生常谈的内存泄露问题,这个浅拷贝怎么也泄露了,好吧好吧那我再析构函数里面delete掉总行了吧。NO NO NO,这个浅拷贝带来的内存泄漏问题更加的严重,还有可能会导致段错误直接让程序崩溃。

    代码例子还是看我们的角色类,我们不写拷贝构造函数:

    #include <iostream> using namespace std; 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() { delete wp_ptr; cout << "角色被销毁!" << endl; } void print() { cout << "name: " << name << endl; cout << "hp: " << hp << endl; cout << "mp: " << mp << endl; } }; void fun(Character hero) { cout << "fun()" << endl; hero.print(); } int main() { Character *mario_ptr = new Character (100, 50, "马里奥"); fun(*mario_ptr); delete mario_ptr; cout << "程序正常结束" << endl; return 0; } [dyamo@~/code 21:18]$ g++ -o copy.exe copy.cpp [dyamo@~/code 21:18]$ ./copy.exe 新建英雄成功! fun() name: 马里奥 hp: 100 mp: 50 角色被销毁! *** Error in `./copy.exe': double free or corruption (fasttop): 0x00000000017d8c60 *** ======= Backtrace: ========= /lib64/libc.so.6(+0x81299)[0x7f80221f7299] ./copy.exe[0x400f4a] ./copy.exe[0x400da3] /lib64/libc.so.6(__libc_start_main+0xf5)[0x7f8022198555] ./copy.exe[0x400c19] ======= Memory map: ======== 00400000-00402000 r-xp 00000000 fd:01 1058810 /home/dyamo/code/copy.exe 00601000-00602000 r--p 00001000 fd:01 1058810 /home/dyamo/code/copy.exe 00602000-00603000 rw-p 00002000 fd:01 1058810 /home/dyamo/code/copy.exe 017c7000-017f9000 rw-p 00000000 00:00 0 [heap] 7f801c000000-7f801c021000 rw-p 00000000 00:00 0 7f801c021000-7f8020000000 ---p 00000000 00:00 0 7f8022176000-7f8022339000 r-xp 00000000 fd:01 265441 /usr/lib64/libc-2.17.so 7f8022339000-7f8022539000 ---p 001c3000 fd:01 265441 /usr/lib64/libc-2.17.so 7f8022539000-7f802253d000 r--p 001c3000 fd:01 265441 /usr/lib64/libc-2.17.so 7f802253d000-7f802253f000 rw-p 001c7000 fd:01 265441 /usr/lib64/libc-2.17.so 7f802253f000-7f8022544000 rw-p 00000000 00:00 0 7f8022544000-7f8022559000 r-xp 00000000 fd:01 277386 /usr/lib64/libgcc_s-4.8.5-20150702.so.1 7f8022559000-7f8022758000 ---p 00015000 fd:01 277386 /usr/lib64/libgcc_s-4.8.5-20150702.so.1 7f8022758000-7f8022759000 r--p 00014000 fd:01 277386 /usr/lib64/libgcc_s-4.8.5-20150702.so.1 7f8022759000-7f802275a000 rw-p 00015000 fd:01 277386 /usr/lib64/libgcc_s-4.8.5-20150702.so.1 7f802275a000-7f802285b000 r-xp 00000000 fd:01 277396 /usr/lib64/libm-2.17.so 7f802285b000-7f8022a5a000 ---p 00101000 fd:01 277396 /usr/lib64/libm-2.17.so 7f8022a5a000-7f8022a5b000 r--p 00100000 fd:01 277396 /usr/lib64/libm-2.17.so 7f8022a5b000-7f8022a5c000 rw-p 00101000 fd:01 277396 /usr/lib64/libm-2.17.so 7f8022a5c000-7f8022c24000 r-xp 00000000 fd:01 285536 /usr/lib64/libstdc++.so.6.0.26 7f8022c24000-7f8022e24000 ---p 001c8000 fd:01 285536 /usr/lib64/libstdc++.so.6.0.26 7f8022e24000-7f8022e2f000 r--p 001c8000 fd:01 285536 /usr/lib64/libstdc++.so.6.0.26 7f8022e2f000-7f8022e32000 rw-p 001d3000 fd:01 285536 /usr/lib64/libstdc++.so.6.0.26 7f8022e32000-7f8022e35000 rw-p 00000000 00:00 0 7f8022e35000-7f8022e57000 r-xp 00000000 fd:01 284913 /usr/lib64/ld-2.17.so 7f8023048000-7f802304d000 rw-p 00000000 00:00 0 7f8023053000-7f8023056000 rw-p 00000000 00:00 0 7f8023056000-7f8023057000 r--p 00021000 fd:01 284913 /usr/lib64/ld-2.17.so 7f8023057000-7f8023058000 rw-p 00022000 fd:01 284913 /usr/lib64/ld-2.17.so 7f8023058000-7f8023059000 rw-p 00000000 00:00 0 7ffe96d0a000-7ffe96d2b000 rw-p 00000000 00:00 0 [stack] 7ffe96dc0000-7ffe96dc2000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] Aborted

    看起来程序运行得十分凶险,那为什么会出现这样的错误呢?说好的内存泄漏又在哪呢?别急,看我慢慢给你捋清楚。

    你先想一下,在整个程序中,mario这个对象被析构了多少次?聪明的同学一看就知道有两次:一次是fun()函数里面的拷贝对象离开了作用域,被析构了一次,还有一次就是main()函数里的delete。那mario的wp_ptr被delete了多少次?当然也是两次,问题就出在这个第二次delete wp_ptr。上面浅拷贝带来的指针问题也讲了,这个拷贝对象的wp_ptr和原对象的wp_ptr的值是一样的,也就是说他们两个都指向了同一个内存地址。 那么在第一次delete,也就是fun()函数里面的拷贝对象离开了作用域,析构函数被调用,wp_ptr被顺利delete。问题在第二次,main()函数里面的mario对象,他的wp_ptr已经被delete掉了,因为他们指的是同一个位置。已经被delete掉的东西再被delete一次,就会出现上面的问题。

    原因就是这么个原因,知道原因的同学可以就会想:欸那么将main()函数里面的delete wp_ptr这句话删掉不就解决问题了。确实,把那句话删除之后,程序能够顺利运行并且顺利结束。

    [dyamo@~/code 21:22]$ g++ -o copy.exe copy.cpp [dyamo@~/code 21:22]$ ./copy.exe 新建英雄成功! fun() name: 马里奥 hp: 100 mp: 50 角色被销毁! 程序正常结束

    但这就带来了第二个问题——内存泄漏。来了来了他来了,内存怎么又双叒叕泄漏了,不是已经好好地delete掉了吗。确实,fun()函数里面的copy对象的wp_ptr有被好好delete掉,而且他和和原对象指向同一块内存,按道理这块内存有被好好回收掉。可是计算机那会那么简单,要是他能像人一样正常思考就不会有这么多破事了。

    这里我简单说说为什么泄露了:copy对象的wp_ptr被delete掉了,对应内存里边的内容也有好好地被清理掉,但原对象的wp_ptr还拿着这块内存的所有权。对,原对象现在是拿了个寂寞,而且处境十分尬尴:一来他没有办法delete掉他,真正做到回收内存;二来他也没 有办法使用wp_ptr,因为对象已经被销毁了,再访问的话就报段错误了。这段内存空间就是一烂摊子,用也不是回收也不是。那就丢了吧,抛弃旧爱另寻新欢。这个烂摊子就丢在那,就算没了主人,内存也是会认为他是属于某某某的,也就不会回收了。所以久而久之,这样的烂摊子越来越多了,就是所谓的内存泄漏了。

    那如何避免上述情况,就只能老老实实写一下拷贝构造函数了,不要用编译器生成的:

    #include <iostream> using namespace std; class Weapon { Weapon(Weapon &) { cout << "武器也进行了拷贝" << endl; } }; 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(const Character &another) : hp(another.hp) , mp(another.mp) , name(another.name) , wp_ptr(new Weapon(*another.wp_ptr)) //调用Weapon类的拷贝构造函数 { cout << "拷贝角色成功!" << endl; }; ~Character() { delete wp_ptr; cout << "角色被销毁!" << endl; } void print() { cout << "name: " << name << endl; cout << "hp: " << hp << endl; cout << "mp: " << mp << endl; } }; void fun(Character hero) { cout << "fun()" << endl; hero.print(); } int main() { Character *mario_ptr = new Character (100, 50, "马里奥"); fun(*mario_ptr); delete mario_ptr; cout << "程序正常结束" << endl; return 0; } [dyamo@~/code 21:53]$ g++ -o copy.exe copy.cpp [dyamo@~/code 21:53]$ ./copy.exe 新建英雄成功! 拷贝角色成功! fun() name: 马里奥 hp: 100 mp: 50 角色被销毁! 角色被销毁! 程序正常结束

    总结

    到此类与对象的三大函数算是原原本本地讲完了,还有没讲的就是虚拟析构函数,这个到后面将到继承和多态的时候在详细讲,也是解决内存泄漏问题的。

    这一篇的知识点比较多,最重要的知识点就是内存泄漏及其处理方法,在明白了原理之后,处理起来就会比较的方便。不明白的同学可以反反复复地看,结合代码慢慢琢磨。这个真的是C/C++程序员老生常谈的问题,也花了我比较多的篇幅去一一讲明白。虽然到后面有比较方便的工具来解决,那就是智能指针。但是我还是认为得去了解和认识产生内存泄漏的原因到底是什么,以及我要怎么去解决这个问题。在了解了原因之后,再去学习怎么用工具,就会特别得心应手。

    Processed: 0.009, SQL: 9