【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)

简介: 本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。

前言

       之前我们在 类和对象(上)中了解了关于类的定义、对象的创建等一些基本知识:


https://developer.aliyun.com/article/1637204?spm=a2c6h.13262185.profile.8.204b2c70t7pAjo


今天,我们深入学习类和对象中默认成员函数相关的内容。


什么是默认成员函数

       所谓默认成员函数,就是在类当中我们没有显式实现,但是编译器自动生成的成员函数称之为默认成员函数。在c++11之前,默认成员函数一共有六个:



接下来我们会根据它们的特点,使用规则以及自实现方面逐一讲解。


一、构造函数

       构造函数的主要作用是:在对象被创建时,调用该函数对其成员变量进行初始化。就像我们在实现栈和队列时写的Init函数一样,会对它的成员先赋初值。


它的特点如下:


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

2. 构造函数无返回值。(连void都不写)

3. 构造函数可以重载。

4. 当对象被创建时,自动调用构造函数。


代码示例:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //这里我们手动创建一个构造函数
    MyClass(int a = 0, int b = 0, int c = 0)//不传参时给个默认值为0
    {
        _a = a;
        _b = b;
        _c = c;
    }
    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
        cout << _c << endl;
    }
private:
    int _a;
    int _b;
    int _c;
};
 
int main()
{
    MyClass a;
 
    //打印一下数据
    a.Print();
    return 0;
}

运行结果:



可以看到,三个成员变量的值被初始化为0。这说明对象在创建时构造函数是自动调用的。接下来我们尝试给构造函数传参:

int main()
{
    //可以用类似函数调用的方式传参
    MyClass a(1, 2, 3);
 
    //也可以使用类似结构体初始化的方式传参
    MyClass b = { 4,5,6 };
 
    //打印一下数据
    a.Print();
    b.Print();
    return 0;
}

运行结果:



它还有以下三点特性:


5. 当我们在类中没有显示地定义构造函数时,编译器会自动生成一个无参的构造函数,用于创建对象时的初始化。一旦用户显示定义了构造函数之后,编译器则不会生成。

6. 显示定义的无参构造函数、全缺省构造函数,以及编译器自动生成的构造函数统称为默认构造函数。在一个类当中,这三种函数必须且只能存在一个。总的来说,不传参就可以调用的构造函数称之为默认构造函数。

7. 对于编译器自动生成的构造函数,当其对对象成员变量进行初始化时,如果成员是内置类型,则编译器通常不会为其赋初值;如果成员是由class或者struct创建的自定义类型(也就是类嵌套的情况),则会自动调用该自定义类型的默认构造函数。如果该成员没有默认构造函数,就会报错。这也就是默认构造函数必须存在的原因。


总结


       构造函数就是用于对创建的对象进行初始化的函数。我们在创建对象时,编译器会自动调用构造函数对成员变量进行初始化,这样我们就不需要单独定义或者使用Init函数对某个类进行初始化了


二、析构函数

       与构造函数相反,析构函数是在对象销毁时调用的,它的作用是在对象被销毁时完成对对象生成的资源的清理释放工作。就像我们在实现队列时使用的Destroy函数一样,完成对数据的销毁。


它的特点如下:


1. 析构函数的函数名是在类名之前加一个波浪号(~)。


2. 析构函数无返回值(void也不写),且不能加入参数。


3. 一个类当中只能有一个析构函数。


4. 当一个对象的生命周期结束之时,会自动调用析构函数。


5. 当我们没有在类中显示定义析构函数时,编译器会自动生成一个析构函数,供对象调用。


代码示例:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //构造函数
    MyClass(int a = 0, int b = 0, int c = 0)
    {
        _a = a;
        _b = b;
        _c = c;
    }
 
    //析构函数
    ~MyClass()
    {
        _a = 0;
        _b = 0;
        _c = 0;
    }
 
    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
        cout << _c << endl;
    }
private:
    int _a;
    int _b;
    int _c;
};
 
int main()
{
    MyClass a(1, 2, 3);
    return 0;
}

调试观察:



可以看到,程序中我们创建对象时,给三个成员变量分别赋初值1、2、3,而当程序运行结束时,这三个成员变量的值已经变为了0,这说明对象销毁时确实自动调用了析构函数


6. 与构造函数类似,对于编译器自己生成的析构函数,当其对象被销毁时,内置类型成员变量通常不被处理;对于自定义类型成员变量,则会调用其析构函数。

7. 对于一个局部域中的多个对象在进行销毁时,c++规定后创建的对象先析构。


