CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

高效学C++|组合类的构造函数-永久免费的源码丞旭猿

设计好MyString类后,就可以像使用普通类型一样使用它了。例如,类的对象可以像普通的变量一样作为另一个类的数据成员。比如,MyString类的对象作为CStudent类的数据成员如例3.5所示。

【例3.5】MyString类的对象作为CStudent类的数据成员。

1.//MyString类的定义省略2.//注意:保留其构造函数、析构函数、复制构造函数和赋值运算符中的输出内容3.//file: student.h4.pragma once5.include"MyString.h"6.classCStudent7.{8.public:9.CStudent() { cout <<"CStudent的默认构造函数被调用"<< endl; }10.CStudent(intnum,constMyString & name,constMyString & major,doublescore);11.~CStudent() { cout <<"CStudent的析构函数被调用"<< endl; }12.voidset_number(intnum) { number = num; }13.intget_number(){returnnumber; }14.MyString & set_name(constMyString & name);15.MyString & get_name() {returnname; }16.MyString & set_major(constMyString & major);17.MyString & get_major() {returnmajor; }18.voidset_score(doublescore) {this->score = score; }19.doubleget_score() {returnscore; }20.private:21.intnumber;22.MyString name;23.MyString major;24.doublescore;25.};26.//file: student.cpp27.include"student.h"28.CStudent::CStudent(intnum,constMyString &name,constMyString &major,doublescore)29.{30.number = num;31.this->name = name;32.this->major = major;33.this->score = score;34.cout <<"CStudent的有参构造函数被调用"<< endl;35.}36.MyString & CStudent::set_name(constMyString & name)37.{38.this->name = name;39.returnthis->name;40.}41.//file: main.cpp42.include"student.h"43.include44.using namespace std;45.intmain()46.{47.MyString name("zhangsan"), major("computer");48.CStudent stu(1, name, major,100), stu2;49.CStudent stu3(stu);50.stu2 = stu3;51.cout << stu.get_name().get_string() << endl;52.cout << stu2.get_name().get_string() << endl;53.return0;54.}

其输出如下:

1.MyString的有参构造函数被调用2.MyString的有参构造函数被调用3.MyString的默认构造函数被调用4.MyString的默认构造函数被调用5.MyString的赋值运算符函数被调用6.MyString的赋值运算符函数被调用7.CStudent的有参构造函数被调用8.MyString的默认构造函数被调用9.MyString的默认构造函数被调用10.CStudent的默认构造函数被调用11.MyString的复制构造函数被调用12.MyString的复制构造函数被调用13.MyString的赋值运算符函数被调用14.MyString的赋值运算符函数被调用15.zhangsan16.zhangsan17.CStudent的析构函数被调用18.MyString的析构函数被调用19.MyString的析构函数被调用20.CStudent的析构函数被调用21.MyString的析构函数被调用22.MyString的析构函数被调用23.CStudent的析构函数被调用24.MyString的析构函数被调用25.MyString的析构函数被调用26.MyString的析构函数被调用27.MyString的析构函数被调用

对于例3.5,在定义CStudent类时使用了MyString类,比如其数据成员name是MyString类型的,也就是说MyString类的对象name作为CStudent的数据成员。这样,对于编写CStudent类的程序员来说,只需要知道MyString类的用法就行了,而不需要再去考虑如动态内存分配等细节,因而大大减轻了程序员的工作量。不过,类毕竟与普通的数据类型不同,因而就带来了一些问题。下面结合程序的输出,分析程序的运行过程如下:

(1)输出的第1、2行是程序第47行中构造name和major时产生的。

(2)程序第48行会调用CStudent类的有参构造函数构造对象stu、调用CStudent的默认构造函数构造stu2,而输出中的第7行才是CStudent的有参构造函数中输出的信息、第10行才是CStudent的默认构造函数输出的信息,因此输出的第3行至第10行都是因程序第48行产生的输出。这些输出表明,在CStudent的有参构造函数执行之前,先调用了两次MyString类的默认构造函数,然后调用了两次MyString类的赋值运算符函数;在执行CStudent的默认构造函数之前,先调用了两次MyString类的默认构造函数。然而,在CStudent类的有参构造函数中没有看到调用MyString类的默认构造函数初始化内嵌对象name和major的地方,那么两次调用MyString类的默认构造函数是怎么发生的?同理,在CStudent类的默认构造函数的实现中也没有显式调用MyString类的默认构造函数初始化内嵌对象name和major的地方,那么两次调用MyString类的默认构造函数是怎么发生的?这就需要介绍构造函数的初始化列表了。另外,在CStudent类的有参构造函数中的语句this->name = name; this->major = major;中直接使用了类的内嵌对象name和major(由此两次调用MyString类的赋值运算符函数,产生输出的第5和第6行),这说明这两个对象在进入该构造函数之前就已经构造完毕。既然在进入CStudent类的构造函数之前就能调用MyString类的构造函数初始化name和major,那么能不能通过传递CStudent类的有参构造函数中的参数name和major来调用MyString类的复制构造函数初始化CStudent类的成员对象name和major呢?这样做还可以省去在CStudent类的有参构造函数中对它们的赋值,即省去两次调用MyString类的赋值运算符函数的过程。

