C++面向对象编程之二:构造函数、拷贝构造函数、析构函数

news2025/4/20 3:44:18

构造函数和析构函数

C++利用构造函数和析构函数,完成对象的初始化和清理工作。

对象的初始化和清理工作,是编译器强制我们要做的事情,如果我们不提供构造函数和析构函数,编译器会提供3个函数:

  1. 默认无参构造函数

  1. 默认拷贝构造函数

  1. 默认析构函数

构造函数:在对象初始化时,对对象的成员属性赋初始值。构造函数由编译器自动调用,不用手动调用。

拷贝构造函数:在对象初始化时,将一个已有的对象的所有成员属性拷贝到这个被创建的对象上。拷贝构造函数由编译器自动调用,不用手动调用。

析构函数:在对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:

类名()
{
}
  1. 构造函数没有返回值,也不用写void。

  1. 构造函数的函数名跟类名相同。

  1. 构造函数可以有参数,因此可以发生重载。

  1. 构造函数会在程序在创建对象的时候,被自动调用,不用手动调用,而且创建该对象只会调用一次。

拷贝构造函数语法:

类名(const 类名 &obj)
{
}
  1. 拷贝构造函数没有返回值,也不用写void。

  1. 拷贝构造函数的函数名和类名相同。

  1. 拷贝构造函数的参数是固定的,并且只有一个参数,这个参数为:const 类名 &obj。

  1. 拷贝构造函数被调用的时机

  1. 在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。

  1. 将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。

  1. 函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。

析构函数语法:

~类名()
{
}
  1. 析构函数没有返回值,也不用写void。

  1. 析构函数的函数名跟类名相同,并且在函数名之前加上~。

  1. 析构函数不可以有参数,因此不能发生重载。

  1. 析构函数在对象销毁前会自动被调用,不用手动调用,而且只会调用一次。

example:设计一个怪物类,并测试打印无参构造函数,有参构造函数,拷贝构造函数,析构函数被调用的时机

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        cout << "Monster()无参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        cout << "Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
        cout << "Monster(const int monsterId)有参构造函数被调用" << endl;
    }
    ~Monster()
    {
        cout << "~Monster()析构函数被调用" << endl;
    }
    private:
    int m_monsterId; //怪物id
};

int main(int argc, char *argv[])
{
    Monster m1; //无参构造函数被调用
    Monster m2(10001); //有参构造函数被调用
    Monster m3(m2); //拷贝构造函数被调用
    
    return 0;
}

对象被创建的三种方法

  1. 括号法

//括号法
Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明
Monster m2(10001); //有参构造函数被调用
Monster m3(m2); //拷贝构造函数被调用
  1. 等号法

//等号法
Monster m4; //注意:不是Monster m4 = Monster();写成这样子相当于手动调用无参构造函数,
            //但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象
Monster m5 = Monster(10001); //有参构造函数被调用
Monster m6 = Monster(m5); //拷贝构造函数被调用
Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)
//Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,
               //编译器会认为:Monster(m5); == Monster m5;
  1. 隐式等号法

//隐式等号法
Monster m7 = 10001; //相当于:Monster m7 = Monster(10001);
Monster m8 = m7; //相当于:Monster m8 = Monster(m7);

注意:

Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明

Monster m4; //注意:不是Monster m4 = Monster(); 写成这样子相当于手动调用无参构造函数,

//但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象

Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)

//Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,编译器会认为:Monster(m5); == Monster m5;

example:验证对象被创建的三种方法

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        cout << "Monster()无参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        cout << "Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
        cout << "Monster(const int monsterId)有参构造函数被调用" << endl;
    }
    ~Monster()
    {
        cout << "~Monster()析构函数被调用" << endl;
    }
    private:
    int m_monsterId; //怪物id
};

int main(int argc, char *argv[])
{
    //括号法
    Monster m1; //注意:不是 Monster m1(); 写成这样子编译器会不知道这个是创建一个对象,还是函数声明
    Monster m2(10001); //有参构造函数被调用
    Monster m3(m2); //拷贝构造函数被调用

    //等号法
    Monster m4; //注意:不是Monster m4 = Monster(); 写成这样子相当于手动调用无参构造函数,
                //但构造函数是没有返回值的,这时m4 = void; 并没有创建一个对象
    Monster m5 = Monster(10001); //有参构造函数被调用
    Monster m6 = Monster(m5); //拷贝构造函数被调用
    Monster(10002); //匿名对象,当前行执行完毕,系统会立即回收掉这个匿名对象(即这时析构函数会被调用)
    //Monster(m5); //错误:不要利用拷贝构造函数初始化匿名对象,编译器会认为:Monster(m5); == Monster m5;

    //隐式等号法
    Monster m7 = 10001; //相当于:Monster m7 = Monster(10001);
    Monster m8 = m7; //相当于:Monster m8 = Monster(m7);

    return 0;
}

