【C++入门到精通】C++入门 —— 类和对象(拷贝构造函数、赋值运算符重载、const成员函数)上

简介: 【C++入门到精通】C++入门 —— 类和对象(拷贝构造函数、赋值运算符重载、const成员函数)上

一、前言


        这一篇文章是上一篇的续集(这里有上篇链接)前面我们讲了C语言的基础知识,也了解了一些数据结构,并且讲了有关C++的命名空间的一些知识点以及关于C++的缺省参数、函数重载,引用 和 内联函数。也相信大家都掌握的不错,接下来博主将会带领大家继续学习有关C++比较重要的知识点——类和对象(拷贝构造函数、赋值运算符重载、const成员、取地址及const取地址操作符重载)。下面话不多说坐稳扶好咱们要开车了。


4f1e1bbf0ac7261756e82e5b3a7f952c_e2ed0e2c659b426383f6037ab1e379b3.png


二、拷贝构造函数


⭕拷贝构造函数概念


       拷贝构造函数是C++中的一个特殊成员函数,用于创建对象的副本。它的作用是通过使用已有对象的属性值来初始化新对象,实现对象的复制操作。通过定义拷贝构造函数,我们可以控制对象的拷贝过程,并确保正确处理含有指针或动态分配内存的类。


拷贝构造函数的定义形式如下:


类名(const 类名& 对象名)
{
    // 构造函数的主体部分
    // 将对象的属性值拷贝到新对象
}


⭕拷贝构造函数的特点


       拷贝构造函数在C++中具有自动调用、形参类型为const引用、逐一复制对象成员、隐式调用与显式调用、需要自定义情况等特点下面我会按顺序逐一分析:


       1. 自动调用:拷贝构造函数会在特定场景下自动调用,例如对象的初始化、对象作为参数传递给函数、函数返回对象等情况。无需显式调用,编译器会根据需要自动选择合适的拷贝构造函数进行调用。


       2. 形参类型为const引用:拷贝构造函数的参数类型通常是一个常引用(const引用),即“ const类名& 对象名 ”。这是为了避免修改原对象的属性值,保证在拷贝过程中原对象不会被修改。


       3. 对象的成员逐一复制:拷贝构造函数会将原对象的属性值通过复制或拷贝的方式赋值给新对象的相应属性。对于基本数据类型的成员变量,会直接进行值的复制;对于对象类型的成员变量,会调用该对象的拷贝构造函数进行复制。


       4. 隐式调用与显式调用:在大多数情况下,拷贝构造函数会由编译器隐式调用,无需手动编写代码。然而,在某些特殊情况下,需要显式地调用拷贝构造函数,例如通过拷贝构造函数初始化新对象、创建对象数组、创建对象的副本等。


       5. 需要自定义情况:当类中存在指针成员、动态分配的内存或资源时,通常需要自己定义拷贝构造函数。这是因为默认的拷贝构造函数只进行浅拷贝,即简单地复制指针成员的值,而不会复制指针所指向的内存。因此,需要手动编写拷贝构造函数来进行深拷贝,确保新对象和原对象具有独立的内存空间。


       6. 只针对同类对象:拷贝构造函数只能用于同类对象之间的拷贝,无法用于不同类之间的对象拷贝。如果需要实现类之间的对象拷贝,可以使用转换构造函数或赋值运算符重载等方式。


       掌握拷贝构造函数的特点有助于正确地实现对象的复制操作,并解决潜在的问题。


