类 下篇

  • 1.类的6个默认成员函数
  • 2.构造函数
    • 2.1 构造函数出现的原因
    • 2.2 特性
    • 2.3 深刻解读—构造函数可以重载
    • 2.4 深刻解读—默认构造函数
      • 补充:
  • 3.析构函数
    • 3.1概念
    • 3.2 特性
    • 3.3深刻解读
    • 例子 总结
  • 4.拷贝构造函数
    • 4.1 概念
    • 4.2 特性
    • 4.3深刻解读—拷贝构造是构造的一种重载
    • 4.4深刻理解—形参必须用引用来修饰
    • 4.5深刻理解—编译器自动生成的默认拷贝构造函数

1.类的6个默认成员函数

如果一个类中什么都没有, 简称为空类
空类中真的就什么都没有吗? 并不是, 任何类在什么都不写的时候, 编译器会自动生成 6 个默认成员函数
==>默认成员函数: 用户没有显示实现, 编译器生成的成员函数称为默认成员函数
就是这个6个默认成员函数, 如果你没有定义出来, 编译器就会自动调用

2.构造函数

2.1 构造函数出现的原因

相信很多小伙伴们跟我想的一样, 当我们调用函数时, 比如: 栈, 队列, 堆… …, 我们都要初始化一下~. 一个两个的还好说, 十几个的我们就受不了了, 成百上千的更不成遑论!!
这时候就想, 如果编译器能够帮我们自己初始化函数就好了 ⇒ 这样, 我们就害怕初始化函数, 而且也不会忘记初始化函数
我们的构造函数就闪亮登场了!!

构造函数是一个特殊的成员函数, 函数名和类名相同, 对象实例化时会自动调用构造函数,以保证类中的每一个成员变量都有一个合适的初始值, 并且在对象的整个生命周期内只调用一次.

2.2 特性

构造函数 虽然名字叫做构造, 听的有点像是开辟一块空间来创建对象, 实际不然 ==> 构造函数并不会开辟空间创建对象, 而是初始化对象
在明确了构造函数的主要功能, 来了解一下构造函数的特性:

  1. 函数名 和 类名相同
  2. 没有返回值
  3. 对象实例化时编译器会自动调用对应的构造函数(分为内置类型 和 自定义类型)
  4. 构造函数可以重载
  5. 如果我们没有显示定义构造函数, 编译器会自动生成一个无参默认构造函数, 一旦我们默认显示定义, 编译器将不会生成, 而是去使用我们定义的构造函数
  6. 默认构造函数有三个: 无参构造函数, 全缺省构造函数, 编译器默认生成的构造函数, 但是编译器的默认构造函数只能是其中的一个

2.3 深刻解读—构造函数可以重载

class Date{public:// 无参构造Date(){}// 有参构造Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << " " << _month << " " << _day << endl;}private:int _year;int _month;int _day;};int main(){Date d1;// 无参构造不能这样调用// Date d1();d1.Print();Date d2(2023, 5,5);d2.Print();}*****-858993460 -858993460 -8589934602023 5 5*****

总结:

  • 这个构造函数虽然也是函数, 但跟普通的函数有所不同:
    一. 即使没有返回值, 也没有 void
    二. 函数名跟普通函数也有所不同, 函数名是 类名
    三. 调用无参构造函数时, 不能跟普通函数一样, 只能 类名 + 对象 <== 这个也从侧面说明了对象实例化会 自动调用构造函数
    四. 调用有参构造函数时, 就跟上面不一样了, 类名 + 对象 (参数)

疑问:

  1. 为什么调用无参构造, 不能用 类名 + 对象 ()” />
  2. 上面的有参构造能不能用全缺省啊?

上面的函数如果用全缺省的话, 当无参调用时, 虽然语法上是构成重载的, 但是当无参调用时, 就会构成歧义 ==> 这个到底是真的无参调用 还是 全缺省啊~

2.4 深刻解读—默认构造函数

前面我们已经知道, 我们的成员变量的类型分为两种: 内置类型 和 自定义类型. 那么编译器在面对这两种类型生成的构造函数是否相同 是我们现在主要思考的一个问题?? ==> 这里先给出结论: 自定义类型会调用它的默认构造函数, 而编译器不会对内置类型做处理.

那么问题来了: 什么是默认构造函数??
默认构造函数有三种形式: 无参构造函数, 全缺省构造函数, 编译器自动生成的构造函数

由于编译器的不同, 编译器版本的不同… …, 编译器对内置类型所做的处理也就不同. ==>在这里, 我们就默认编译器对内置类型都不做处理~~(本人偷个懒)


那问题来了,自定义类型构造函数是调用默认构造函数. 默认构造函数是从上面三种形式选一个, 那么哪一种比较好呢??
我的建议是全缺省的比较好, 相比较无参, 全缺省可以随时符合我们的选择
相比较于编译器自动生成的构造函数, 编译器生成的构造函数对内置类型不做处理~


补充:

  1. C++ 11 针对 “编译器对内置类型不处理” 做了一些 ‘补丁’ : 用户可以在类的成员变量声明的时候赋初值(注意这里还不是初始化, 因为初始化是针对对象来说的), 以此作为编译器生成的构造函数的缺省值

当然, 编译器的处理我们还是当做不放心的来看, 就跟我们没有初始化 a 数组, 这个版本下的编译器会自动处理, 有的编译器就不会处理. 这种情况下, 我们要不就对全部的内置类型都做处理, 要不然就都不做处理吧~
2.


对构造函数做一个总结吧:

3.析构函数

3.1概念

前面已经了解了构造函数, 现在我们来看一下它的反面 — 析构函数

析构函数: 与构造函数功能相反, 是完成对象中资源的清理工作. 注意: 不是完成对 对象本身的销毁, 局部对象的销毁是由编译器完成的, 而是对象在销毁时会自动调用析构函数, 完成对象中资源的清理工作

3.2 特性

析构函数是特殊的成员函数, 其特性有:

  1. 函数名: ~ + 类名
  2. “三无”: 无参数, 无返回值 和 无void
  3. 一个类只能有一个析构函数. 如果没有显示定义, 系统会自动生成默认的析构函数. 因为三无⇒ 析构函数能重载 ⇒ 所以析构函数比构造函数简单一点
  4. 对象生命周期结束时, 编译器会自动调用析构函数
  5. 编译器默认生成的析构函数, 对内置类型不做处理, 对自定义类型调用它的析构函数

3.3深刻解读

  1. 对象生命周期结束时, 编译器会自动调用析构函数
class Stack{public:// ...//...~Stack(){cout << "~Stack()" << endl;_a = NULL;_top = 0;_capacity = 0;}private:int* _a;int _top = 0;int _capacity = 4;};int main(){Stack st1;Stack st2;Stack st3;}*****~Stack()~Stack()~Stack()*****

通过上面的代码可以发现: 我们并没有调用析构函数, 当对象 st1, st2, st3的生命周期结束时, 即出了main函数, 就会自动调用析构函数来清理对象里面的资源

  1. 编译器默认生成的析构函数对内置类型不做处理, 对自定义类型调用它的析构函数
  • 细心的小伙伴应该就会发现上面的析构函数其实是多此一举的. <== 因为成员变量都是内置类型, 出了函数作用域, 编译器就会自动地进行销毁, 我们就不需要写一个析构函数来处理

  • 那么, 问题来了: 什么时候适合我们写析构函数, 什么时候又可以偷个懒” />

    • 有些小伙伴有些问题了: 如果成员变量里面有自定义类型还有没有申请动态资源的内置类型, 可不可以偷个懒
      答案是当然可以 <== 因为没有申请动态资源的内置类型就不用析构函数区处理, 出了作用域就会销毁.

    例子 总结

    class Time{public:~Time(){cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;};class Date{private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){Date d;return 0;}*****~Time()*****
    • 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
      因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
      而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time 类的析构函数即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
      注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

    4.拷贝构造函数

    4.1 概念

    双胞胎, 两个外表一样却又独立思想的两个人. 这是一个多么美妙的事情啊~~
    如果在我们创建对象时, 能不能创建一个 和已存在对象一模一样的新对象呢? ⇒