拷贝构造函数

  1. 拷贝构造函数被调用的时机:

  1. 在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用。

  1. 将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用。

  1. 函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。

example:验证拷贝构造函数被调用的时机

#include <iostream>
using namespace std;

int line = 0;

class Monster
{
    public:
    Monster()
    {
        m_monsterId = 0;
        m_blood = 0;
        line++;
        cout << line << "行:Monster()无参构造函数被调用" << endl;
    }
    Monster(const int monsterId, const int blood)
    {
        m_monsterId = monsterId;
        m_blood = blood;
        line++;
        cout << line << "行:Monster(const int monsterId, const int blood)有参构造函数被调用" << endl;
    }
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
        m_blood = m.m_blood;
        line++;
        cout << line << "行:Monster(const Monster &m)拷贝构造函数被调用" << endl;
    }
    ~Monster()
    {
        line++;
        cout << line << "行:~Monster()析构函数被调用" << endl;
    }

    void setMonsterId(const int monsterId)
    {
        m_monsterId = monsterId;
    }
    int getMonsterId()
    {
        return m_monsterId;
    }

    void setBlood(const int blood)
    {
        m_blood = blood;
    }
    int getBlood()
    {
        return m_blood;
    }

    private:
    int m_monsterId; //怪物id
    int m_blood; //血量
};

void subMonsterBlood(Monster m, const int val)
{
    int blood = m.getBlood() - val;
    if (blood < 0)
        blood = 0;
    
    m.setBlood(blood);
}

Monster getTempMonster(const int monsterId, const int blood)
{
    Monster m(monsterId, blood);
    return m;
}

int main(int argc, char *argv[])
{
    Monster m1(10001, 1000); //有参构造函数被调用
    Monster m2(m1); //在创建对象时,用一个已有对象来初始化这个被创建的对象时,拷贝构造函数会被调用

    subMonsterBlood(m1, 500); //将一个已有的对象,作为函数的实参,进行值传递时,拷贝构造函数会被调用

    Monster m3 = getTempMonster(10002, 15000); //函数的返回值为对象,并且将一个对象返回时,其实这时编译器会调用拷贝
                                               //构造函数,拷贝一个临时的对象进行返回
    
    return 0;
}

g++ monster_copy_constructor.cpp -o monster_copy_constructor 编译链接生成可执行文件

根据打印结果进行代码分析:

main函数内:
Monster m1(10001, 1000); 
(打印第1行)有参构造函数被调用

Monster m2(m1);
在创建对象时,用一个已有对象来初始化这个被创建的对象时,(打印第2行)拷贝构造函数被调用

subMonsterBlood(m1, 500); 
将一个已有的对象,作为函数的实参,进行值传递时,此时,(打印第3行)拷贝构造函数被调用,拷
贝所有成员属性给形参,这个函数调用执行结束后,形参被析构,所以(打印第4行)析构函数被调用

Monster m3 = getTempMonster(10002, 15000);
调用getTempMonster函数,这个函数体内执行:Monster m(monsterId, blood);
所以(打印第5行)有参构造函数被调用。按照getTempMonster函数的返回值为对象,并且将一个
对象返回时,其实这时编译器会调用拷贝构造函数创建一个临时的对象进行返回。但程序并没有打印
拷贝构造函数被调用,这是为什么呢?


main函数执行结束
m3被释放,所以(打印第6行)析构函数被调用
m2被释放,所以(打印第7行)析构函数被调用
m1被释放,所以(打印第8行)析构函数被调用

程序调用getTempMonster函数返回一个对象,程序并没有打印拷贝构造函数被调用,这是为什么呢?

其原因是:RVO(return value optimization),被G++进行值返回时优化了,具体的RVO的相关技术,可以百度。

我们可以将RVO优化关闭,可以对g++增加选项-fno-elide-constructors,重新编绎之后

g++ monster_copy_constructor.cpp -fno-elide-constructors -o monster_copy_constructor

接下来我们再根据打印结果进行代码分析:

main函数内:
Monster m1(10001, 1000); 
(打印第1行)有参构造函数被调用