⭕拷贝构造函数的几种类型


       C++中常见的拷贝构造函数的几种类型包括:默认拷贝构造函数、自定义浅拷贝拷贝构造函数以及自定义深拷贝拷贝构造函数。


       1. 默认拷贝构造函数:如果没有显式定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数。默认拷贝构造函数执行的是逐一复制成员的操作,即将原对象的每个成员属性的值赋值给新对象的对应成员属性。然而,如果类中存在指针成员、动态分配的内存或资源,该默认拷贝构造函数通常不能满足需求,可能会导致浅拷贝问题。


       2. 自定义浅拷贝拷贝构造函数:自定义浅拷贝拷贝构造函数会简单地复制原对象的成员属性的值给新对象的对应成员属性。这种拷贝构造函数适用于对象中没有指针、动态分配的内存或资源的情况,因为它不会进行深拷贝。


       3. 自定义深拷贝拷贝构造函数:在类中存在指针成员、动态分配的内存或资源时,需要自定义拷贝构造函数来进行深拷贝。自定义深拷贝拷贝构造函数会为新对象的指针成员分配独立的内存,并将原对象的指针指向的内容复制到新对象中,确保对象的独立性和数据完整性。


下面这段代码,展示了一个具有指针成员的类的自定义深拷贝拷贝构造函数:


class MyClass {
private:
    int* data;
public:
    // 自定义深拷贝拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int;
        *data = *(other.data);
    }
    // ...
};

       在上面的代码中,自定义的拷贝构造函数通过new操作符为新对象的指针成员分配独立的内存,并将原对象的指针指向的值复制到了新对象中,实现了深拷贝。


       需要根据类的具体情况来选择是否需要自定义拷贝构造函数,并根据类中是否存在指针、动态分配的内存或资源来决定是否需要进行深拷贝。


       总的来说,根据类中成员的特点和需求,选择合适的拷贝构造函数类型,确保对象的复制操作正确、高效地执行。拷贝构造函数在C++中有诸多优点,它们使得对象的复制、传递和返回更加方便和安全。通过合理定义和使用拷贝构造函数,可以提高代码的可维护性和可读性,避免资源冲突和问题的发生,并使得程序的设计更加灵活和优雅。


三、赋值运算符重载


⭕运算符重载概念


       在C++中,运算符重载(Operator Overloading)是一种特性,允许程序员重新定义或重新定义运算符的操作行为。运算符重载允许我们使用相同的运算符来执行不同类型的操作,使得代码更加简洁、直观和易于理解。


       通过运算符重载,我们可以为用户自定义的类、枚举类型以及内置的数据类型(如整数、浮点数等)定义运算符的行为。这就意味着我们可以使用常规运算符(如"+", "-", "*", "/"等)来操作自定义类型的对象,以及在自定义类型之间实现特定的运算。


🚨注意:(  .*   ::   sizeof   ?:   .  )  以上5个运算符不能重载。


       运算符重载使用特定的函数来实现。通过重载运算符的函数,我们可以指定运算符在自定义类型上的操作方式。运算符重载函数是成员函数或非成员函数,其命名形式为"operator op",其中"op"表示要重载的运算符。


       类型转换运算符:重载类型转换运算符允许我们将一个类类型转换为另一个类型。通过定义转换运算符函数,我们可以自定义对象之间的类型转换行为,使得对象的使用更加灵活。重载类型转换运算符使用关键字 "operator type()",其中 " type " 表示要转换的目标类型。


       运算符重载应该根据常理、一致性、可读性和符合预期的原则进行操作,避免过度使用和滥用运算符重载,以免导致代码可读性差、难以理解和不符合预期的行为。


       运算符重载是C++中的一项重要特性,它允许重新定义运算符的操作行为,使得对用户自定义类型和内置类型的对象可以使用相同的运算符来进行操作。通过重载运算符的函数,我们可以定制对象的运算符行为,使代码更加简洁、直观和易于理解。


⭕赋值运算符重载


       在C++中,赋值运算符(=)是用于将一个对象的值赋给另一个对象的运算符。通过重载赋值运算符,我们可以自定义对象之间的赋值操作行为,使得在赋值时能够正确进行成员属性的拷贝,而不仅仅是简单的指针复制或浅拷贝。


赋值运算符的重载函数一般采用类似于以下形式的函数签名:


class MyClass {
public:
    // 赋值运算符重载函数
    MyClass& operator=(const MyClass& other) {
        // 在这里进行对象的赋值操作
        // 包括成员变量的拷贝或释放资源等
        // 并返回新的对象引用
        return *this;
    }
    // ...
};

