【C++】揭秘类与对象的内在机制(核心卷之构造函数与析构函数的奥秘)

devtools/2025/1/16 8:00:14/

在这里插入图片描述

文章目录

  • 一、类的默认成员函数
  • 二、构造函数
    • 1. 默认生成的构造函数能干什么?
    • 2. 怎么写构造函数
  • 三、析构函数
    • 1. 默认生成的析构函数能干什么?
    • 2. 怎么写析构函数

一、类的默认成员函数

   默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数,它可以进行自动调用,无需使用者手动调用,比如后面介绍的构造函数就是用于给对象进行初始化,之前我们都要给一个类写Init函数,然后调用它给对象初始化,但是如果我们有默认构造函数,那么在创建对象时就会自动进行调用,非常方便

   ⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可,其次就是C++11标准还增加了两个默认成员函数:移动构造和移动赋值,这个我们后⾯再讲解。现在我们来学这6个默认成员函数,它们很重要,也⽐较复杂,我们要从两个⽅⾯去学习:

   1. 我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求
   2. 编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

   以下是这几个默认成员函数的基本作用,大家先认识认识,接下来的两篇文章我们就将它们一一剖析进行讲解,如下:

在这里插入图片描述

二、构造函数

   构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时开的空间),⽽是对象实例化时初始化对象

   构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,利用默认构造函数能够⾃动调⽤的特点完美的替代Init函数,接着我们就从编译器默认生成的构造函数能做什么,以及如果默认生成的构造函数不够用,该怎么写构造函数两个方面来展开学习

1. 默认生成的构造函数能干什么?

   1. 自定义类型:当我们不写构造函数时,编译器会默认⽣成一个构造函数,虽然我们看不到,但是编译器确实会进行生成,这个构造对内置类型成员变量的初始化没有要求,比如int、double等类型,这个默认生成的构造函数对它们进行初始化时没有要求,可能是随机值,也可能是0,要看编译器的具体实现

   2. 而对于⾃定义类型的成员变量,自定义类型的成员变量就是一个类类型或者结构体类型的成员变量,比如我们之前在数据结构那里做过的一道题:使用两个栈实现队列的基本操作,在这个队列中就包含了自定义类型的两个成员变量栈,如下:

class Stack
{//栈的实现...
};class Queue
{
public://使用栈实现队列的各种方法放这里...private://这里的st1和st2就是自定义类型的成员变量Stack st1;Stack st2;
};

   上面演示的伪代码就是两个栈实现队列,可以看到,这里队列的两个成员变量不是int等内置类型,而是一个类类型,也就是自定义类型,那么对于自定义类型,编译器生成的构造函数会如何进行处理呢?

   编译器会调⽤这些自定义类型成员变量的默认构造函数,用它们自己的默认构造函数对这些自定义类型的成员变量进行初始化,默认构造函数不止有编译器默认生成的构造函数,还有全缺省构造函数以及无参构造函数都属于默认构造函数,我们在后面怎么写构造函数部分讲解

   如果这个自定义类型的成员变量,它没有自己的默认构造函数,那么编译器就会报错,要解决这个问题我们需要使⽤初始化列表,初始化列表我们在下一篇文章给大家进行讲解,因为稍微有点复杂,我们先学会基本知识再去学习初始化列表就要好多了

   上面就是默认生成的构造函数的所有行为,**总结一下:**如果是内置类型的成员变量,默认生成的构造函数的处理是由编译器决定,有可能是随机值,也可能是0,如果是自定义类型的成员变量,默认生成的构造函数会调用这个自定义类型的默认构造,如果这个自定义类型没有默认构造编译器就会报错

   那么很显然编译器默认生成的构造函数肯定不够我们使用,所以我们大部分情况还是需要自己来写构造函数,接下来我们来讲讲怎么写构造函数

