【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。


总结

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

目录
打赏
0
4
4
0
138
分享
相关文章
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
39 16
|
18天前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
63 6
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
88 19