在赋值运算符重载函数中,我们需要注意以下几点:


🔴函数的返回类型通常为一个引用类型 MyClass& ,返回新的对象引用。这是为了支持赋值链的操作,例如 a = b = c

🔴函数的形参类型是一个常引用 const MyClass& ,表示传入的参数是一个常量对象的引用。这是为了确保原对象在赋值过程中不会被修改。

🔴在重载赋值运算符的函数体内,我们需要完成对象的赋值操作。这通常包括逐个成员的赋值或拷贝,并处理动态分配的内存等资源。


下面这段代码,展示了一个类中赋值运算符的重载:


class MyString {
private:
    char* data;
public:
    // 赋值运算符重载函数
    MyString& operator=(const MyString& other) {
        // 避免自我赋值
        if (this == &other)
            return *this;
        // 释放当前的资源
        delete[] data;
        // 深拷贝数据
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
        return *this;
    }
    // ...
};


       在上面的代码中,重载了赋值运算符的函数,避免了自我赋值的情况。首先释放当前对象的资源,然后进行深拷贝,确保新对象与原对象的数据是独立的。这样,通过赋值运算符重载,我们可以使用赋值操作对类对象进行正确的拷贝和赋值。


       🚨注意:除非有特殊需求,否则不应当滥用赋值运算符重载。C++提供的默认赋值运算符对大多数的类和数据类型都能够正常工作,只有在存在资源管理和深拷贝等特殊情况下才需要自定义赋值运算符重载。


       通过重载赋值运算符,我们可以自定义对象之间的赋值操作行为。通过正确实现赋值运算符重载函数,可以确保对象属性的正确拷贝和处理,使得赋值操作在自定义类中更加符合预期和安全。


⭕前置++和后置++重载


        前置递增运算符和后置递增运算符的重载函数名称的区别在于后置递增运算符的参数列表中有一个额外的int参数(但实际上并不使用该参数),通过重载前置递增运算符和后置递增运算符,我们可以在自定义类中实现递增操作,使得对象可以像内置的数据类型一样使用递增运算符。


1. 前置递增运算符重载(++x):

       前置递增运算符可以通过重载函数 operator++() 来实现。该重载函数没有参数,返回类型为引用类型,通常是类本身的引用。它负责将对象的值增加1,并返回递增后的对象的引用。


class MyClass {
public:
    // 前置递增运算符重载
    MyClass& operator++() {
        // 在这里完成对象值的递增操作
        // 并返回递增后的对象的引用
        // 注意:这里可以是任何递增逻辑
        return *this;
    }
    // ...
};

       函数体内的递增逻辑可以根据类的需求和语义进行自定义,例如递增成员变量的值、修改对象属性等。


2. 后置递增运算符重载(x++):

       后置递增运算符可以通过重载函数 operator++(int) 来实现。该重载函数的参数为 int 类型,但实际上在调用时并不传递任何实参,只是用于与前置递增运算符做区分。返回类型为类本身,通常是一个临时对象的拷贝。


class MyClass {
public:
    // 后置递增运算符重载
    MyClass operator++(int) {
        // 在这里完成对象值的递增操作
        // 创建一个临时对象来保存递增前的值
        // 并返回递增前的临时对象
        // 注意:这里可以是任何递增逻辑
        MyClass temp = *this;
        // 对象值的递增操作
        // ...
        return temp;
    }
    // ...
};


       后置递增运算符重载返回一个临时对象的拷贝,通常是递增前的对象值。递增操作可以根据需要进行自定义。


目录
相关文章
|
1月前
|
存储 安全 编译器
第二问:C++中const用法详解
`const` 是 C++ 中用于定义常量的关键字,主要作用是防止值被修改。它可以修饰变量、指针、函数参数、返回值、类成员等,确保数据的不可变性。`const` 的常见用法包括:
108 0
|
3月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
3月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
127 6
|
3月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
53 0
|
3月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
594 1
|
3月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
79 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
11天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
51 18
|
11天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
37 13
|
11天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
37 5
|
11天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
27 5