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

news2024/11/18 3:27:35

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

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

1.  //MyString类的定义省略
2.  //注意:保留其构造函数、析构函数、复制构造函数和赋值运算符中的输出内容

3.  //file: student.h
4.  #pragma once
5.  #include"MyString.h"
6.  class CStudent
7.  {
8.  public:
9.     CStudent() { cout << "CStudent的默认构造函数被调用" << endl; }
10.     CStudent(int num, const MyString & name,
      const MyString & major, double score);
11.     ~CStudent() { cout << "CStudent的析构函数被调用" << endl; }
12.     void set_number(int num) { number = num; }
13.     int get_number(){ return number; }
14.     MyString & set_name(const MyString & name);
15.     MyString & get_name() { return name; }
16.     MyString & set_major(const MyString & major);
17.     MyString & get_major() { return major; }
18.     void set_score(double score) { this->score = score; }
19.     double get_score() { return score; }
20.  private:
21.     int        number;
22.     MyString name;
23.     MyString major;
24.     double     score;
25.  };

26.  //file: student.cpp
27.  #include"student.h"

28.  CStudent::CStudent(int num, const MyString &name,
   const MyString &major, double score)
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(const MyString & name)
37.  {
38.     this->name = name;
39.     return this->name;
40.  }

41.  //file: main.cpp
42.  #include"student.h"
43.  #include<iostream>
44.  using namespace std;
45.  int main()
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.     return 0;
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. zhangsan
16. zhangsan
17. CStudent的析构函数被调用
18. MyString的析构函数被调用
19. MyString的析构函数被调用
20. CStudent的析构函数被调用
21. MyString的析构函数被调用
22. MyString的析构函数被调用
23. CStudent的析构函数被调用
24. MyString的析构函数被调用
25. MyString的析构函数被调用
26. MyString的析构函数被调用
27. 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】 在声明一个类之前就试图使用这个类则会出现编译错误。

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类的赋值运算符函数
      major = stu.major; //调用MyString类的赋值运算符函数
      score = stu.score;
   }
   return *this;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398493.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode——2379. 得到 K 个黑块的最少涂色次数

一、题目 给你一个长度为 n 下标从 0 开始的字符串 blocks &#xff0c;blocks[i] 要么是 ‘W’ 要么是 ‘B’ &#xff0c;表示第 i 块的颜色。字符 ‘W’ 和 ‘B’ 分别表示白色和黑色。 给你一个整数 k &#xff0c;表示想要 连续 黑色块的数目。 每一次操作中&#xff0…

Java 中的拆箱和装箱

在 Java 中&#xff0c;每个基本数据类型都对应了一个包装类型&#xff0c;比如&#xff1a;int 的包装类型是 Integer&#xff0c;double 的包装类型是 Double…那么&#xff0c;基本数据类型和包装类型有什么区别呢&#xff1f; 大概有以下几点区别&#xff1a; 成员变量的…

详解Vue安装与配置(2023)

文章目录一、官网下载node.js二、安装Node.js三、环境配置四、idea导入vue项目五、IDEA添加Vue.js插件一、官网下载node.js Vue是前端开发框架。搭建框架&#xff0c;首先要搭建环境。搭建Vue的环境工具&#xff1a;node.js&#xff08;JavaScript的运行环境&#xff09;&…

【最重要的 G 代码命令列表】

【最重要的 G 代码命令列表】1. 什么是G代码&#xff1f;2. 如何阅读G代码命令&#xff1f;3. 最重要/最常见的 G 代码命令3.1 G00 – 快速定位3.2 G01 – 线性插值3.3 G02 – 顺时针圆形插值3.4 G00、G01、G02 示例 – 手动 G 代码编程3.4 G03 – 逆时针圆形插补3.5 G20/ G21 …

【Unity游戏破解】外挂原理分析

文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cpp dumper例子…

yolov5双目检测车辆识别(2023年+单目+双目+python源码+毕业设计)

行人识别yolov5和v7对比yolo车距源码:yolov5双目检测车辆识别(2023年单目双目python源码毕业设计)上盒岛APP&#xff0c;开线上盲盒商店http://www.hedaoapp.com/yunPC/goodsDetails?pid4132 为了提高传统遗传算法(genetic algorithm, GA)IGA优化BP网络迭代时间过长以及精度偏…

ArrayList与LinkedList的区别 以及 链表理解

list接口中ArrayList、LinkedList都不是线程安全&#xff0c;Vector是线程安全 1、数据结构不同 ArrayList是Array(动态数组)的数据结构&#xff0c;LinkedList是Link(链表)双向链表的数据结构。 2、空间灵活性 ArrayList最好指定初始容量 LinkedList是比ArrayList灵活的&a…

Noah-MP陆面过程模型建模

【方式】&#xff1a;直播永久回放长期答疑群辅助全套资料【目标】&#xff1a;了解陆表过程的主要研究内容以及陆面模型在生态水文研究中的地位和作用&#xff1b;熟悉模型的发展历程&#xff0c;常见模型及各自特点&#xff1b;理解Noah-MP模型的原理&#xff0c;掌握Noah-MP…