那么我们什么时候该显示写析构函数呢?来看一段代码:

class A
{
public:
    //...
private:
    int _a;
    char _c;
};
 
class B
{
public:
    B(int n = 4)//初始化时在堆区申请内存空间
    {
        _p = (int*)malloc(n * sizeof(int));
        if (_p == nullptr)
        {
            perror("malloc");
            exit(1);
        }
        _n = n;
    }
private:
    int* _p;
    int _n;
};

对于类A,他所创建的对象并没有申请额外的内存空间,在销毁时不会造成内存泄漏,此时我们就不需要手动写析构函数;对于类B,由于它在创建时在堆区申请了空间,它在销毁时编译器自己生成的析构函数并不会将这部分空间销毁掉,需要我们手动释放,所以此时就需要我们显示地写析构函数。


总的来说,如果类中没有申请资源,一般不需要手动写析构函数;如果申请了资源,就需要写析构函数,否则会造成内存泄漏。

三、拷贝构造函数

       拷贝构造函数是构造函数的一个重载,它用于完成对象的拷贝。它的特点如下:


1. c++规定对象只要发生拷贝行为,就必须调用拷贝构造,包括对象传参或者做返回值,都需要产生一份临时拷贝。

2. 拷贝构造函数的第一个参数必须是类类型的引用,而不是对象的值。因为对象在传值传参的时候需要调用拷贝构造,如果拷贝构造的参数带有对象的临时拷贝,那就会再次调用拷贝构造,以至于发生无限递归。

3. 如果我们没有显示定义拷贝构造函数,编译器会自动生成一个拷贝构造。这个自动生成的拷贝构造在完成拷贝工作时,对内置类型会完成它的浅拷贝,对类类型则会调用该类的拷贝构造函数。


接下来我们尝试写一个拷贝构造函数并且使用它:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //构造函数
    MyClass(int a = 0, int b = 0, int c = 0)
    {
        _a = a;
        _b = b;
        _c = c;
    }
 
    //拷贝构造函数
    MyClass(const MyClass& m)//确保源数据不被修改,在引用之前加上const
    {
        //逐一完成成员变量的复制
        _a = m._a;
        _b = m._b;
        _c = m._c;
    }
 
    //析构函数
    ~MyClass()
    {
        _a = 0;
        _b = 0;
        _c = 0;
    }
 
    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
        cout << _c << endl;
    }
private:
    int _a;
    int _b;
    int _c;
};
 
int main()
{
    MyClass a1(1, 2, 3);//创建对象a1并对其初始化
    MyClass a2(a1);//调用拷贝构造,将a1拷贝给a2
 
    //打印一下a2
    a2.Print();
    return 0;
}

运行结果:



可以看到,我们通过拷贝构造函数将a1拷贝给了a2。


       那么我们什么时候需要显示写拷贝构造函数供我们使用呢?之前我们提到,编译器自动生成的拷贝构造完成的是浅拷贝。这就意味着如果我们在类中有向堆区申请内存空间的方法,浅拷贝就无法达到预期效果



所以对于这种情况(类中有额外申请资源),我们就需要手动去写一个拷贝构造函数,实现深拷贝将申请的内存也复制一份出来


小技巧:是否需要显示写拷贝构造函数,就看类中是否有显示写析构函数。如果有写析构函数,那么通常需要写拷贝构造。


        当我们在某个函数当中将对象作为返回值时,由于这个返回值是一份临时拷贝,所以会自动调用拷贝构造函数,造成运行效率的下降。所以此时我们可以考虑返回该对象的引用,避免发生拷贝,提高运行效率。需要注意的是:一定要确保该对象在函数栈帧销毁后仍然存在,避免出现悬挂引用。


四、赋值重载

       在了解赋值重载之前,我们先学习一个概念:运算符重载


1. 运算符重载

       所谓运算符重载,指的就是当对象在使用一些运算符时,我们可以为该运算符设定新的含义。而这种含义的实现方式就是通过定义函数,该函数就叫做运算符重载。


       当对象在使用运算符时,如果没有对应的运算符重载,就会发生报错。


       它的定义方式如下:


(返回值类型) operator(运算符)(函数参数)

{

       (函数体)

}


这里的operator是一个关键字,与需要定义的运算符相连接,构成函数名


