Bootstrap

高效学 C++|组合类的构造函数(1)

1.  class A
2. {
3.  public:
4.     void A_fun(B b); //因之前没有声明类型B,故这里试图引用B会造成编译错误
5.     int i;
6.  };

7.  class B
8. {
9.  public:
10.     void B_fun(A a);
11.     int j;
12.  };

在例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、组合类的析构函数

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

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

03、组合类的复制构造函数

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

class CStudent
{
public:
   CStudent(const CStudent & stu);
   ...
};

CStudent::CStudent(const CStudent & stu) : number(stu.number),
    name(stu.name), major(stu.major), score(stu.score)
{
}

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

CStudent::CStudent(const CStudent & 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=(const CStudent & stu)
{
   if (this != &stu) //防止自赋值
   {
      number = stu.number;
      name = stu.name; //调用MyString类的赋值运算符函数


![img](https://img-blog.csdnimg.cn/img_convert/572640ce92ea27a52325b80f3a2fd75c.png)
![img](https://img-blog.csdnimg.cn/img_convert/5dfd148d85a1d2d1a2ee64f0f1ef218e.png)
![img](https://img-blog.csdnimg.cn/img_convert/f3e7b8be3b3117a3a89b3ce35c574e6b.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

中...(img-kVobv5oa-1714552620199)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

;