第5章 构造函数和析构函数_第1页
第5章 构造函数和析构函数_第2页
第5章 构造函数和析构函数_第3页
第5章 构造函数和析构函数_第4页
第5章 构造函数和析构函数_第5页
已阅读5页,还剩35页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

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

int

GetAge()const;private:charm_strName[100];int

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

此句背后的动作是首先为对象s分配内存,然后调用ctor初始化对象。classStudent{public:Student();

int

GetAge()const;private:charm_strName[100];int

m_nAge;doublem_fScore;}; Student::Student(){

m_nAge=10;

m_fScore=100;}5.1构造函数-要点构造函数不能定义为const的,但是可以用来构造常量对象,这是因为只有当构造函数执行完毕后,对象的常量性才能够建立起来。Rectangle::Rectangle()const{}//错误的ctor定义对于常量对象只能调用常成员函数的规则,构造函数是个例外。如果一个类对象item是另一个类Container的数据成员,则创建Container的对象时,会首先自动为item调用构造函数。换句话说,编译器会自动在Container的构造函数的函数体前插入对item构造函数的调用。首先调用对象成员的ctor,其次才是自身的ctor。[演示]理解构造函数的调用时机5.2析构函数-背景一个对象的生存期结束(销毁)时可能需要做些清理工作。打开的文件需要关闭分配的堆内存需要释放如果清理工作对应的函数能够被自动调用,就会减少程序员的工作量,甚至出错的可能。可以使用析构函数(Destructor,dtor)自动完成清理工作。5.2析构函数-要点析构函数名必须为~加上类名。示例dtor是一类特殊的成员函数,它没有参数,不能重载,不能为其指定返回值。类中至多只有一个dtor。dtor在对象的生存期即将结束时由系统自动调用。析构函数返回后,对象结束其生存期。如果没有清理工作要做,则可以不在类中定义析构函数。【示例】析构函数classStudent{public:Student();//ctor

~Student();//dtor

int

GetAge()const;private:charm_strName[100];

int

m_nAge;doublem_fScore;}; 5.2析构函数-要点dtor不能定义为常成员函数,但是常量对象在析构的时候同样会调用dtor。Rectangle::~Rectangle()const{}//错误的dtor定义对于常量对象只能调用常成员函数的规则,dtor是个例外。如果类中包含成员对象,在执行析构函数时将首先执行dtor的函数体,然后为类中的每个对象成员调用析构函数。换句话说,编译器会在类的dtor函数体之后插入对类中每个成员对象的dtor的调用。对dtor的调用顺序正好和对ctor的调用顺序相反。最先构造的对象最后被析构。[演示]理解析构函数的调用时机classPerson{public:

Person(){m_strName=newchar[20];}//分配堆空间

~Person(){delete[]m_strName;}//释放堆空间

voidSetName(constchar*name){

strcpy(m_strName,name);}private:char*m_strName;}5.3构造函数重载与dtor不同,ctor允许带参而且可以带不同的参数,ctor允许重载。教材P273页的例子MFC中的CString的类定义在一个构造函数中调用重载的另一个构造函数以简化编程的方法是错误的。因为构造函数只用于创建对象。这一点和普通重载函数不同。Tdate::Tdate(intm,intd,inty){….}Tdate::Tdate(intd){//下面的语句只是创建

//了另一个对象,并且

//很快结束生存期。

Tdate(4,d,1995);}专门定义一个成员函数,然后所有的构造函数调用此成员函数即可。解决办法通过给参数设置默认值,可以将重载的构造函数合并为一个。示例classTdate{public:

Tdate(intm=4,d=22,y=2005);private:

intmonth;

intday;

intyear;};Tdate::Tdate(intm/*=4*/,intd/*=22*/,inty/*=2005*/){ month=m;day=d;year=y;

cout<<month<<"/"<<day<<"/"<<year<<endl;}#include<iostream.h>classTdate{public:

Tdate() {Init(4,15,1995);}

Tdate(intd) {Init(4,d,1995);}