关于运算符重载,有以下要注意的几点:


      1. 运算符重载的参数个数与该运算符的操作数一样多。例如 + 号进行重载时,第一个参数表示左操作数,第二个参数表示右操作数。如果这个运算符重载是成员函数,一定要注意成员函数第一个位置已经有一个参数是this指针,所以我们要少写一个参数。

       2. 当我们使用一个运算符重载时,要注意该运算符本来的优先级和结合性是不变的。

       3. 不能以“莫须有”的方式去重载本来就没有的运算符,例如operator@。

       4. 这五个运算符不能重载: .*    : :    sizeof    ? :    .  

       5. 我们在定义运算符重载时,必须要有类类型的参数,否则就会与重载的本意相悖。

       6. 对于++和--运算符的重载,由于前置和后置无法区分,所以c++规定:对于后置++/--,需要在函数的参数中增加一个哑元(通常是int类型),这个参数不在函数体中使用,但是有了这个参数就表示重载的是后置++/--。


小知识


第 4 点中有一个运算符 “ .* ”,有很多人可能没有接触过这个运算符,我们来介绍一下它。


首先让我们创建一个类,这个类当中只有一个成员函数:

class A
{
public:
    void fun()
    {
        cout << "Hello World" << endl;
    }
};

接下来,我们将该函数的地址存储在一个函数指针当中:

int main()
{
    void (A::*pf)() = &A::fun;
}

可以看到,以上代码非常奇怪。实际上,对于类的成员函数,我们在声明它的类型时,要表明它所在的类域。其次,对于类的成员函数,想要得到它的地址,需要加上&符号,而普通函数是否加&都表示它的地址。


接下来,我们创建一个A类对象,并通过该指针调用函数fun:

int main()
{
    void (A::*pf)() = &A::fun;
    A a;
    (a.*pf)();
}

运行结果:



可以看到,运行成功了。这里我们在调用函数时,就使用到了“ .* ”运算符,它用于通过函数指针调用类的成员函数。


接下来,我们针对MyClass类,尝试实现运算符重载:+ 。

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //构造函数
    MyClass(int a = 0, int b = 0, int c = 0)
    {
        _a = a;
        _b = b;
        _c = c;
    }
 
    //拷贝构造函数
    MyClass(const MyClass& m)
    {
        _a = m._a;
        _b = m._b;
        _c = m._c;
    }
 
    //析构函数
    ~MyClass()
    {
        _a = 0;
        _b = 0;
        _c = 0;
    }
 
    //+号重载
    //我们定义一个含义:对象加上一个整数,该对象的所有成员变量都加上这个整数
    MyClass operator+(int a)
    {
        MyClass tmp(*this);//将该对象的内容拷贝给临时变量tmp
        tmp._a += a;
        tmp._b += a;
        tmp._c += a;
        return tmp;//返回tmp的临时拷贝,表示的值就是加后的值,并且原对象未发生改变
    }
 
    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
        cout << _c << endl;
    }
private:
    int _a;
    int _b;
    int _c;
};

接着,我们来使用这个运算符重载:

int main()
{
    MyClass a;//创建对象a
 
    //将a与数字相加的值拷贝给其他对象
    MyClass b(a + 1);//可以直接使用运算符
    MyClass c(a.operator+(3));//也可以使用函数调用的方式
 
    b.Print();
    cout << endl;
    c.Print();
    return 0;
}

运行结果:



可以看到,运算符重载的编写成功了。注意:不管是用什么方式去使用运算符重载,本质都是函数调用


2. 赋值运算符重载

       了解了运算符重载的概念、特性、定义方法以及使用方法之后,我们切入正题--赋值重载。


       顾名思义,赋值重载就是对赋值运算符的重载函数,这个函数有点类似于拷贝构造,它的功能是完成已经存在的对象的拷贝赋值,这一点要和拷贝构造区分。


它的特点如下:


1. 赋值重载是运算符重载中的一种,必须重载为成员函数。一般情况下,它的参数和返回值都是当前类类型的引用,这样会减少拷贝提高效率。

2. 当我们没有显示写出赋值重载时,编译器会自动生成。自动生成的赋值重载会对内置类型成员变量完成浅拷贝,对于自定义类型成员变量,则会调用其赋值重载函数。

3. 与拷贝构造相同,如果我们的类中申请了资源,则需要自己显示写赋值重载来完成深拷贝;若没有申请资源,则可直接使用自动生成的赋值重载。


小技巧:是否需要显示写赋值重载函数,就看类中是否有显示写析构函数。如果有写析构函数,那么通常需要写赋值重载。


接下来我们针对MyClass类实现一个简单的赋值重载:

#include <iostream>
using namespace std;
 
class MyClass
{
public:
    //构造函数
    MyClass(int a = 0, int b = 0, int c = 0)
    {
        _a = a;
        _b = b;
        _c = c;
    }
 
