构造函数和析构函数.ppt

上传人:小飞机 文档编号:6227603 上传时间:2023-10-07 格式:PPT 页数:50 大小:270.50KB
返回 下载 相关 举报
构造函数和析构函数.ppt_第1页
第1页 / 共50页
构造函数和析构函数.ppt_第2页
第2页 / 共50页
构造函数和析构函数.ppt_第3页
第3页 / 共50页
构造函数和析构函数.ppt_第4页
第4页 / 共50页
构造函数和析构函数.ppt_第5页
第5页 / 共50页
点击查看更多>>
资源描述

《构造函数和析构函数.ppt》由会员分享,可在线阅读,更多相关《构造函数和析构函数.ppt(50页珍藏版)》请在三一办公上搜索。

1、第5章 构造和析构函数,构造函数和析构函数的引入带参构造函数和默认构造函数成员初始化列表拷贝构造函数对象的构造顺序再看new和delete,5.1 构造函数背景,与结构和其他基本数据类型的变量一样:定义一个全局对象会将其所占内存空间清0定义一个局部对象时,不会执行任何初始化动作,其数据成员的值完全依赖于堆栈上的情况。对象应该表达了现实世界中的相应实体,一旦建立对象,其每个数据成员理应有合理的初始值。,5.1 构造函数背景,struct Rectangledouble fWidth;double fHeight;,class Studentpublic:int GetAge()const;pri

2、vate:char m_strName100;int m_nAge;double m_fScore;,C语言使用下面的语法初始化结构变量:Rectangle r=10,20.0;C+中不能使用这种方法,因为某些数据成员可能不是公有成员。/错误:不能在类的作用/域外访问非公有数据成员Student stu=“aa”,10,90;为了避免破坏类的封装性,使用特殊的成员函数来负责对象初始化。,5.1 构造函数要点,与类同名的成员函数称为构造函数(Constructor,ctor),此函数在该类的对象被创建时会被自动调用,负责完成该对象的初始化工作。构造函数不能指定返回类型。每定义一个对象,该对象的c

3、tor被自动调用。Student s;此句背后的动作是首先为对象s分配内存,然后调用ctor初始化对象。,class Studentpublic:Student();int GetAge()const;private:char m_strName100;int m_nAge;double m_fScore;Student:Student()m_nAge=10;m_fScore=100;,5.1 构造函数要点,构造函数不能定义为const的,但是可以用来构造常量对象,这是因为只有当构造函数执行完毕后,对象的常量性才能够建立起来。Rectangle:Rectangle()const/错误的ctor

4、定义对于常量对象只能调用常成员函数的规则,构造函数是个例外。如果一个类对象item是另一个类Container的数据成员,则创建Container的对象时,会首先自动为item调用构造函数。换句话说,编译器会自动在Container的构造函数的函数体前插入对item构造函数的调用。首先调用对象成员的ctor,其次才是自身的ctor。演示 理解构造函数的调用时机,5.2 析构函数背景,一个对象的生存期结束(销毁)时可能需要做些清理工作。打开的文件需要关闭分配的堆内存需要释放如果清理工作对应的函数能够被自动调用,就会减少程序员的工作量,甚至出错的可能。可以使用析构函数(Destructor,dto

5、r)自动完成清理工作。,5.2 析构函数要点,析构函数名必须为加上类名。示例dtor是一类特殊的成员函数,它没有参数,不能重载,不能为其指定返回值。类中至多只有一个dtor。dtor在对象的生存期即将结束时由系统自动调用。析构函数返回后,对象结束其生存期。如果没有清理工作要做,则可以不在类中定义析构函数。【示例】析构函数,class Studentpublic:Student();/ctor Student();/dtor int GetAge()const;private:char m_strName100;int m_nAge;double m_fScore;,5.2 析构函数要点,dto

6、r不能定义为常成员函数,但是常量对象在析构的时候同样会调用dtor。Rectangle:Rectangle()const/错误的dtor定义对于常量对象只能调用常成员函数的规则,dtor是个例外。如果类中包含成员对象,在执行析构函数时将首先执行dtor的函数体,然后为类中的每个对象成员调用析构函数。换句话说,编译器会在类的dtor函数体之后插入对类中每个成员对象的dtor的调用。对dtor的调用顺序正好和对ctor的调用顺序相反。最先构造的对象最后被析构。演示 理解析构函数的调用时机,class Personpublic:Person()m_strName=new char20;/分配堆空间