Tdate(int

m,intd) {Init(m,d,1995);}

Tdate(int

m,int

d,inty) {Init(m,d,y);}

protected:

intmonth;

intday;

intyear;

voidInit(int

m,int

d,inty){month=m;day=d;year=y;

cout<<month<<"/"<<day<<"/"<<year<<endl;}};5.4默认(缺省)构造函数默认构造函数(dctor):不需要用户指定实参就能被调用的构造函数。(教材上缺明确定义)Tdate::Tdate(){…}Rectangle::Rectangle(floatw=0.0,floath=0.0);如果没有在类中定义任何ctor,则C++提供一个dctor,该dctor为无参构造函数,不作任何工作。类的数据成员如果为简单类型则不会被初始化,如果为其它类的对象则自动为其调用构造函数。classStudent{private:charname[20];};等价于classStudent{public:Student(){}private:charname[20];};5.4默认构造函数-示例classStudent{public:

Student(char*pName){strcpy(name,pName);}

Student(){name[0]=‘\0’;}private:charname[20];};voidmain(void){

//错误:没有匹配的ctorStudents;}只要为类定义了一个带参的构造函数,C++就不再提供默认构造函数,如果还需要无参的构造函数,则必须显式定义。5.5使用参数初始化对象Tdate

adate;//调用默认构造函数(dctor)Tdatebdate(10);//OK,Tdate::Tdate(intd)等价于Tdate

bdate=Tdate(10);Tdatecdate(3,12);//OK,Tdate::Tdate(intm,intd)等价于Tdate

cdate=Tdate(3,12);Tdate

ddate();//此句没有语法错误

//Error:leftof'.GetYear'musthaveclass/struct/uniontype

int

iy=ddate.GetYear();编译器认为ddate是个函数声明。所以使用dctor构造对象时,对象名后面不能跟()。为什么Tdatebdate(10);不看作是函数声明?根据重载函数的匹配原则来决定调用哪个构造函数。补充:函数声明和对象定义的判别Tdate

aa;Tdate

aa();Tdateaa(1);Tdate

aa(z=1);Tdate

aa(intz);Tdate

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

Rational(intnumerator=0,

intdenominator=1);…};Tdatedate(10);Tdatedate=Tdate(10);Tdatedate=10;Rationalr(2);Rationalr=Rational(2);Rationalr=2;等价等价5.6单参数构造函数(2)缺省情况下,单参数构造函数可用于类型转换。重载函数的匹配原则:在严格匹配方式,经内部转换后匹配方式都失败后,将寻找通过用户定义的转换后参数匹配的函数并调用。单参数构造函数就是其中一种用户定义的转换。当同时存在多种可能的转换时,重载函数匹配失败,从而导致语法错误。单参数构造函数导致重载函数匹配失败的例子classA{public:

A(char*);…};voidf(Aa);voidmain(){//OK,构造A的一个无//名对象,然后调用f

f(“yes?”);}voidf(Bb){…}voidf(Aa){…}voidmain(){

//Error:Ambiguous,二义性

//Callf(Aa)?orCallf(Ba)?

f(“yes?”);}classA{public:

A(char*);…};classB{public:

B(char*);…};5.6单参数构造函数(3)单参数ctor用于类型转换时极有可能导致意外的错误。[演示]使用关键字explicit可以抑制上述转换行为。使用explicit关键字后,必须显式的调用构造函数来初始化对象,也就是说不能再使用Tdatedate=10;

这样的构造对象方式;否则将导致编译错误。classA{public:

explicitA(char*);…};voidf(Aa);voidmain(){Aa=“Hi”;//ERROR

f(“yes?”);//ERROR}Aa=A(“Hi”);//OKf(A(“Yes?”));//OK教材P325的错误14.9第一段最后一句:“定义含一个参数的构造函数”应为“定义单参数构造函数”;倒数第二行“只会尝试含一个参数的构造函数”应为“只会尝试单参数构造函数”;voidfn(Student&s);此代码不能通过编译,应为voidfn(constStudent&s);否则会发生编译错误。5.7成员初始化列表-背景当一个对象是另一个类的成员时:Point3daPoint;将自动调用成员m_point2d的默认构造函数。//二维平面的一个点classPoint2d{public: Point2d(intx=0,inty=0){m_x=x;m_y=y;}private:

int

m_x;

int

m_y;};//三维空间的一个点classPoint3d{public:Point3d(){m_z=0;}

