1、C+面向对象程序设计教学内容 第1章 C+概述第2章 类和对象第3章 面向对象程序设计概述第4章 进一步学习类和对象第5章 堆与复制构造函数第6章 继承性:派生类第7章 运算符重载第8章 虚函数和多态性第9章 模板第10章 类库和C+的标准模板库STL第11章 输入输出流第12章 异常处理第5章 堆与复制构造函数5.1 堆5.2 需要new和delete的原因5.3 默认的复制构造函数5.4 自定义复制构造函数5.1 堆 Heap 堆是按动态方式分配的内存区域。在程序中需要空间存放数据时,就申请动态内存单元,使用完毕后释放动态内存单元。这种动态内存分配方式能够显著地提高内存的利用率。C+程序的
2、内存布局 Stack 栈Heap 堆Global data 全局数据区程序代码区program code 代码区存放程序的代码(可执行指令);全局数据区存放全局变量、静态变量、常量。固定存储区域栈是存放程序中的所有动态局部变量、函数参数、函数返回值等信息的一块内存区域。在固定存储区域与堆栈之间的自由区域称为堆 栈 Stack 栈的内存管理严格遵循后进先出(LIFO:Last in,First Out)的顺序,即释放栈中对象所占内存时的顺序刚好与给这些对象分配栈中内存时的顺序相反,这一点正是实现函数调用所需要的。从栈中分配内存效率特别高,对栈的充分利用是C/C+编译程序能产生优质高效代码的原因之
3、一。动态内存分配 堆的内存是以动态分配方式管理的。所谓动态分配的内存是在程序运行期间获得的。动态存储分配方式允许我们的程序可以在执行期间根据实际的需要根据实际的需要存放的数据量来申请合适数量的内存单元。这种动态分配方式不但能够提高内存的利用率,而且对于链表和二叉树等动态数据结构特别有用。动态内存申请和释放 用函数malloc()分配的动态内存必须用函数free()释放;用new申请的动态内存必须用delete 释放。因为在C+程序中,从堆中获取的内存单元不会被自动释放,因此必须使用函数free()或者用delete释放这种内存。如果从堆中获取的内存在使用完后没有被释放,这部分内存在程序结束之前
4、会一直被占用,这种情况被称为“内存泄漏”。5.2 需要new和delete的原因 5.2.1 需要new和delete的原因 5.2.2 在堆上创建对象5.2.1 需要new和delete的原因 对自定义的类类型,使用函数malloc()给对象分配动态空间时不能自动调用构造函数;使用函数free()释放对象所占用的动态空间时也不能调用析构函数。C+语言创建了new和delete两个运算符来满足面向对象的新特性,在C+语言程序中,我们应该使用new和delete来创建和销毁类的对象。5.2.2 在堆上创建对象 使用new运算符在堆上创建对象时能够自动调用构造函数进行初始化;使用 delete运算
5、符释放对象占用的动态内存时能够自动调用该对象的析构函数进行善后处理。【例5.1】在堆上创建对象#include using namespace std;class Square int side;public:Square(int x)side=x;coutConstructiongn;Square()coutDestructiongn;void display()cout side display();delete ps;/自动调用析构函数,然后释放堆内存 return 0;该程序运行后的输出结果如下:Constructiong10Destructiong【例5.2】传值调用例子 class
6、 Square int side;public:Square(int x)side=x;coutConstructiongn;Square()coutDestructiongn;void display()cout side n;void f(Square ob)/对象作为函数参数 ob.display();【例5.2】传值调用例子(续)int main()Square s(10);f(s);/对象s以传值方式传送给临时对象obs.display();return 0;该程序运行后的输出结果如下:Constructiong10DestructiongDestructiong10对象的副本 当一
7、个对象被作为参数传递给函数时,同时也创建了该对象的副本 这个副本将成为函数的参数。也就是说,创建了一个新的对象创建了一个新的对象。当函数结束时,作为函数的实际参数的副本将被销毁。也就是说,一个对象被销毁了一个对象被销毁了两个问题 第一个问题:在创建对象的副本时是否调用了构造函数?第二个问题:当销毁对象的副本时是否调用了析构函数?第一个问题的答案 首先,在调用函数的时候,程序创建了一个对象的副本作为形式参数,此时普通的构造函数(normal constructor)并没有被调用,而是复制构造函数复制构造函数(copy constructor)被调用。为什么需要复制构造函数 由于普通的构造函数通常
8、用于初始化对象的某些成员,因此就不能调用普通构造函数创建对象的副本,因为这样产生的对象可能与现有的对象不完全相同。当把一个对象传递给函数时,我们需要使用的是对象的当前状态对象的当前状态,而不是初始状态 5.3 默认的复制构造函数 复制构造函数定义了如何创建一个对象的副本。如果一个类中没有显式地定义类的复制构造函数,那么C+将提供一个默认的复制构默认的复制构造函数造函数(default copy constructor)。默认的复制构造函数将以按位复制的形式创建一个对象的副本,即创建一个与原对象一模一样的副本。第二个问题的答案 当函数结束时,由于作为参数的对象副本超出了作用域,因此它将被销毁,从
9、而调用了析构函数。结论 当创建一个对象的副本作为函数的参数时,复制构造函数被调用。当对象的副本被销毁时(通常因为函数返回而超出作用域),析构函数被调用。自定义复制构造函数的例子Square(const Square&ob)side=ob.side;coutCopy Constructiongn;如果程序员自定义了一个复制构造函数,编译时将不会产生默认的复制构造函数。【例5.2】的另一种版本 class Square int side;public:Square(int x)side=x;coutConstructiongn;Square(const Square&ob)/自定义复制构造函数 s
10、ide=ob.side;coutCopy Constructiongn;Square()coutDestructiongn;void display()cout side n;void f(Square ob)/对象作为函数参数 ob.display();【例5.2】的另一种版本(续)int main()Square s(10);f(s);/对象s以传值方式传送给临时对象obs.display();return 0;该程序运行后的输出结果如下:ConstructiongCopy Constructiong10Destructiong10Destructiong自定义复制构造函数 在很多情况下,
11、使用按位复制的方法来创建一个相同的对象副本是可行的,这时我们可以直接使用C+语言提供的默认复制构造函数。但是,在某些情况下使用默认的复制构造函数会出现问题(例如【例5.3】),这时我们需要自己创建复制构造函数。【例5.3】class my_string char*s;public:my_string(char*str)s=new charstrlen(str)+1;coutAllocating room for sn;strcpy(s,str);my_string()if(s)delete s;cout Freeing sn;void show()cout s n;【例5.3】(续)void
12、display(my_string ob)ob.show();int main()my_string obj(Hello!);display(obj);obj.show();/这条语句输出的是垃圾数据return 0;使用默认的复制构造函数出现了问题该程序运行后的输出结果如下:Allocating room for sHello!Freeing s(撤销对象obj的副本ob时调用析构函数)葺葺葺葺葺(这是输出的垃圾数据,不同的系统中输出的内容会不相同)Freeing s(撤销对象obj时调用析构函数)【例5.3】C+程序中的对象内存分布示意图 Hello实参对象obj形参对象ob(a)从函数d
13、isplay()返回前 SS【例5.3】C+程序中的对象内存分布示意图 实参对象obj(b)从函数display()返回后 S解决这个问题的方法之一void display(my_string&ob)ob.show();经过修改后,程序运行后的输出结果如下:Allocating room for sHello!Hello!Freeing s避免复制对象初始化(initialization)(1)当一个对象副本被作为参数传递给函数时。my_string y;display(y);/y被作为参数传递给函数(2)当一个对象被另一个对象显式地初始化时,例如在对象的声明中。my_string x=y;/
14、对象y被显式地用来初始化对象x(3)当创建一个临时对象时(最常见的情况是作为函数的返回值)。y=func2();/y得到一个返回对象5.4.2 复制构造函数与函数参数 当对象被作为参数传递给函数时,会产生该对象的一个副本。如果我们创建了自定义的复制构造函数,那么这个自定义的复制构造函数将被调用来制作这个对象副本。【例5.4】自定义的复制构造函数#include#include using namespace std;class my_string char*s;public:my_string(char*str);/普通构造函数my_string(const my_string&obj);/自
15、定义的复制构造函数my_string()if(s)delete s;cout Freeing sn;void show()cout s n;【例5.4】(续1)my_string:my_string(char*str)/普通构造函数s=new charstrlen(str)+1;coutAllocating room for sn;strcpy(s,str);my_string:my_string(const my_string&obj)/自定义的复制构造函数s=new charstrlen(obj.s)+1;strcpy(s,obj.s);coutCopy constructor calle
16、d.n;【例5.4】(续2)void display(my_string ob)ob.show();int main()my_string obj(Hello!);display(obj);obj.show();return 0;【例5.4】(续3)该程序运行后的输出结果如下:Allocating room for sCopy constructor called.Hello!Freeing sHello!Freeing s【例5.4】中C+程序的对象内存分布示意图 Hello实参对象obj形参对象ob(a)从函数display()返回前 SSHello【例5.4】C+程序中的对象内存分布示意
17、图 实参对象obj(b)从函数display()返回后 SHello5.4.3 复制构造函数与初始化 当使用一个对象来初始化另一个对象时,也将调用复制构造函数【例5.5】初始化对象时调用复制构造函数。#include#include using namespace std;class my_string char*s;public:my_string(char*str);/普通构造函数my_string(const my_string&obj);/自定义的复制构造函数my_string()if(s)delete s;cout Freeing sn;void show()cout s n;【例5
18、.5】(续1)my_string:my_string(char*str)/普通构造函数s=new charstrlen(str)+1;coutNormal constructor called.n;strcpy(s,str);my_string:my_string(const my_string&obj)/自定义的复制构造函数s=new charstrlen(obj.s)+1;strcpy(s,obj.s);coutCopy constructor called.n;【例5.5】(续2)int main()my_string obj(Hello!);/调用普通构造函数my_string ob1
19、(obj);/调用复制构造函数my_string ob2=ob1;/调用复制构造函数ob1.show();ob2.show();return 0;【例5.5】(续3)该程序运行后的输出结果如下:Normal constructor called.Copy constructor called.Copy constructor called.Hello!Hello!Freeing sFreeing sFreeing s复制构造函数不会影响赋值运算记住,复制构造函数只有在初始化对象时才被调用。例如,下面的代码将不会调用在前面程序中定义的复制构造函数:my_string s1(“Hello”),s2
20、(“world”);/s1=s2;赋值运算5.4.4 在返回对象时使用复制构造函数当函数返回对象时,函数创建了一个临时对象来保存要返回的值,而函数所返回的对象实际上是这个临时对象。在对象的值被返回之后,临时对象将被销毁。在某些情况下,这会引起不可预料的错误。【例5.6】函数返回对象#include#include using namespace std;class my_string char*s;public:my_string(char*str);/普通构造函数my_string()if(s)delete s;cout Freeing sn;void show()cout s n;【例5.
21、6】(续1)my_string:my_string(char*str)/普通构造函数s=new charstrlen(str)+1;coutNormal constructor called.n;strcpy(s,str);my_string input()/返回一个my_string类型的对象char instr80;coutinstr;my_string ob(instr);/调用普通构造函数return ob;【例5.6】(续2)int main()my_string obj=input();/调用默认的复制构造函数/在函数input()中将调用普通构造函数obj.show();/输出的
22、是垃圾数据return 0;该程序运行后的输出结果如下:Enter a string:HelloNormal constructor called.Freeing s葺葺葺葺葺葺葺葺葺葺輛(这是输出的垃圾数据,不同的系统中输出的内容会不相同)【例5.7】使用自定义的复制构造函数避免错误#include#include using namespace std;class my_string char*s;public:my_string(char*str);/普通构造函数my_string(const my_string&obj);/自定义的复制构造函数my_string()if(s)delet
23、e s;cout Freeing sn;void show()cout s n;【例5.7】(续1)my_string:my_string(char*str)/普通构造函数s=new charstrlen(str)+1;coutNormal constructor called.n;strcpy(s,str);/自定义的复制构造函数my_string:my_string(const my_string&obj)s=new charstrlen(obj.s)+1;strcpy(s,obj.s);coutCopy constructor called.n;【例5.7】(续2)my_string i
24、nput()/返回一个my_string类型的对象char instr80;coutinstr;my_string ob(instr);/调用普通构造函数return ob;/调用复制构造函数int main()my_string obj=input();/调用复制构造函数/在函数input()中将调用普通构造函数obj.show();return 0;【例5.7】(续3)该程序运行后的输出结果如下:Enter a string:HelloNormal constructor called.Copy constructor called.Freeing sHelloFreeing s 一个经验 如果你的类需要析构函数来释放资源,则它也需要一个自定义的复制构造函数。