Monster m2(m1);
在创建对象时,用一个已有对象来初始化这个被创建的对象时,(打印第2行)拷贝构造函数被调用

subMonsterBlood(m1, 500); 
将一个已有的对象,作为函数的实参,进行值传递时,此时,(打印第3行)拷贝构造函数被调用,拷
贝所有成员属性给形参,这个函数调用执行结束后,形参被析构,所以(打印第4行)析构函数被调用

Monster m3 = getTempMonster(10002, 15000);
调用getTempMonster函数,这个函数体内执行:Monster m(monsterId, blood);
所以(打印第5行)有参构造函数被调用。getTempMonster函数的返回值为对象,并且将一个
对象返回时,其实这时编译器会(打印第6行)调用拷贝构造函数创建一个临时的对象进行返回。
此时,getTempMonster函数执行结束,函数体内创建的临时对象m会被释放,所以
(打印第7行)析构函数被调用
回到main函数,将返回的临时的对象利用隐式等号法赋值给m3,相当于执行:
Monster m3 = Monster(temp);所以所以(打印第8行)拷贝构造函数被调用
当这句代码执行结束后,临时对象temp被释放,所以(打印第9行)析构函数被调用

main函数执行结束
m3被释放,所以(打印第10行)析构函数被调用
m2被释放,所以(打印第11行)析构函数被调用
m1被释放,所以(打印第12行)析构函数被调用
  1. 浅拷贝与深拷贝

浅拷贝:对成员属性进行简单的赋值操作的拷贝构造函数,编译器提供的默认的拷贝构造函数就是浅拷贝。

深拷贝:对于可以简单赋值的成员属性进行简单的赋值操作,对于在堆区的成员属性,在堆区重新申请空间,进行拷贝操作。

example:验证浅拷贝会导致程序崩掉的情况,以及应该用深拷贝进行避免因浅拷贝出现的问题

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster()
    {
        m_monsterId = 0;
        mp_blood = new int(0);
    }

    Monster(const int monsterId, const int blood)
    {
        m_monsterId = monsterId;
        mp_blood = new int(blood);
    }

    /*浅拷贝*/
    // Monster(const Monster &m)
    // {
    //     m_monsterId = m.m_monsterId;
    //     mp_blood = m.mp_blood; //浅拷贝,正确的做法:用深拷贝,mp_blood这个成员变量是在堆中申请的空间,
    //                            //我们应该在堆中重新申请空间,进行拷贝操作
    // }
    
    /*深拷贝*/
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
        mp_blood = new int(*m.mp_blood); //深拷贝,mp_blood这个成员变量是在堆中申请的空间,我们在堆中
                                         //重新申请空间,进行拷贝操作
    }

    ~Monster()
    {
        if (mp_blood != NULL) //如果用浅拷贝,会出现mp_blood空间多次被重复释放的,导致程序崩掉
        {
            delete mp_blood;
            mp_blood = NULL;
        }
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << ",怪物血量 = " << *mp_blood << endl; 
    }

    private:
    int m_monsterId; //怪物id
    int *mp_blood; //血量
};

int main(int argc, char *argv[])
{
    Monster m1(10001, 1000);
    m1.print_monster_info();

    Monster m2(m1);
    m2.print_monster_info();
    
    return 0;
}

浅拷贝时出错打印输出:代码中将浅拷贝实现打开,深拷贝实现注释掉

深拷贝时,程序正确输出:

对象的初始化和清理工作,是编译器强制我们要做的事情:

  1. 如果我们不提供构造函数和析构函数,编译器会提供3个函数:

  1. 默认无参构造函数,函数体是空实现

  1. 默认拷贝构造函数,函数体是值拷贝(浅拷贝)

  1. 默认析构函数,函数体是空实现

  1. 如果我们提供了有参构造函数,编译器会提供2个函数

  1. 默认拷贝构造函数,函数体是值拷贝(浅拷贝)

  1. 默认析构函数,函数体是空实现

  1. 如果我们提供了拷贝构造函数,那么编译器只提供1个函数

  1. 默认析构函数,函数体是空实现

example:验证我们提供了有参构造函数,编译器不会再提供默认无参构造函数

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster(const int monsterId)
    {
        m_monsterId = monsterId;
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << endl;
    }
    private:
    int m_monsterId;
};

int main(int argc, char *argv[])
{
    //Monster m1; //错误:Monster类只提供了有参构造函数,那么编译器就不会提供默认无参构造函数了
    Monster m2(10001);
    Monster m3(m2); //正确:Monster类只提供了有参构造函数,那么编译器就会提供默认拷贝构造函数
    m3.print_monster_info();
    return 0;
}