2. 怎么写构造函数

   我们在写构造函数之前,要先了解手写构造函数的特点以及一些注意事项,所以我们先简单列举一下,如下:
   1. 构造函数的函数名与类名相同,比如类名为Date,那么构造函数的函数名为Date()
   2. 构造函数⽆返回值,也就是它的返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此
    3. 对象实例化时编译器会⾃动调⽤对应的默认构造函数,也就是当创建一个对象时,编译器会自动调用默认构造函数,不需要我们手动进行初始化操作,非常方便
   4 构造函数可以重载,也就是只要参数不同,我们可以有多个构造函数,它们的函数名都是类名,在调用时会根据参数自动确认调用哪个构造函数
   5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,一旦用户显式写了构造函数那么编译器将不再⽣成

   上面就是手写构造函数的注意事项,当然还有几点比较重要的我们放在后面,现在根据上面的5条规则我们就可以实现构造函数了,现在我们来写一个日期类的无参构造函数,就是这个构造函数不需要任何参数,默认全部初始化为2025年1月1日,如下:

class Date
{
public:Date(){//为了照顾新手,这里我们显示使用this指针方便理解this->_year = 2025;this->_month = 1;this->_day = 1;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Print();return 0;
}

   上面的Date函数就是一个日期类的无参构造函数,我们可以观察一下它的特点,它的函数名就是类名,并且没有返回值,void都没有,当我们这个构造函数写出来之后,编译器就不会帮我们生成构造函数了,会直接调用我们写的构造函数

   在上面的代码中,我们实例化出d1这个对象,但是我们并没有对这个对象进行任何的修改,创建好这个对象后我们就直接调用了Print函数对d1进行打印,如果我们没有写构造函数那么肯定就打印的是随机值,但是我们写了构造函数,我们看看编译器会不会自动帮我们调用构造函数去将d1初始化为2025年1月1日,如下:

在这里插入图片描述

   可以看到我们确实什么都没有做,但是d1已经初始化好了,这是编译器帮我们自动调用了日期类的构造函数,我们再来调试一下这段代码看看能不能发现什么猫腻,如下:

在这里插入图片描述

   可以看到,在创建对象时,编译器确实帮我们自动执行了构造函数,现在我们大致会写构造函数了,但是我们感觉它确实有点low,只能初始化为2025年1月1日,那么有没有什么办法既可以在创建对象时让它默认为2025年1月1日,也可以在创建对象时手动修改呢?

   其实是有办法的,我们可以利用前面讲过的函数缺省参数来实现,给每个变量一个缺省值,在我们不传任何参数时它就会按照缺省值进行初始化,如果我们传了参数就会按照给定的参数进行初始化,如下:

Date()
{//为了照顾新手,这里我们显示使用this指针方便理解this->_year = 2025;this->_month = 1;this->_day = 1;
}Date(int year = 2025, int month = 1, int day = 1)
{this->_year = year;this->_month = month;this->_day = day;
}

   可以看到下面的这个构造函数明显比上面的那个构造函数更加全能,在不想传参数的时候就默认初始化为了2025年1月1日,如果想传参数可以自己传,自己传参数的方法我们后面会讲到,这里我们先试试代码能否正常运行,如下:

在这里插入图片描述

   可以看到代码出错了,这是为什么呢?是因为无参的构造函数和全缺省的构造函数在调用时都不需要传参数,函数名又相同,所以编译器不知道到底该调用哪一个函数,所以导致了函数重载并不有效,现在我们注释掉上面的无参构造即可,如下:

在这里插入图片描述

   现在说明白了不传参数怎么使用,现在我们来学习如何自己传参数给构造函数进行初始化,其实很简单,就是在对象名后面加一对小括号,在小括号内部传参,听起来可能有点绕,举一个例子就明白了,如下:

在这里插入图片描述

   可以看到,我们默认情况下的初始化应该是2025年1月1日,现在我们自己传了参数之后就使用的我们自己传的参数进行初始化,所以写这么一个全缺省构造使用起来是嘎嘎香的,不写参数就用函数缺省值初始化,写参数就按写的参数初始化,非常好用

   那么是不是所有构造函数都可以这样不写参数就能自动调用呢?既然我这么提问了,那么答案很明显就是不能了,为了讲明白这件事,我们接下来引入默认构造和非默认构造函数的概念,只有默认构造函数才能在对象实例化时自动进行调用

   默认构造函数有三种,即没有写构造函数时编译器默认生成的构造函数、无参构造函数、全缺省构造函数,这三种函数也是我们刚刚重点讲的三种函数,它们都有一个特点就是:即使用户不传参数也能正常自动调用,所以才能在实例化对象时自动调用