(3)程序第49行是调用CStudent类的复制构造函数,但例3.5中没有设计该函数,因此执行的是编译器自动提供的复制构造函数;程序第50行是一个赋值运算,由于例3.5中也没有为CStudent设计赋值运算符函数,因此编译器自动提供了默认的赋值运算符函数。显然程序第51行和第52行产生的输出为输出中的第15行和第16行,因此程序第49行和第50行产生的输出为输出中的第11行至第14行:显示调用了两次MyString类的复制构造函数和两次赋值运算符函数。根据输出的第15行和第16行的内容相同,且程序运行正常,可以判断编译器自动提供的复制构造函数和赋值运算符是正确的。那么,编译器自动提供的复制构造函数和赋值运算符函数是什么样的?

(4)例3.5中设计了CStudent类的析构函数,但在其函数体中只有一条输出语句。这是因为CStudent类中没有涉及动态内存分配,因此不涉及回收堆内存的问题。注意,MyString类型的对象name和major涉及了堆内存,不过回收其堆内存的工作由MyString类的析构函数完成。从程序中可以看出,对象的析构顺序为:依次析构对象stu3、stu2和stu,然后析构对象major和name。整个析构过程产生了输出中的第17行至第27行,其中析构stu3产生了输出中的第17行至第19行。那么,析构组合类对象stu3为什么是这样的一个过程?

下面会解释上面提出的问题。不过,在此之前,先介绍一下类的前向引用声明问题,因为这个问题在定义组合类时经常会用到。

在C++语言中,使用基本数据类型的变量时需要遵循先声明后引用的规则。与此类似,在定义新的类型时也要遵循这一规则。例如在例3.5中,在定义CStudent类之前,先通过预编译指令引入了MyString类的定义(例3.5的第5行)。在声明一个类之前就试图使用这个类则会出现编译错误,如例3.6所示。

【例3.6】在声明一个类之前就试图使用这个类则会出现编译错误。