example:验证我们提供了拷贝构造函数,编译器就不再提供默认无参构造函数

#include <iostream>
using namespace std;

class Monster
{
    public:
    Monster(const Monster &m)
    {
        m_monsterId = m.m_monsterId;
    }

    void print_monster_info()
    {
        cout << "怪物id = " << m_monsterId << endl;
    }
    private:
    int m_monsterId;
};

int main(int argc, char *argv[])
{
    //Monster m1; //错误:Monster类只提供了拷贝构造函数,那么编译器就不会提供默认无参构造函数了
    //m1.print_monster_info();

    return 0;
}

好了,关于C++面向对象编程之二:构造函数、拷贝构造函数、析构函数,先写到这。

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

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

相关文章

004+limou+HTML——(4)HTML表格

000、前言 表格在实际开发中的应用还是比较多的&#xff0c;表格可以更加清晰地排列数据 001、基本结构 &#xff08;1&#xff09;构成 表格&#xff1a;<table>行&#xff1a;<tr>&#xff08;table row&#xff0c;表格行&#xff09;&#xff0c;由多少组t…

【每日随笔】中国当前社会阶层 ( 技术无关 | 随便写写 )

文章目录一、阶层划分根据收入划分的阶层根据分工逻辑划分根据权利划分二、根据社会地位和掌握的资源划分的阶层三、赚钱的方式四、如何进入高阶层看了一个有意思的视频 , 讲的是中国当前的社会阶层 , 感觉好有道理 , 搜索了一些资料 ; 参考资料 : 关于中国的社会阶层社会在分…

【一】【socket聊天室】-多线程,socket编程

本文主要实现基于socket编程的聊天室&#xff0c;主要分为下面三个步骤&#xff1a; &#xff08;1&#xff09;多用户聊天&#xff1a;一个服务器多个客户端&#xff0c;客户端信息显示在公共的服务端窗口&#xff0c;利用多线程实现&#xff1b; ——客户端双线程&#xff1a…

OpenCV基础(一)

1.认识图像&#xff08;彩色图中每一个像素点都包含三个颜色通道RGB&#xff0c;数值范围为0~255&#xff0c;0代表黑色&#xff0c;255代表白色&#xff09; import cv2 #opencv 读取的格式为BGRimg cv2.imread(cat.png) #读取图像 cv2.imshow(cat, img) #显示图像img&#x…

Matlab实现FFT变换

Matlab实现FFT变换 文章目录Matlab实现FFT变换原理实现手算验证简单fft变换和频谱求取功率谱结论在信号处理中&#xff0c;快速傅里叶变换&#xff08;FFT&#xff09;是一种非常常见的频域分析方法。本文将介绍如何使用Matlab实现FFT变换&#xff0c;并通过Matlab代码演示实际…

SAP ABAP 深度解析Smartform打印特殊符号等功能

ABAP 开发人员可以在 Smartform 输出上显示 SAP 图标或 SAP 符号。例如,需要在 SAP Smart Forms 文档上显示复选框形状的输出。SAP Smartform 文档上可以轻松显示空复选框、标记复选框以及 SAP 图标等特殊符号。 在 SAP Smartform 文档中添加一个新的文本节点。 1. 单击“更…

开发一款系统软件的流程步骤是什么

在如今的数字化时代&#xff0c;软件开发成为了一个重要的行业。无论是大型企业还是小型创业公司&#xff0c;软件开发都是不可或缺的一环。在本文中&#xff0c;我将介绍一些网上常见的软件开发步骤&#xff0c;以便开发者能够更好地理解和实践。1、需求分析需求分析是开发系统…

基于transformer的多帧自监督深度估计 Multi-Frame Self-Supervised Depth with Transformers

Multi-Frame Self-Supervised Depth with Transformers基于transformer的多帧自监督深度估计0 Abstract 多帧深度估计除了学习基于外观的特征外&#xff0c;也通过特征匹配利用图像之间的几何关系来改善单帧估计。我们采用深度离散的核极抽样来选择匹配像素&#xff0c;并通过一…

基于Jeecgboot前后端分离的ERP系统开发代码生成(六)

商品信息原先生成的不符合要求&#xff0c;重新生成&#xff0c;包括一个附表商品价格信息表 一、采用TAB主题一对多的模式 因为主键&#xff0c;在online表单配置是灰的&#xff0c;所以不能进行外键管理&#xff0c;只能通过下面数据库进行关联录入&#xff0c;否则online界面…