7、Person()delete m_strName;/释放堆空间 void SetName(const char*name)strcpy(m_strName,name);private:char*m_strName;,5.3 构造函数重载,与dtor不同,ctor允许带参而且可以带不同的参数,ctor允许重载。教材P273页的例子MFC中的CString的类定义在一个构造函数中调用重载的另一个构造函数以简化编程的方法是错误的。因为构造函数只用于创建对象。这一点和普通重载函数不同。,Tdate:Tdate(int m,int d,int y).Tdate:Tdate(int d)/下面的语句只是创

8、建/了另一个对象,并且/很快结束生存期。Tdate(4,d,1995);,专门定义一个成员函数,然后所有的构造函数调用此成员函数即可。解决办法通过给参数设置默认值,可以将重载的构造函数合并为一个。示例,class Tdatepublic:Tdate(int m=4,d=22,y=2005);private:int month;int day;int year;Tdate:Tdate(int m/*=4*/,int d/*=22*/,int y/*=2005*/)month=m;day=d;year=y;cout month/day/year endl;,#include class Tdate

9、public:Tdate()Init(4,15,1995);Tdate(int d)Init(4,d,1995);Tdate(int m,int d)Init(m,d,1995);Tdate(int m,int d,int y)Init(m,d,y);protected:int month;int day;int year;void Init(int m,int d,int y)month=m;day=d;year=y;cout month/day/year endl;,5.4 默认(缺省)构造函数,默认构造函数(dctor):不需要用户指定实参就能被调用的构造函数。(教材上缺明确定义)Tda

10、te:Tdate()Rectangle:Rectangle(float w=0.0,float h=0.0);如果没有在类中定义任何ctor,则C+提供一个dctor,该dctor为无参构造函数,不作任何工作。类的数据成员如果为简单类型则不会被初始化,如果为其它类的对象则自动为其调用构造函数。,class Studentprivate:char name20;,等价于,class Studentpublic:Student()private:char name20;,5.4默认构造函数示例,class Studentpublic:Student(char*pName)strcpy(name,p

11、Name);Student()name0=0;private:char name20;void main(void)/错误:没有匹配的ctor Student s;,只要为类定义了一个带参的构造函数,C+就不再提供默认构造函数,如果还需要无参的构造函数,则必须显式定义。,5.5 使用参数初始化对象,Tdate adate;/调用默认构造函数(dctor)Tdate bdate(10);/OK,Tdate:Tdate(int d)等价于 Tdate bdate=Tdate(10);Tdate cdate(3,12);/OK,Tdate:Tdate(int m,int d)等价于Tdate cda

12、te=Tdate(3,12);Tdate ddate();/此句没有语法错误/Error:left of.GetYear must have class/struct/union type int iy=ddate.GetYear();编译器认为ddate是个函数声明。所以使用dctor构造对象时,对象名后面不能跟()。为什么Tdate bdate(10);不看作是函数声明?根据重载函数的匹配原则来决定调用哪个构造函数。,补充:函数声明和对象定义的判别,Tdate aa;Tdate aa();Tdate aa(1);Tdate aa(z=1);Tdate aa(int z);Tdate aa(

13、int z=1);判别上面的形式是函数声明还是对象定义的关键在于括号内的字符串能否被看作参数列表,如果能则一定是函数声明。,5.6 单参数构造函数(1),单参数构造函数是指只用一个参数即可以调用的构造函数。可以是只定义了一个参数的构造函数,可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值的ctor。当使用带一个参数的ctor构造对象时,有三种等价的写法。最后一种写法只适用于单参数构造函数。,class Rational/有理数类 public:Rational(int numerator=0,int denominator=1);,5.6 单参数构造函数(2),缺省情况下,单参数构造

14、函数可用于类型转换。重载函数的匹配原则:在严格匹配方式,经内部转换后匹配方式都失败后,将寻找通过用户定义的转换后参数匹配的函数并调用。单参数构造函数就是其中一种用户定义的转换。当同时存在多种可能的转换时,重载函数匹配失败,从而导致语法错误。单参数构造函数导致重载函数匹配失败的例子,class Apublic:A(char*);void f(A a);void main()/OK,构造A的一个无/名对象,然后调用f f(“yes?”);,void f(B b)void f(A a)void main()/Error:Ambiguous,二义性/Call f(A a)?or Call f(B a)