1.classA2. {3.public:4.voidA_fun(B b);//因之前没有声明类型B,故这里试图引用B会造成编译错误5.inti;6.};7.classB8. {9.public:10.voidB_fun(A a);11.intj;12.};

在例3.6中,在类A的定义中引用了类B。然而,B类还没有被声明,所以会造成编译错误。解决办法是进行前向类型声明,比如在声明A之前加入声明语句class B;。

进行了类的前向声明之后,仅能保证声明的符号可见,但在给出类的具体定义之前,并不能涉及类的具体内容,如下面的程序。

classB;classA{public:intA_fun(B b){returnb.j; }//在给出B的具体定义之前涉及了其//具体内容,所以会出现编译错误inti;};classB{public:intB_fun(A a);intj;};

在上面的程序中,类A的函数A_fun()试图访问对象b的数据成员j,即试图引用B类的具体内容。然而,在此之前,类B的具体定义尚未给出,所以会出现编译错误。解决办法是将该函数的实现写在类外并且在类B的完整定义之后。

类似地,在给出类的完整定义之前,不能定义类的对象,因为定义类的对象就会涉及对象的构造,从而会涉及类的具体内容,如下面的程序。

classB;classA{public:intA_fun(B b);B m_b;//在给出类B的完整定义之前定义B的对象会造成编译错误A m_a;//在类A的定义内部定义A的对象会造成编译错误};classB{public:intB_fun(A a);intj;};

在上面的程序中,类A试图定义B的对象m_b和A的对象m_a,然而此时类B和类A的定义都不完整,因而会造成编译错误。解决办法是:首先把类B的完整定义放到类A的定义之前;其次,在类A中不能定义类A的对象,只能定义类A的指针,如下面的程序。

classA;//因为定义类B时引用了类A,所以需要做前向声明classB{public:intB_fun(A a);intj;};classA{public:intA_fun(B b){returnb.j; }//前面已有类B的完整定义,故该语句正确B m_b;//前面已有类B的完整声明,故此处能够定义类B的对象A* m_pa;//永远不能在类定义中定义自身的对象,可以定义自身的指针};

01

组合类的构造函数

如前所述,在CStudent类的有参构造函数中可以直接使用内嵌的对象name,这就意味着该对象在程序执行CStudent类的有参构造函数之前就已经调用了MyString的构造函数完成了初始化。为了解释这个问题,就需要介绍初始化列表的概念了。

类的构造函数都带有一个初始化列表,主要作用是为初始化类的数据成员提供一个机会。如果在设计构造函数时没有在初始化列表中给出数据成员的初始化方式,则编译器会采用数据成员的默认的初始化方式——对于类的对象来说就是调用其默认的构造函数——进行初始化,且初始化列表中的内容会在执行构造函数之前执行。这就是在上面例3.5中的CStudent类的有参构造函数中可以使用其成员对象name的原因。

一般地,带初始化列表的构造函数的形式如下(仅以写在类的声明内部为例;写在类的声明外部与此相似,只是需要在函数名前加上类名和域作用符):

class类名{public:类名(): 初始化数据成员1, 初始化数据成员2, ...{}...};

以写在类的声明外部为例,CStudent类的有参构造函数可以写成如下形式。

CStudent::CStudent(intnum,constMyString & name,constMyString & major,doublescore): number(num), name(name), major(major), score(score){cout<<"CStudent的有参构造函数被调用"<<endl;}

其中初始化列表中的第一个name是CStudent的数据成员,第二个name是构造函数中的参数。在这个实现中,由于在初始化列表中使用复制构造函数初始化了name和major,所以在CStudent的构造函数内部就不需要再次为成员name和major赋值了。另外,基本数据类型number和score也可以在初始化列表中初始化,但要注意不能写成类似于number = num的形式。

另外,需要说明的是构造函数的调用顺序。由于初始化列表的存在,在调用组合类的构造函数之前会先调用其成员对象的构造函数,且当有多个成员对象时,C++语言规定按照成员对象在组合类声明中出现的顺序依次构造,而与它们在初始化列表中出现的顺序无关。例如,虽然name和major在上述构造函数的初始化列表中出现的顺序与在下面构造函数的初始化列表中出现的顺序不同,但在执行时都是先初始化name再初始化major,程序如下:

CStudent::CStudent(intnum,constMyString & name,constMyString & major,doublescore): number(num), major(major), name(name), score(score){cout<<"CStudent的有参构造函数被调用"<<endl;}

最后要强调的是,初始化列表可以省去——此时使用数据成员的默认方式初始化,但不意味着没有初始化列表。例如例3.5中,CStudent的默认构造函数实际的实现形式为在初始化列表中调用MyString的默认构造函数初始化name和major,但基本数据类型的成员number和score没有初始化,程序如下:

CStudent() : name(), major(){cout<<"CStudent的默认构造函数被调用"<<endl;}

例3.5中CStudent的有参构造函数实际的实现形式中的初始化列表与上面的类似:仅在初始化列表中使用MyString类的默认构造函数初始化数据成员name和major,没有初始化number和score,程序如下:

CStudent::CStudent(intnum,constMyString &name,constMyString &major,doublescore) : name(), major(){number = num;this->name = name;this->major = major;this->score = score;cout<<"CStudent的有参构造函数被调用"<<endl;}

显然,这个实现中,为初始化name和major需要调用两次MyString类的默认构造函数和两次赋值运算符函数。因此,充分利用初始化列表还可以减少函数调用的次数,提高程序的运行效率。

02

组合类的析构函数

对于CStudent类来说,其析构函数没有多少特殊的地方:其要完成的功能主要是负责该类数据成员的清理。在CStudent类中,由于数据成员没有用到堆内存(对象name和major用到了,但它们由MyString类负责处理),所以不需要专门为它编写析构函数。

不过,对于组合类的析构函数也有需要说明的地方,那就是当组合类的对象超出生存期时析构函数的调用顺序问题。这里只需要遵循一个原则:析构函数的调用顺序与构造函数的调用顺序完全相反。如果把对象的初始化过程比喻为按照严格规程生产一台机器的过程,那么显然需要先按照设定的规程生产各个零部件(相当于调用作为数据成员的对象的构造函数),然后调试整台机器(相当于调用组合类的构造函数);当需要拆卸机器时,需要按照完全相反的顺序拆卸(相当于调用各部分的析构函数),否则就无法拆卸开来。对于CStudent类的对象,调用析构函数的顺序是:调用CStudent类的析构函数析构CStudent类的对象,然后调用MyString类的析构函数析构对象major,最后调用MyString类的析构函数析构对象name。

03

组合类的复制构造函数

正象普通的复制构造函数一样,如果没有编写它,编译器就会自动提供一个,并且其完成的功能就是实现对应数据成员的复制。比如,在例3.5中没有给出CStudent类的复制构造函数,因此编译器会自动提供一个如下形式的复制构造函数——注意在初始化列表中调用了MyString类的复制构造函数来初始化name和major。

classCStudent{public:CStudent(constCStudent & stu);...};CStudent::CStudent(constCStudent & stu) : number(stu.number),name(stu.name), major(stu.major), score(stu.score){}

如果明确给出了复制构造函数的定义,则编译器就不再提供默认的实现,因此关于复制构造函数的一切都需要程序员负责——一定要在初始化列表中使用复制构造函数初始化对象成员,比如下面这个实现就不太好。

CStudent::CStudent(constCStudent & stu){number = stu.number;name = stu.name;major = stu.major;score = stu.score;}

这个实现没有明确给出初始化列表,但这并不意味着没有初始化列表,而是意味着在初始化列表中采用默认的形式对数据成员初始化,即name和major的初始化是通过调用MyString的默认构造函数——而不是复制构造函数——实现的,而基本数据类型的成员number和score没有初始化。也正因为如此,在上面的实现中需要分别为各数据成员赋值,否则将不能正确完成CStudent对象的复制。

04

组合类的赋值运算符

当没有为类提供赋值运算符函数时,编译器会自动提供一个赋值运算符函数,其完成的功能就是对数据成员逐一赋值:对于基本数据类型就是按位赋值,对于对象成员就是调用其赋值运算符函数进行赋值。在CStudent类中,虽然其对象成员name和major使用了堆内存,但因为已经为MyString类提供了实现深复制的赋值运算符函数,因此,编译器为CStudent类自动提供的赋值运算符函数能够正确运行,其实现形式如下:

CStudent & CStudent::operator=(constCStudent & stu){if(this!= &stu)//防止自赋值{number = stu.number;name = stu.name;//调用MyString类的赋值运算符函数major = stu.major;//调用MyString类的赋值运算符函数score = stu.score;}return*this;}

在例3.5中,第50行调用了CStudent类的赋值运算符函数。根据上述编译器为CStudent类自动提供的赋值运算符函数的形式,在函数实现中两次调用MyString类的赋值运算符函数。这正是第50行的程序产生了输出中的第13行和第14行的原因。

本文来源

*本文摘自《C++面向对象程序设计(微课视频版)》第3章3.2节

05

参考书籍

C++面向对象程序设计(微课视频版)

ISBN:9787302586906

董兴业 瞿有利 王涛 编著

定价:49.9元

内容简介

本书介绍了面向对象程序设计思想及其在C++语言中的实现方式。本书采用启发式的叙述方法展现面向对象程序设计的相关知识,语言通俗易懂; 根据封装、继承、多态的顺序编排主要内容,根据学习规律和要求穿插相关内容; 逻辑清楚,内容全面,注重运用; 示例严谨连贯、循序渐进、丰富生动; 在Visual C++ 2015中调试运行,并配有大量习题。全书共9章,分别为面向对象程序设计简介、从C语言到C++语言、类与对象、运算符重载、流类库与输入输出、继承、虚函数和多态、模板与STL、异常处理。本书能帮助读者快速地建立面向对象程序设计的思维方式,获得使用C++语言进行面向对象程序设计的能力。

本书为有C语言基础、希望通过C++语言进一步学习面向对象程序设计的读者编写,适合作为高等院校计算机类、信息管理类及电子信息类等相关专业的教材,也可供自学者使用。

编辑推荐

● 启发式的叙述方式,语言通俗易懂,示例严谨连贯,内容完整全面。● 设计必要场景、逐步完善示例,培养学习者设计和完整封装类的能力。● 强调继承的使用条件,避免使用类似虎父犬子的错误继承关系。

● 完整介绍多态的使用方法,培养灵活运用多态的思维能力。

● 将文件内容提前,通过文件练习理解多态并充分练习文件的使用。● 举例丰富,代码详尽,提供Visual C++ 2015和Dev-C++ 5.11版本的源程序。

06

精彩推荐

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容