   那我们试想一下,如果我们的函数是半缺省或者直接不是缺省函数,那么是不是就说明这个构造一定需要我们进行传参,如果我们不传参,又没有对应的缺省值,那么编译器也不知道该填什么值上,也就不能帮我们自动调用了,如下:

Date(int year, int month, int day)
{this->_year = year;this->_month = month;this->_day = day;
}

   上面演示的默认构造函数就不是一个带有缺省值的参数,很明显不属于那三种默认构造函数,如果对象实例化时编译器去自动调用这个构造函数,就会发现三个形参没有值可以用,一定会报错,所以C++就规定了只有上面那三种默认构造可以对构造函数进行自动调用

   如果我们写的构造函数中没有出现上面说的那三种默认构造,那么我们在初始化对象时就必须传参进行初始化,否则就会报错,我们可以来测试一下,如下:

在这里插入图片描述

   如果我们写的构造函数中没有默认构造函数,那么就会提示没有适合的默认构造函数可用,这个时候我们就必须手动传参进行初始化,如下:

在这里插入图片描述

所以最后我们来总结上面的知识点,形成我们写构造函数的第6条规则:
   6. 只有默认构造函数可以在对象初始化时自动调用,⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,这三种构造函数都叫做默认构造函数,但是这三个函数有且只有⼀个存在,不能同时存在
   ⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义,我们上面也演示了的,如果我们写了无参构造函数或者全缺省构造函数,那么编译器也就不会自动生成构造函数,所以这三种默认构造不能同时出现

三、析构函数

   析构函数相较于上面的构造函数就要简单多了,它是用来清理我们需要释放的资源,那么哪些资源需要我们手动释放呢?大部分情况下是我们从堆上申请来的空间,当对象被销毁后,我们需要释放掉堆上申请的空间,否则会引起内存泄漏

   它就相当于我们在数据结构部分经常写的Destroy函数,只不过我们之前在C语言部分写的Destroy函数需要我们手动调用去进行释放,但是不方便不说,怕就怕我们忘记调用销毁函数,所以C++在此基础上做了优化,当对象的生命周期结束时自动调用析构函数进行资源的释放

1. 默认生成的析构函数能干什么?

   默认情况下编译器也会生成一个析构函数,如果是内置类型的成员它就什么也不做,如果遇到了自定义类型的成员,那么就会调用这个自定义类型成员的析构函数对资源进行清理,和构造函数很像

   但是我们要注意的是,如果这个自定义类型没有写析构,编译器也不会报错,这个时候就很容易造成内存泄漏,所以我们在写一个自定义类型时,一定要权衡一下什么时候要写析构,什么时候不用写析构函数,关键就看我们是否在堆上申请了空间,只有在堆上申请了空间才需要写析构函数来清理资源

   比如我们的日期类,它的三个成员变量都是int类型,它们就不存在去堆上申请空间,只会在开辟函数栈帧时在栈上申请空间,所以不用写析构函数来清理资源,又比如我们要实现一个栈,它存放数据时使用了的是动态申请空间的数组,而动态申请的空间是来自堆的,这个时候就需要我们手动写析构函数来清理资源,如下:

class Date
{
public://日期类的实现...private://都是从栈上申请空间,所以日期类不需要写析构int _year;int _month;int _day;
};class Stack
{
public://栈的实现...private://从堆上动态申请了空间,要写析构函数int* _arr;int _top;int _capacity;
};

   那么现在我们知道了编译器自动生成的析构函数能做什么,并且知道了什么时候需要写析构函数,什么时候不需要写析构函数,接下来我们就来学习如果要写析构函数,该怎么写

2. 怎么写析构函数

首先我们还是列出析构函数的一些简单的特点,如果有难点再从后面进行分析,如下:
   1. 析构函数名是在类名前加上字符 ~ ,比如Stack类的析构函数函数名为 ~Stack
   2. 析构函数无参数无返回值,跟构造函数类似,void也不需要写
   3. ⼀个类只能有⼀个析构函数,若未显式定义,系统会自动生成一个析构函数
   4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数对资源进行释放
   5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,而⾃定义类型成员会调⽤他的析构函数
   6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会自动调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数,所以手动写析构时不需要管自定义类型的成员
   7. 如果⼀个局部域实例化出了多个对象,那么C++规定后定义对象的先进行析构,这个经常考试中考到