private:Point2dm_point2d;

int

m_z;};5.7成员初始化列表-背景Point3dpoint(1,2,3);需要在Point3d的构造函数中调用Point2d的非默认构造函数,应该怎么做?调用构造函数不可行,因为m_point2d已经构造过了。构造一个无名对象,赋给m_point2d,可行但是效率低。【演示】在Point3d的构造函数中构造了一个无名对象,赋给m_point2d,随后将无名对象析构。//三维空间的一个点classPoint3d{public:Point3d(){m_z=0;} Point3d(intx,inty,intz){//错误,构造函数不能显式调用

m_point2d.Point2d(x,y);

m_z=z;}private:Point2dm_point2d;

int

m_z;};//正确,但是效率低m_point2d=Point2d(x,y);5.7成员初始化列表应使用成员初始化列表(memberinitializationlist)完成上述工作。Point3d::Point3d(intx,inty,intz):m_point2d(x,y),m_z(z){}成员初始化列表必须出现在构造函数参数列表结束(右括号)后和函数体之间,并且必须使用冒号。列表中对数据成员的初始化只能使用括号,不能使用等号。每个数据成员在初始化列表中最多只能出现一次。使用成员初始化列表的方式将只调用一次Point2d的ctor。演示补充:赋值和初始化注意区别初始化和赋值:int

aInt=10;//初始化intaInt(10);//初始化aInt=8;//赋值Point2dpoint(1,2);//初始化Point2dpoint=Point2d(1,2);//初始化point=Point2d(1,2);//赋值一旦进入构造函数的函数体,对象结构已经确立,数据成员就已经存在,在函数体中就只能进行赋值操作。5.7成员初始化列表-使用下面的四种情况只能使用成员初始化列表需要调用成员对象的带参构造函数时。需要调用基类的带参构造函数(后续章节讲解)。需要初始化常量数据成员。需要初始化引用数据成员。推荐使用成员初始化列表来初始化类中的每个成员,即使不是上述四种情况之一。Point2d::Point2d(intx=0,inty=0):m_x(x),m_y(y){}5.7成员初始化列表-常数据成员const修饰的数据成员不能在类体中显式初始化。//类体中以下代码是错误的constfloatfMaxOverdraft=100.0;只能在构造函数的初始化列表中初始化。const修饰的类数据成员可以通过常量、变量、函数返回值初始化。const修饰的数据成员一但初始化就不能再修改,无论是在类体中还是类体外。const修饰的数据成员在同一个类的不同的对象中可以有不同的值。//Credit.hclassCreditCard//信用卡{public:

CreditCard(floate=100.0);…private://最大透支额度

constfloatfMaxOverdraft;}//Credit.cpp#include<Credit.h>CreditCard::CreditCard(floata):fMaxOverdraft(a){}#include<Credit.h>CreditCardc1;CreditCardc2(1000.0);5.7成员初始化列表-初始化顺序初始化的顺序并不是按照成员名在初始化列表中的顺序初始化;而是按照成员在类中声明的顺序初始化。初始化列表中的数据成员总是在ctor函数体中的成员赋值前被初始化。Point3d::Point3d(intx,inty,intz):m_z(z),m_point2d(x,y){}初始化顺序为m_point2d,m_zPoint3d::Point3d(intx,inty,intz):m_z(z){m_point2d=Point2d(x,y);}初始化顺序为m_point2d,m_z再对m_point2d赋值。5.7成员初始化列表-难于发现的错误教材P287ch12_15.cpp的例子。因为num先于age初始化,所以num的值不正确。当需要使用一个成员去初始化另一个成员时,应该将这样的代码放在ctor的函数体中。查看修改后代码classA{public:

A(intj):age(j){

num=age+1;

cout<<"age:"<<age<<endl;

cout<<"num:"<<num<<endl;}protected:

intnum;

intage;};voidmain(){Asa(15);}classA{public:

A(intj):age(j),num(age+1){

cout<<"age:"<<age<<endl;

cout<<"num:"<<num<<endl;}protected:

intnum;

intage;};voidmain(){Asa(15);}5.8拷贝构造函数-背景对象作为函数参数传递时,因为参数传递的传值语义,形参实际上是实参对象的一个拷贝,换句话说形参对象是以实参对象为原本构造出来的一个新的对象。有时候也需要用一个已有的对象去构造一个新的对象。支持Undo的类对象:在用户对对象执行修改之前,首先备份一个拷贝;当用户需要撤销对象的修改时可以使用备份对象恢复。5.8拷贝构造函数-语法拷贝构造函数的声明语法:classname(classname&src);或者classname(const

classname&src);参数必须是引用,为什么?拷贝构造函数的调用时机:使用已有的对象去初始化一个新的对象时。Point2dp1(1,2);Point2dp2(p1);或者Point2dp2=p1;从函数中返回对象时。向函数传递对象时。5.8拷贝构造函数-默认行为如果没有显式定义拷贝构造函数,C++将为我们提供一个默认拷贝构造函数从而支持对象以拷贝方式初始化,对象的拷贝方式和结构变量一样,都是按成员初始化(MemberwiseCopy)。如果一个类的数据成员包括其他类对象,则首先对每个成员对象调用拷贝构造函数或者默认拷贝构造函数,然后再拷贝类中的非对象成员。【演示】P318页的例子:理解调用时机实际上默认拷贝构造函数的初始化方式可以理解为将源对象占用内存空间完整的拷贝到目的对象。Point2dp1(1,2);Point2dp2=p1;//或Point2dp2(p1);则p2.m_x为1,p2.m_y为2Point3dp1(1,2,3);Point3dp2=p1;//或Point3dp2(p1);则p2.m_x为1,p2.m_y为2,p2.m_z为35.8拷贝构造函数-默认行为C++所提供的默认拷贝行为在某些情况下是不合适的,甚至是错误的。【演示】默认拷贝构造函数带来的问题默认的拷贝行为只是拷贝每个成员,包括指向堆内存的指针,从而使得两个对象指向了同一块内存;其中一个对象在析构时释放了该内存,导致了另一个对象的成员指针指向了无效内存,成为野指针。5.8拷贝构造函数-自定义拷贝为解决按成员初始化(浅拷贝)存在的问题,可以自定义拷贝构造函数。【演示】使用深拷贝解决上一个演示的问题当我们需要的拷贝行为和默认行为不一样时就需要定义拷贝构造函数。默认的拷贝行为导致两个对象拥有对同一个资源的所有权时,需要拷贝构造函数。如某个类有一个成员,该成员应该对每个对象都唯一。此时需要定义拷贝构造函数通常如果类需要一个析构函数,则它也需要一个拷贝构造函数,因为一个自定义的析构函数意味着额外的资源需要在析构之前被释放。类Point3d是否需要我们编写自定义拷贝构造函数?Point3d类定义//二维平面的一个点classPoint2d{public: Point2d(intx=0,inty=0):m_x(x),m_y(y){}private:

int

m_x;

int

m_y;};//三维空间的一个点classPoint3d{public:Point3d(intx=0,inty=0,intz=0):m_point2d(x,y),m_z(z){}private:Point2dm_point2d;

int

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