    //拷贝构造函数
    MyClass(const MyClass& m)
    {
        _a = m._a;
        _b = m._b;
        _c = m._c;
    }
 
    //析构函数
    ~MyClass()
    {
        _a = 0;
        _b = 0;
        _c = 0;
    }
 
    //+号重载
    MyClass operator+(int a)
    {
        MyClass tmp(*this);
        tmp._a += a;
        tmp._b += a;
        tmp._c += a;
        return tmp;
    }
 
    //赋值重载
    MyClass& operator=(MyClass& src)
    {
        _a = src._a;
        _b = src._b;
        _c = src._c;
        return *this;//返回当前对象的引用可以完成连续赋值
    }
 
    void Print()
    {
        cout << _a << endl;
        cout << _b << endl;
        cout << _c << endl;
    }
private:
    int _a;
    int _b;
    int _c;
};
 
int main()
{
    MyClass a(1, 2, 3);
 
    MyClass b;
    MyClass c;
    c = b = a;
 
    b.Print();
    cout << endl;
    c.Print();
    return 0;
}

运行结果:



可以看到,我们成功将a的内容赋值给了b和c。


总结

       今天我们学习了四个类的默认成员函数以及它们的特点、使用方法:构造函数、析构函数、拷贝构造函数和赋值重载,它们能够确保资源的正确管理和对象状态的正确维护。之后博主会和大家分享其余的两个默认成员函数和其他知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
4天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
8天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
4天前
|
人工智能 运维 双11
2024阿里云双十一云资源购买指南(纯客观,无广)
2024年双十一,阿里云推出多项重磅优惠,特别针对新迁入云的企业和初创公司提供丰厚补贴。其中,36元一年的轻量应用服务器、1.95元/小时的16核60GB A10卡以及1元购域名等产品尤为值得关注。这些产品不仅价格亲民,还提供了丰富的功能和服务,非常适合个人开发者、学生及中小企业快速上手和部署应用。
|
13天前
|
人工智能 弹性计算 文字识别
基于阿里云文档智能和RAG快速构建企业"第二大脑"
在数字化转型的背景下,企业面临海量文档管理的挑战。传统的文档管理方式效率低下,难以满足业务需求。阿里云推出的文档智能(Document Mind)与检索增强生成(RAG)技术,通过自动化解析和智能检索,极大地提升了文档管理的效率和信息利用的价值。本文介绍了如何利用阿里云的解决方案,快速构建企业专属的“第二大脑”,助力企业在竞争中占据优势。
|
15天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3936 2
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
4天前
|
算法 安全 网络安全
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
2024阿里云11.11金秋云创季活动火热进行中,活动月期间(2024年11月01日至11月30日)通过折扣、叠加优惠券等多种方式,阿里云WoSign SSL证书实现优惠价格新低,DV SSL证书220元/年起,助力中小企业轻松实现HTTPS加密,保障数据传输安全。
499 3
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
|
10天前
|
安全 数据建模 网络安全
2024阿里云双11,WoSign SSL证书优惠券使用攻略
2024阿里云“11.11金秋云创季”活动主会场,阿里云用户通过完成个人或企业实名认证,可以领取不同额度的满减优惠券,叠加折扣优惠。用户购买WoSign SSL证书,如何叠加才能更加优惠呢?
985 3
|
8天前
|
机器学习/深度学习 存储 人工智能
白话文讲解大模型| Attention is all you need
本文档旨在详细阐述当前主流的大模型技术架构如Transformer架构。我们将从技术概述、架构介绍到具体模型实现等多个角度进行讲解。通过本文档,我们期望为读者提供一个全面的理解,帮助大家掌握大模型的工作原理,增强与客户沟通的技术基础。本文档适合对大模型感兴趣的人员阅读。
407 17
白话文讲解大模型| Attention is all you need
|
8天前
|
算法 数据建模 网络安全
阿里云SSL证书2024双11优惠,WoSign DV证书220元/年起
2024阿里云11.11金秋云创季火热进行中,活动月期间(2024年11月01日至11月30日),阿里云SSL证书限时优惠,部分证书产品新老同享75折起;通过优惠折扣、叠加满减优惠券等多种方式,阿里云WoSign SSL证书将实现优惠价格新低,DV SSL证书220元/年起。
560 5
|
4天前
|
安全 网络安全
您有一份网络安全攻略待领取!!!
深入了解如何保护自己的云上资产,领取超酷的安全海报和定制鼠标垫,随时随地提醒你保持警惕!
694 1
您有一份网络安全攻略待领取!!!