   那么我们现在大致知道了析构函数的特点,接下就拿Stack来练练手,析构其实就和Desroy函数一模一样,当然,为了练习一下构造函数,我们在写析构前可以把构造函数写来练练手,可以自己先尝试尝试,如下:

#include <iostream>
using namespace std;typedef int STDataType;class Stack
{
public://为了继续照顾新手,增强this指针的理解,我们依旧显示写出this指针Stack(int n = 4){this->_arr = (STDataType*)malloc(n * sizeof(STDataType));if (_arr = nullptr){perror("malloc");return;}this->_top = 0;this->_capacity = n;}~Stack(){if (this->_arr)free(_arr);this->_arr = nullptr;this->_top = this->_capacity = 0;}private:STDataType* _arr;int _top;int _capacity;
};

   上面就是Stack的构造函数和析构函数的实现,我们可以发现,析构函数写起来确实和之前我们学过的Destroy函数差不多,只是函数名和返回值特殊,但是这一点和我们上面学过的构造函数差不多,所以理解起来也不难,但是它非常好用,在对象销毁时自动调用

   那么今天构造函数与析构函数的分享就到这里结束啦,但是我们的核心卷还没有结束,在下一篇文章我们会详细介绍剩下的4种默认成员函数,即拷贝构造、赋值重载以及取址重载
   bye~


http://www.ppmy.cn/devtools/150885.html

相关文章

【I/O编程】UNIX文件基础

IO编程的本质是通过 API 操作 文件。 什么是 IO I - Input 输入O - Output 输出 这里的输入和输出都是站在应用&#xff08;运行中的程序&#xff09;的角度。外部特指文件。 这里的文件是泛指&#xff0c;并不是只表示存在存盘中的常规文件。还有设备、套接字、管道、链接…

比较之舞,优雅演绎排序算法的智美篇章

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言正文一、冒泡排序&#xff1a;数据海…

对受控组件和非受控组件的理解?应用场景?

受控组件与非受控组件的理解与应用 在 React 中&#xff0c;组件可以通过两种方式管理表单元素的状态&#xff1a;受控组件和非受控组件。这两者在处理表单输入数据时有很大的区别&#xff0c;理解它们的应用场景和优劣对于开发者来说非常重要。 目录结构&#xff1a; 受控组…

qt-C++笔记之自定义继承类初始化时涉及到parents的初始化

qt-C笔记之自定义继承类初始化时涉及到parents的初始化 code review! 参考笔记 1.qt-C笔记之父类窗口、父类控件、对象树的关系 2.qt-C笔记之继承自 QWidget和继承自QObject 并通过 getWidget() 显示窗口或控件时的区别和原理 3.qt-C笔记之自定义类继承自 QObject 与 QWidget …

Linux入门——权限

shell命令以及运行原理 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;“ &#xff0c;但我们一般用户&#xff0c;不能直接使用kernel。 而是通过kernel的“外壳”程序&#xff0c;也就是所谓的shell&#xff0c;来与kernel…

【网络云SRE运维开发】2025第3周-每日【2025/01/15】小测-【第14章ospf高级配置】理论和实操

文章目录 【网络云SRE运维开发】2025第3周-每日【2025/01/15】小测-【第14章ospf高级配置】理论和实操 14.1选择题 在H3C设备上配置OSPF时&#xff0c;以下哪个命令用于启动OSPF进程&#xff1f; A. [H3C] ospf enable B. [H3C] ospf 1 C. [H3C] ospf start D. [H3C] ospf proc…

基于华为atlas的重车(满载)空车(空载)识别

该教程主要是想摸索出华为atlas的基于ACL的推理模式。最终实现通过煤矿磅道上方的摄像头&#xff0c;识别出车辆的重车&#xff08;满载&#xff09;、空车&#xff08;空载&#xff09;情况。本质上是一个简单的检测问题。 但是整体探索过程比较坎坷&#xff0c;Tianxiaomo的…

集合帖:区间问题

一、AcWing 803&#xff1a;区间合并 &#xff08;1&#xff09;题目来源&#xff1a;https://www.acwing.com/problem/content/805/ &#xff08;2&#xff09;算法代码&#xff1a;https://blog.csdn.net/hnjzsyjyj/article/details/145067059 #include <bits/stdc.h>…