A(constA&){…}…};classB{public:B(){…}

B(constB&b){…}private:Am_a;};voidmain(){Bb1;//为B生成默认的拷贝构造函数,将调用A::A(constA&)Bb2=b1;}//将不会调用A::A(constA&),而只是调用B::B(constB&)B(constB&b):m_a(b.m_a){…}//将首先调用A::A(constA&),然后调用B::B(constB&)classA{public:A(){…}private:

//只是声明此函数,并不定义此函数。

//使用了C++中的延迟错误检查特性,即只有当一个函数

//在程序中被调用了,编译器才会去检查函数的定义是否

//存在。

A(constA&);…};…Aa;//errorC2248:'A::A':cannotaccessprivatememberdeclaredinclass'A'Ab(a);5.9示例P326习题14.23个印刷错误for(inti=0;i<size;i++)cout<<buffer[j]<<endl;buffer[j]=j+1;拷贝构造的实现为:Vector::Vector(constVector&v) :size(v.size){buffer=newint[size];

for(inti=0;i<size;i++)

buffer[i]=v.buffer[i];}Vector::Vector(constVector&v) :size(v.size){buffer=newint[size];

memcpy(buffer,v.buffer, size*sizeof(int));}5.9示例使用构造函数和析构函数来完成第四章中类LinkList的Initialize函数和Destroy函数所完成的功能,并编写拷贝构造函数实现链表类的深拷贝。演示使用构造函数和析构函数重写第四章中的FileWrapper类。为了防止FileWrapper的两个对象引用到同一份打开的文件上,将拷贝构造函数声明为私有。演示5.10再看new和deletemalloc和free无能为力的地方:void*malloc(size_tsize);voidfree(void*memblock);上述两个函数原型没有包含类型信息,因此在为对象分配内存时无法自动调用构造函数,在释放对象占用内存时无法自动调用析构函数。new和delete用于解决上述问题。在使用new分配内存时提供了类型信息,实现计算类型的大小,分配相应数量的堆内存,然后根据类名自动调用构造函数。在使用delete释放内存时,首先根据指针的类型信息自动调用析构函数,然后释放内存。5.10再看new和delete使用new为创建一个对象时,可以在类名后跟参数,new根据参数匹配的原则调用构造函数;如果没有跟参数,则调用默认构造函数。Point2d*p=newPoint2d; deletep;Point2d*p=newPoint2d(); deletep;Point2d*p=newPoint2d(1,2); deletep;Point2d*p2=newPoint2d(*p); deletep2;在堆上分配对象数组时,不能提供参数,所以只能调用类的默认ctor。如果该类没有定义默认构造函数,则不能分配此类的对象数组。在堆上分配的对象数组必须用delete[]释放。演示Point2d*p=newPoint2d[10];delete[]p;5.10示例P326习题14.1#include<iostream.h>classSamp{public:voidSetij(inta,intb){i=a,j=b;}

~Samp(){cout<<"Destroying.."<<i<<endl;}

int

GetMulti(){returni*j;}protected:

inti;

intj;};voidmain(){

Samp*p=newSamp[10];

if(!p){

cout<<"Allocationerror\n";return;}

for(intj=0;j<10;j++)p[j].Setij(j,j);

for(intk=0;k<10;k++){

cout<<"Multi["<<k<<"]is:"<<p[k].GetMulti()<<endl;delete[]p;}5.11对象的构造顺序局部自动对象在块(局部)作用域内按照定义顺序构造并逆序析构。在局部作用域中最早构造的对象最迟析构。局部静态对象在第一次被使用时构造,并且在程序退出时析构。所有全局对象在主函数main之前都将构造完毕。如果构造全局对象时发生了错误或者死循环,main函数将得不到控制权。全局(静态或者非静态)对象在文件作用域中按照定义顺序构造。不同文件作用域中的全局对象的构造顺序不定。不应该在一个全局对象中使用另一个全局对象来初始化对象成员按其在所属类中的声明顺序构造并逆序析构。【演示】不同作用域对象的构造顺序5.12临时对象当函数返回一个对象时,要创建一个临时变量以存放返回的值。是否使用临时对象是由编译器来决定的,C++标准中并没有明确规定什么时候一定要使用临时变量。教材P323的例子将这个例子main函数中的代码修改为

Students=fn();//初始化,不是赋值编译器执行优化后将不会产生临时对象,直接使用fn

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论