- }
其输出如下:
- MyString的有参构造函数被调用
- MyString的有参构造函数被调用
- MyString的默认构造函数被调用
- MyString的默认构造函数被调用
- MyString的赋值运算符函数被调用
- MyString的赋值运算符函数被调用
- CStudent的有参构造函数被调用
- MyString的默认构造函数被调用
- MyString的默认构造函数被调用
- CStudent的默认构造函数被调用
- MyString的复制构造函数被调用
- MyString的复制构造函数被调用
- MyString的赋值运算符函数被调用
- MyString的赋值运算符函数被调用
- zhangsan
- zhangsan
- CStudent的析构函数被调用
- MyString的析构函数被调用
- MyString的析构函数被调用
- CStudent的析构函数被调用
- MyString的析构函数被调用
- MyString的析构函数被调用
- CStudent的析构函数被调用
- MyString的析构函数被调用
- MyString的析构函数被调用
- MyString的析构函数被调用
- MyString的析构函数被调用
对于例1,在定义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类的复制构造函数,但例1中没有设计该函数,因此执行的是编译器自动提供的复制构造函数;程序第50行是一个赋值运算,由于例1中也没有为CStudent设计赋值运算符函数,因此编译器自动提供了默认的赋值运算符函数。显然程序第51行和第52行产生的输出为输出中的第15行和第16行,因此程序第49行和第50行产生的输出为输出中的第11行至第14行:显示调用了两次MyString类的复制构造函数和两次赋值运算符函数。根据输出的第15行和第16行的内容相同,且程序运行正常,可以判断编译器自动提供的复制构造函数和赋值运算符是正确的。那么,编译器自动提供的复制构造函数和赋值运算符函数是什么样的?
(4)例1中设计了CStudent类的析构函数,但在其函数体中只有一条输出语句。这是因为CStudent类中没有涉及动态内存分配,因此不涉及回收堆内存的问题。注意,MyString类型的对象name和major涉及了堆内存,不过回收其堆内存的工作由MyString类的析构函数完成。从程序中可以看出,对象的析构顺序为:依次析构对象stu3、stu2和stu,然后析构对象major和name。整个析构过程产生了输出中的第17行至第27行,其中析构stu3产生了输出中的第17行至第19行。那么,析构组合类对象stu3为什么是这样的一个过程?
下面会解释上面提出的问题。不过,在此之前,先介绍一下类的前向引用声明问题,因为这个问题在定义组合类时经常会用到。
在C++语言中,使用基本数据类型的变量时需要遵循先声明后引用的规则。与此类似,在定义新的类型时也要遵循这一规则。例如在例1中,在定义CStudent类之前,先通过预编译指令引入了MyString类的定义(例1的第5行)。在声明一个类之前就试图使用这个类则会出现编译错误,如例2所示。
【例2】 在声明一个类之前就试图使用这个类则会出现编译错误。
-
class A
-
{
-
public:
-
void A_fun(B b); //因之前没有声明类型B,故这里试图引用B会造成编译错误
-
int i;
-
};
-
class B
-
{
-
public:
-
void B_fun(A a);
-
int j;
-
};
在例2中,在类A的定义中引用了类B。然而,B类还没有被声明,所以会造成编译错误。解决办法是进行前向类型声明,比如在声明A之前加入声明语句“class B;”。
进行了类的前向声明之后,仅能保证声明的符号可见,但在给出类的具体定义之前,并不能涉及类的具体内容,如下面的程序。
class B;
class A
{
public:
int A_fun(B b){ return b.j; } //在给出B的具体定义之前涉及了其
//具体内容,所以会出现编译错误
int i;
};
class B
{
public:
int B_fun(A a);
int j;
};
在上面的程序中,类A的函数A\_fun()试图访问对象b的数据成员j,即试图引用B类的具体内容。然而,在此之前,类B的具体定义尚未给出,所以会出现编译错误。解决办法是将该函数的实现写在类外并且在类B的完整定义之后。
类似地,在给出类的完整定义之前,不能定义类的对象,因为定义类的对象就会涉及对象的构造,从而会涉及类的具体内容,如下面的程序。
class B;
class A
{
public:
int A_fun(B b);
B m_b; //在给出类B的完整定义之前定义B的对象会造成编译错误
A m_a; //在类A的定义内部定义A的对象会造成编译错误
};
class B
{
public:
int B_fun(A a);
int j;
};
在上面的程序中,类A试图定义B的对象m\_b和A的对象m\_a,然而此时类B和类A的定义都不完整,因而会造成编译错误。解决办法是:首先把类B的完整定义放到类A的定义之前;其次,在类A中不能定义类A的对象,只能定义类A的指针,如下面的程序。
class A; //因为定义类B时引用了类A,所以需要做前向声明
class B
{
public:
int B_fun(A a);
int j;
};
class A
{
public:
int A_fun(B b){ return b.j; } //前面已有类B的完整定义,故该语句正确
B m_b; //前面已有类B的完整声明,故此处能够定义类B的对象
A* m_pa; //永远不能在类定义中定义自身的对象,可以定义自身的指针
};
## 01、组合类的构造函数
如前所述,在CStudent类的有参构造函数中可以直接使用内嵌的对象name,这就意味着该对象在程序执行CStudent类的有参构造函数之前就已经调用了MyString的构造函数完成了初始化。为了解释这个问题,就需要介绍初始化列表的概念了。
类的构造函数都带有一个初始化列表,主要作用是为初始化类的数据成员提供一个机会。如果在设计构造函数时没有在初始化列表中给出数据成员的初始化方式,则编译器会采用数据成员的默认的初始化方式——对于类的对象来说就是调用其默认的构造函数——进行初始化,且初始化列表中的内容会在执行构造函数之前执行。这就是在上面例1中的CStudent类的有参构造函数中可以使用其成员对象name的原因。
一般地,带初始化列表的构造函数的形式如下(仅以写在类的声明内部为例;写在类的声明外部与此相似,只是需要在函数名前加上类名和域作用符):
class 类名
{
public:
类名(): 初始化数据成员1, 初始化数据成员2, …
{
}
…
};
以写在类的声明外部为例,CStudent类的有参构造函数可以写成如下形式。
CStudent::CStudent(int num, const MyString & name,
const MyString & major, double score)
: 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(int num, const MyString & name,
const MyString & major, double score)
: number(num), major(major), name(name), score(score)
{
cout << “CStudent的有参构造函数被调用” << endl;
}
最后要强调的是,初始化列表可以省去——此时使用数据成员的默认方式初始化,但不意味着没有初始化列表。例如例1中,CStudent的默认构造函数实际的实现形式为在初始化列表中调用MyString的默认构造函数初始化name和major,但基本数据类型的成员number和score没有初始化,程序如下:
CStudent() : name(), major()
{
cout << “CStudent的默认构造函数被调用” << endl;
}
例1中CStudent的有参构造函数实际的实现形式中的初始化列表与上面的类似:仅在初始化列表中使用MyString类的默认构造函数初始化数据成员name和major,没有初始化number和score,程序如下:
CStudent::CStudent(int num, const MyString &name,
const MyString &major, double score) : name(), major()
{
number = num;
this->name = name;
this->major = major;
this->score = score;
cout << “CStudent的有参构造函数被调用” << endl;
}
显然,这个实现中,为初始化name和major需要调用两次MyString类的默认构造函数和两次赋值运算符函数。因此,充分利用初始化列表还可以减少函数调用的次数,提高程序的运行效率。
## 02、组合类的析构函数
![img](https://img-blog.csdnimg.cn/img_convert/86b0b6d3d2e00a7afae9a758900631ef.png)
![img](https://img-blog.csdnimg.cn/img_convert/1315d9ef4d215c3067524714ddadf3a9.png)
![img](https://img-blog.csdnimg.cn/img_convert/3c473c8375624f813f501bb802261fbc.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
中...(img-SziPBLFu-1714552653901)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**