15、?f(“yes?”);,class Apublic:A(char*);class Bpublic:B(char*);,5.6 单参数构造函数(3),单参数ctor用于类型转换时极有可能导致意外的错误。演示使用关键字explicit可以抑制上述转换行为。使用explicit关键字后,必须显式的调用构造函数来初始化对象,也就是说不能再使用Tdate date=10;这样的构造对象方式;否则将导致编译错误。,class Apublic:explicit A(char*);void f(A a);void main()A a=“Hi”;/ERROR f(“yes?”);/ERROR,A a=A(“Hi

16、”);/OKf(A(“Yes?”);/OK,教材P325的错误,14.9第一段最后一句:“定义含一个参数的构造函数”应为“定义单参数构造函数”;倒数第二行“只会尝试含一个参数的构造函数”应为“只会尝试单参数构造函数”;void fn(Student否则会发生编译错误。,5.7 成员初始化列表背景,当一个对象是另一个类的成员时:Point3d aPoint;将自动调用成员m_point2d的默认构造函数。,/二维平面的一个点class Point2dpublic:Point2d(int x=0,int y=0)m_x=x;m_y=y;private:int m_x;int m_y;,/三维空间的

17、一个点class Point3dpublic:Point3d()m_z=0;private:Point2d m_point2d;int m_z;,5.7 成员初始化列表背景,Point3d point(1,2,3);需要在Point3d的构造函数中调用Point2d的非默认构造函数,应该怎么做?调用构造函数不可行,因为m_point2d已经构造过了。构造一个无名对象,赋给m_point2d,可行但是效率低。【演示】在Point3d的构造函数中构造了一个无名对象,赋给m_point2d,随后将无名对象析构。,/三维空间的一个点class Point3dpublic:Point3d()m_z=0;

18、Point3d(int x,int y,int z)/错误,构造函数不能显式调用 m_point2d.Point2d(x,y);m_z=z;private:Point2d m_point2d;int m_z;,/正确,但是效率低m_point2d=Point2d(x,y);,5.7 成员初始化列表,应使用成员初始化列表(member initialization list)完成上述工作。Point3d:Point3d(int x,int y,int z):m_point2d(x,y),m_z(z)成员初始化列表必须出现在构造函数参数列表结束(右括号)后和函数体之间,并且必须使用冒号。列表中对数

19、据成员的初始化只能使用括号,不能使用等号。每个数据成员在初始化列表中最多只能出现一次。使用成员初始化列表的方式将只调用一次Point2d的ctor。演示,补充:赋值和初始化,注意区别初始化和赋值:int aInt=10;/初始化int aInt(10);/初始化aInt=8;/赋值Point2d point(1,2);/初始化Point2d point=Point2d(1,2);/初始化point=Point2d(1,2);/赋值一旦进入构造函数的函数体,对象结构已经确立,数据成员就已经存在,在函数体中就只能进行赋值操作。,5.7 成员初始化列表使用,下面的四种情况只能使用成员初始化列表需要调

20、用成员对象的带参构造函数时。需要调用基类的带参构造函数(后续章节讲解)。需要初始化常量数据成员。需要初始化引用数据成员。推荐使用成员初始化列表来初始化类中的每个成员,即使不是上述四种情况之一。Point2d:Point2d(int x=0,int y=0):m_x(x),m_y(y),5.7 成员初始化列表常数据成员,const修饰的数据成员不能在类体中显式初始化。/类体中以下代码是错误的const float fMaxOverdraft=100.0;只能在构造函数的初始化列表中初始化。const修饰的类数据成员可以通过常量、变量、函数返回值初始化。const修饰的数据成员一但初始化就不能再修

21、改,无论是在类体中还是类体外。const修饰的数据成员在同一个类的不同的对象中可以有不同的值。,/Credit.hclass CreditCard/信用卡public:CreditCard(float e=100.0);private:/最大透支额度 const float fMaxOverdraft;,/Credit.cpp#include CreditCard:CreditCard(float a):fMaxOverdraft(a),#include CreditCard c1;CreditCard c2(1000.0);,5.7 成员初始化列表初始化顺序,初始化的顺序并不是按照成员名在初

22、始化列表中的顺序初始化;而是按照成员在类中声明的顺序初始化。初始化列表中的数据成员总是在ctor函数体中的成员赋值前被初始化。,Point3d:Point3d(int x,int y,int z):m_z(z),m_point2d(x,y)初始化顺序为m_point2d,m_z,Point3d:Point3d(int x,int y,int z):m_z(z)m_point2d=Point2d(x,y);初始化顺序为m_point2d,m_z再对m_point2d赋值。,5.7 成员初始化列表难于发现的错误,教材P287 ch12_15.cpp的例子。因为num先于age初始化,所以num的值

23、不正确。当需要使用一个成员去初始化另一个成员时,应该将这样的代码放在ctor的函数体中。查看修改后代码,class Apublic:A(int j):age(j)num=age+1;cout age:age endl;cout num:num endl;protected:int num;int age;void main()A sa(15);,class Apublic:A(int j):age(j),num(age+1)cout age:age endl;cout num:num endl;protected:int num;int age;void main()A sa(15);,5.8

24、 拷贝构造函数背景,对象作为函数参数传递时,因为参数传递的传值语义,形参实际上是实参对象的一个拷贝,换句话说形参对象是以实参对象为原本构造出来的一个新的对象。有时候也需要用一个已有的对象去构造一个新的对象。支持Undo的类对象:在用户对对象执行修改之前,首先备份一个拷贝;当用户需要撤销对象的修改时可以使用备份对象恢复。,5.8 拷贝构造函数语法,拷贝构造函数的声明语法:classname(classname从函数中返回对象时。向函数传递对象时。,5.8 拷贝构造函数默认行为,如果没有显式定义拷贝构造函数,C+将为我们提供一个默认拷贝构造函数从而支持对象以拷贝方式初始化,对象的拷贝方式和结构变量

25、一样,都是按成员初始化(Memberwise Copy)。如果一个类的数据成员包括其他类对象,则首先对每个成员对象调用拷贝构造函数或者默认拷贝构造函数,然后再拷贝类中的非对象成员。【演示】P318页的例子:理解调用时机实际上默认拷贝构造函数的初始化方式可以理解为将源对象占用内存空间完整的拷贝到目的对象。,Point2d p1(1,2);Point2d p2=p1;/或Point2d p2(p1);则p2.m_x为1,p2.m_y为2,Point3d p1(1,2,3);Point3d p2=p1;/或Point3d p2(p1);则p2.m_x为1,p2.m_y为2,p2.m_z为3,5.8

26、拷贝构造函数默认行为,C+所提供的默认拷贝行为在某些情况下是不合适的,甚至是错误的。【演示】默认拷贝构造函数带来的问题默认的拷贝行为只是拷贝每个成员,包括指向堆内存的指针,从而使得两个对象指向了同一块内存;其中一个对象在析构时释放了该内存,导致了另一个对象的成员指针指向了无效内存,成为野指针。,5.8 拷贝构造函数自定义拷贝,为解决按成员初始化(浅拷贝)存在的问题,可以自定义拷贝构造函数。【演示】使用深拷贝解决上一个演示的问题当我们需要的拷贝行为和默认行为不一样时就需要定义拷贝构造函数。默认的拷贝行为导致两个对象拥有对同一个资源的所有权时,需要拷贝构造函数。如某个类有一个成员,该成员应该对每个

27、对象都唯一。此时需要定义拷贝构造函数通常如果类需要一个析构函数,则它也需要一个拷贝构造函数,因为一个自定义的析构函数意味着额外的资源需要在析构之前被释放。类Point3d是否需要我们编写自定义拷贝构造函数?,Point3d类定义,/二维平面的一个点class Point2dpublic:Point2d(int x=0,int y=0):m_x(x),m_y(y)private:int m_x;int m_y;,/三维空间的一个点class Point3dpublic:Point3d(int x=0,int y=0,int z=0):m_point2d(x,y),m_z(z)private:Po

28、int2d m_point2d;int m_z;,5.8 拷贝构造函数调用,一但自定义了拷贝构造函数,则类及其成员对象的拷贝构造工作全部由自定义的拷贝ctor负责。示例当需要调用某个类的拷贝构造函数时(用已有对象初始化一个新对象时,从函数中返回对象时,以传值方式向函数传递对象时):如果对象所属类显式没有定义拷贝构造函数,则执行默认拷贝构造函数(按成员初始化)。如果对象所属类显式定义了拷贝构造函数:如果拷贝构造函数是可访问的,就去调用它如果拷贝构造函数是不可访问的,就产生编译错误。可利用这一点使得类不允许拷贝构造。示例,class Apublic:A()A(const A,/将不会调用A:A(c

29、onst A&),而只是调用B:B(const B&),B(const B&b):m_a(b.m_a),/将首先调用A:A(const A&),然后调用B:B(const B&),class Apublic:A()private:/只是声明此函数,并不定义此函数。/使用了C+中的延迟错误检查特性,即只有当一个函数/在程序中被调用了,编译器才会去检查函数的定义是否/存在。A(const A,5.9 示例 P326习题14.2,3个印刷错误for(int i=0;i size;i+)cout bufferj endl;bufferj=j+1;拷贝构造的实现为:,Vector:Vector(cons

30、t Vector,Vector:Vector(const Vector,5.9 示例,使用构造函数和析构函数来完成第四章中类LinkList的Initialize函数和Destroy函数所完成的功能,并编写拷贝构造函数实现链表类的深拷贝。演示使用构造函数和析构函数重写第四章中的FileWrapper类。为了防止FileWrapper的两个对象引用到同一份打开的文件上,将拷贝构造函数声明为私有。演示,5.10 再看new和delete,malloc和free无能为力的地方:void*malloc(size_t size);void free(void*memblock);上述两个函数原型没有包含

31、类型信息,因此在为对象分配内存时无法自动调用构造函数,在释放对象占用内存时无法自动调用析构函数。new和delete用于解决上述问题。在使用new分配内存时提供了类型信息,实现计算类型的大小,分配相应数量的堆内存,然后根据类名自动调用构造函数。在使用delete释放内存时,首先根据指针的类型信息自动调用析构函数,然后释放内存。,5.10 再看new和delete,使用new为创建一个对象时,可以在类名后跟参数,new根据参数匹配的原则调用构造函数;如果没有跟参数,则调用默认构造函数。Point2d*p=new Point2d;delete p;Point2d*p=new Point2d();d

32、elete p;Point2d*p=new Point2d(1,2);delete p;Point2d*p2=new Point2d(*p);delete p2;在堆上分配对象数组时,不能提供参数,所以只能调用类的默认ctor。如果该类没有定义默认构造函数,则不能分配此类的对象数组。在堆上分配的对象数组必须用delete释放。演示Point2d*p=new Point2d10;delete p;,5.10 示例 P326习题14.1,#include class Samppublic:void Setij(int a,int b)i=a,j=b;Samp()cout Destroying.i

33、endl;int GetMulti()return i*j;protected:int i;int j;,void main()Samp*p=new Samp10;if(!p)cout Allocation errorn;return;for(int j=0;j 10;j+)pj.Setij(j,j);for(int k=0;k10;k+)cout Multi k is:pk.GetMulti()endl;delete p;,5.11 对象的构造顺序,局部自动对象在块(局部)作用域内按照定义顺序构造并逆序析构。在局部作用域中最早构造的对象最迟析构。局部静态对象在第一次被使用时构造,并且在程序退

34、出时析构。所有全局对象在主函数main之前都将构造完毕。如果构造全局对象时发生了错误或者死循环,main函数将得不到控制权。全局(静态或者非静态)对象在文件作用域中按照定义顺序构造。不同文件作用域中的全局对象的构造顺序不定。不应该在一个全局对象中使用另一个全局对象来初始化对象成员按其在所属类中的声明顺序构造并逆序析构。【演示】不同作用域对象的构造顺序,5.12 临时对象,当函数返回一个对象时,要创建一个临时变量以存放返回的值。是否使用临时对象是由编译器来决定的,C+标准中并没有明确规定什么时候一定要使用临时变量。教材P323的例子将这个例子main函数中的代码修改为Student s=fn()

35、;/初始化,不是赋值编译器执行优化后将不会产生临时对象,直接使用fn函数中的局部对象初始化s,应理解为拷贝构造函数。s=fn();/赋值赋值之前对象s已经构造完毕,不能再调用构造函数。赋值之后,临时对象被析构。演示中的Trace类使用了“对象成员按其在所属类中的声明顺序构造并逆序析构。”这一规则。,推敲一下,演示中Trace类的设计实际上存在问题。因为Trace类中的m_name可能指向无效的内存空间。,class Tracepublic:Trace(char*name):m_name(name)cout“创建 func endl;Trace()cout“销毁”m_name endl;char

36、*GetName()return m_name;private:char*m_name;,#include Trace*CreateTrace()char name=“aaa”;/返回的对象中包含指向/无效内存的指针。return new Trace(name);void main(void)Trace*p=CreateTrace();cout GetName();delete p;,5.12 示例 P326习题14.3,#include class Xpublic:X(int s)cout ctor with s endl;X(X,void main()X a(1);X b=f(X(2);a

37、=f(a);,演示,Tdate类定义,/tdate.hclass Tdatepublic:Tdate();Tdate(int d);Tdate(int m,int d);Tdate(int m,int d,int y);int GetYear()const return year;private:int month;int day;int year;,/Tdate.cpp#include“tdate.h”Tdate:Tdate()month=4;day=15;year=1995;Tdate:Tdate(int d)month=4;day=d;year=1996;Tdate:Tdate(int m,int d)month=m;day=d;year=1997;Tdate:Tdate(int m,int d,int y)month=m;day=d;year=y;,

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 生活休闲 > 在线阅读


备案号:宁ICP备20000045号-2

经营许可证:宁B2-20210002

宁公网安备 64010402000987号