案例19-遇见问题的临时解决方案和最终解决方案

目录1、背景介绍2、两种解决方案的概念1、临时解决方案&#xff1a;2、最终解决方案&#xff1a;3、排查问题过程4、总结站在用户的角度思考作为软件开发者5、升华1、背景介绍 首先说明这是系统很早之前的时候的一个功能&#xff0c;当时和学习通还有很强的耦合关系。在学习通…

研究链表空间销毁问题

&#x1f4af;&#x1f4af;&#x1f4af; 1.研究链表空间销毁问题 当链表使用完后&#xff0c;需要将链表销毁&#xff0c;那么该如何销毁呢&#xff1f; void SLTDestroy(SLTNode* phead)//销毁单链表 {SLTNode* cur phead;while(cur){free(cur);cur cur->next;} }你…

Linux下Nginx安装使用

一、下载解压nginx # 进入要放安装包的目录 cd /opt/software # 下载安装包 wget https://nginx.org/download/nginx-1.20.2.tar.gz # 解压缩 tar -zxvf nginx-1.20.2.tar.gz -C /opt/modules # 进入解压后的目录 cd /opt/modules/nginx-1.20.2/二、安装nginx 1、安装编译器 …

剑指 Offer II 021. 删除链表的倒数第 n 个结点

题目链接 剑指 Offer II 021. 删除链表的倒数第 n 个结点 mid 题目描述 给定一个链表&#xff0c;删除链表的倒数第 n个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; …

MySQL的多表操作

多表关系 介绍 实际开发中&#xff0c;一个项目通常需要很多张表才能完成。例如&#xff1a;一个商城项目就需要分类表(category)、商品表(products)、 订单表(orders)等多张表。且这些表的数据之间存在一定的关系&#xff0c;接下来我们将在单表的基础上&#xff0c;一起学习…

DolphinDB 机器学习在物联网行业的应用:实时数据异常率预警

数据异常率预警在工业安全生产中是一项重要工作&#xff0c;对于监控生产过程的稳定性&#xff0c;保障生产数据的有效性&#xff0c;维护生产设备的可靠性具有重要意义。随着大数据技术在生产领域的深入应用&#xff0c;基于机器学习的智能预警已经成为各大生产企业进行生产数…

logback无法删除太久远的日志文件?logback删除日志文件源码分析

logback无法删除太久远的日志文件&#xff1f;logback删除日志文件源码分析 最近发现logback配置滚动日志&#xff0c;但是本地日志文件甚至还有2年前的日志文件&#xff0c;服务器是却是正常的&#xff01; 网上搜索了一波没有发现&#xff0c;只找到说不能删除太久远的旧日志…

Leetcode. 21 合并两个有序列表

尾插 核心思路&#xff1a;依次比较 &#xff0c;取经过比较后较小值进行尾插 cur1 指向list1 ,cur 2指向list2 ,当cur1走完list1 或者cur2 走完list2 后停止 如果cur1走完list1 ,可以将cur2 整个拿下来尾插 如果cur2走完list2 ,可以将cur1 整个拿下来尾插 特殊情况 &#xff1…

c# 32位程序突破2G内存限制

起因 在开发过程中&#xff0c;由于某些COM组件只能在32位程序下运行&#xff0c;程序不得不在X86平台下生成。而X86的32位程序默认内存大小被限制在2G。由于程序中可能存在大数量处理&#xff0c;期间对象若没有及时释放或则回收&#xff0c;内存占用达到了1.2G左右&#xff…

瀑布开发与敏捷开发的区别,以及从瀑布转型敏捷项目管理的5大注意事项

事实证明&#xff0c;瀑布开发管理模式并不适合所有的软件项目&#xff0c;但敏捷项目管理却对大多数项目有效。那么当团队选择转型敏捷的时候有哪些因素必须注意&#xff1f;敏捷开发最早使用者大多是小型、独立的团队&#xff0c;他们通常致力于小型、独立的项目。正是他们的…

Keepalive+LVS群集部署

KeepaliveLVS群集部署一、Keepalive概述1、什么是Keepalive2、Keepalive工作原理3、Keepalive主要模块及作用4、Keepalived 服务重要功能&#xff08;1&#xff09;管理 LVS 负载均衡软件&#xff08;2&#xff09;支持故障自动切换&#xff08;3&#xff09;实现 LVS 负载调度…