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