用Python优雅地求解阿基米德分牛问题

文章目录题目大意sympy求解结果题目大意 问 太阳神有一牛群&#xff0c;由白、黑、花、棕四种颜色的公、母牛组成&#xff0c;其间关系如下&#xff0c;求每种牛的个数。 公牛中&#xff0c;白牛多于棕牛&#xff0c;二者之差为黑牛的1213\frac{1}{2}\frac{1}{3}21​31​&…

【Redis】搭建分片集群

目录 集群结构 准备实例和配置 启动 创建集群 测试 集群结构 分片集群需要的节点数量较多&#xff0c;这里我们搭建一个最小的分片集群&#xff0c;包含3个master节点&#xff0c;每个 master包含一个slave节点&#xff0c;结构如下&#xff1a; 这里我们会在同一台虚…

超详细CentOS7 NAT模式(无图形化界面即最小安装)网络配置

在此附上CentOS7&#xff08;无图形化界面最小安装&#xff09;安装教程 超详细VMware CentOS7&#xff08;无图形化界面最小安装&#xff09;安装教程 打开VMware—>点击编辑---->选择虚拟网络编辑器 打开虚拟网络编辑器后如下图所示&#xff1a; 从下图中我们看到标…

由Deep InfoMax开始对比学习

作者&#xff1a;KON 来源&#xff1a;投稿 编辑&#xff1a;学姐 作者介绍&#xff1a;Kon 擅长是自然语言处理、推荐系统&#xff0c;爱好是cv&#xff1b;著有cv相关专利一篇&#xff0c;西安交通大学软件专业本硕。 1.前言 本次给大家带来的是发表在「ICLR2019」上的一篇…

10Wqps评论中台,如何架构?B站是这么做的!!!

说在前面 在尼恩的&#xff08;50&#xff09;读者社群中&#xff0c;经常遇到一个 非常、非常高频的一个面试题&#xff0c;但是很不好回答&#xff0c;类似如下&#xff1a; 千万级数据&#xff0c;如何做系统架构&#xff1f;亿级数据&#xff0c;如何做系统架构&#xff1…

阿里云服务器使用教程:使用xshell、xFtp工具连接阿里云服务器(Centos7)并修改Centos7的yum源为阿里镜像源

目录 1、下载并安装xshell、xFtp 2、远程连接阿里云服务器 3、 修改Centos7的yum源为阿里镜像源 1、下载并安装xshell、xFtp XShell可以在Windows界面下来访问远端不同系统下的服务器&#xff0c;从而比较好的达到远程控制终端的目的。它支持 RLOGIN、SFTP、SERIAL、TELNET、…

STM32中断分组配置NVIC_PriorityGroup,移植操作系统需需注意NVIC_PriorityGroup_4

一、先说明中断分组的由来中断优先级分组表&#xff1a;优先级分组抢占优先级响应优先级bit[7:4] 分配情况备注NVIC_PriorityGroup_0取值&#xff1a;0取值&#xff1a;0~150:40bit抢占优先级、4bit响应优先级NVIC_PriorityGroup_1取值&#xff1a;0~1取值&#xff1a;0~71:31b…

关于热力图展示大量数据点耗时导致浏览器崩溃问题及解决方案

目录 问题描述 问题分析 解决方案 问题描述&#xff1a; Web前端在地图上加载空间数据库里存储的地块中心点时因为数据点太多从而导致页面崩溃。Mybatis查询大量数据时&#xff0c;耗时时间更长是主要原因。8万多条数据&#xff0c;数据库查询最慢0.6s, Mybatis查询结果需要…

【可信平台】开证问题汇总--1.无采购入库记录,2.箱码无产出记录

这里面的问题主要有两类, 批号无采购入库记录箱码无产出记录批号无采购入库记录 第一个问题,以批号 W200263242022100600018 为例。 MES里入库明细里能查到可信平台集成报错: 入库数量>采购数量 再看下入库明细里的情况: 可信平台集成提示物料库存不存在。(没有入库记…

【LeetCode】剑指 Offer(19)

目录 题目&#xff1a;剑指 Offer 36. 二叉搜索树与双向链表 - 力扣&#xff08;Leetcode&#xff09; 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 写在最后&#xff1a; 题目&#xff1a;剑指 Offer 36. …

JUC并发编程共享模型之管程(三)(上)

三 共享模型之管程&#xff08;上&#xff09; 4.1 共享问题 问题发现 Slf4j public class ShareTest01 {static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() -> {for(int i 0; i < 5000; i){count…

jvm理解

1.堆栈 JVM运行字节码时&#xff0c;所有的操作基本都是围绕两种数据结构&#xff0c;一种是堆栈&#xff08;本质是栈结构&#xff09;&#xff0c;还有一种是队列&#xff0c;如果JVM执行某条指令时&#xff0c;该指令需要对数据进行操作&#xff0c;那么被操作的数据在指令…