    拷贝构造函数: 在用已存在的对象去创建一个相同类型的新对象时由编译器自动调用. 只有单个形参, 该形参是对类型对象的引用(一般用 const 类名& 来修饰)

    4.2 特性

    拷贝构造函数的特性有:

    1. 拷贝构造函数是构造函数的一种重载形式
    2. 拷贝构造函数的参数只有一个 且 必须是类型对象的引用. 使用传值方式编译器会直接报错, 否则将会无限递归下去
    3. 如果没有显示定义, 编译器会生成默认的拷贝构造函数. 默认拷贝构造函数是一种浅拷贝(值拷贝)的行为⇒ 将对象按内存存储按字节完成拷贝(相当于memcpy)

    4.3深刻解读—拷贝构造是构造的一种重载

    class Date{public://Date(int year = 2023, int month = 5, int day = 5)//{//_year = year;//_month = month;//_day = day;//}// 拷贝构造Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;};int main(){Date d1; // error C2512: “Date”: 没有合适的默认构造函数可用}

    上面的代码运行结果就可以看出: 如果我们手动写拷贝构造, 那么我们就必须要写一个构造函数 . 如果我们不写, 就会报错 — 该类没有合适的默认构造函数.
    上面的代码中, 我们期望编译器给我们生成一个默认构造函数出来, 但是我们通过报错就可以看出它是把拷贝构造看成了一个构造, 它们的功能都是初始化对象, ⇒ 这样就肯定调用错误啊~~


    拷贝构造是一种特殊的构造的几个点:

    1. 都是 类(形参列表), 都是无返回值, 无void
    2. 功能都是一样的: 都是在对象实例化时初始化对象

    不同的几个点:
    3. 形参列表不同: 构造函数⇒ 可以无参, 可以有参, 形参数量不确定; 拷贝构造⇒ 只有 且只能有一个形参, 且形参必须用 cons 类名& 来修饰
    4. 构造函数是没有依赖性的, 而拷贝构造函数是有依赖性的⇒ 有一个已经存在的对象, 让这个对象来初始化新建的对象

    4.4深刻理解—形参必须用引用来修饰

    相信大家都有一些疑问: 自定义类型为什么非要传引用啊, 我直接传值不行吗?

    我先问一下: 函数传参的实质是什么呢? ⇒ 让实参去初始化形参~

    我们来假设一下如果自定义类型进行传值传参.
    C++规定: 进行自定义类型传值传参时要先调用拷贝构造函数. 因为要借助拷贝构造函数去初始化形参, 然后再回到调用函数

    根据上图推导过程, 就会发现⇒ 自定义类型如果是传值传参, 就会无限递归下去, 永远不会进去构造函数内部⇒ 所以编译器就会自动把他停止掉⇒ 所以, 即使我们自定义类型 传值传参就不会导致栈溢出~~

    那么, 我们自定义类型该怎么传参呢” />

  • 自定义类型传值传参赋值 都是要调用拷贝构造的, 因为中间要生成一个临时拷贝来充当中间量, 这时候就要用到拷贝构造~~

4.5深刻理解—编译器自动生成的默认拷贝构造函数

前面, 已经知道编译器生成的默认拷贝构造函数是对内置类型也做了处理 — 浅拷贝, 而且非常爽~, 那我们可不可以直接偷懒, 不写拷贝构造函数呢?
答案是不行的, 因为是浅拷贝, 相当于 memcpy, 如果遇到有资源申请的类型, 这样是非常危险的~

总结:

默认拷贝构造函数对内置成员完成浅拷贝, 这个也叫做值拷贝; 自定义类型会调用他的拷贝构造.

换句话说:

如果成员都是没有资源申请的类型, 建议直接偷个懒, 使用默认构造函数; 如果成员里面有资源申请的类型, 建议自己写一个深拷贝构造(深拷贝构造这个以后再说)~~


好大喜功则为宇宙汪洋所吞没,开动脑筋则领悟世界。
——帕斯卡《感想录》