自学内容网 自学内容网

C++之《剑指offer》学习记录(4):赋值运算符函数

笔者最近在找工作时,无意间读到了一本名为《剑指offer》的书,粗略翻阅了一下,感觉这将会是一本能让我不再苦恼于笔试和面试“手搓代码”的书。故笔者写下该系列博客记录自己的学习历程,希望能和这本书的读者朋友们一起交流学习心得。
介绍:《剑指Offer:名企面试官精讲典型编程题(第2版)》剖析了80个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这5个面试要点。
编程题链接:牛客网在线编程_算法面试_面试必刷TOP101 (nowcoder.com)
本博客关键词:赋值运算符函数

由于笔者在读这本书之前没有听说过“赋值运算符函数”,所以第一次看有点没搞懂这个函数是什么意思,后来查阅了资料配合理解代码,才知道原来“赋值运算符函数”其实就是类中重载=运算符的函数。该函数的写法:ClassName& operator=(const ClassName& other);

题设

有如下类的声明,请为该类添加赋值运算符函数。(相比于书中的声明,这里我做了一些小修改。)

class MyClass
{
private:
    char *m_pData;

public:
    MyClass(const char *pData = nullptr); // 构造函数
    MyClass(const MyClass &other); // 拷贝构造函数
    ~MyClass(); // 析构函数
};

常规写法

如果我们只是写算法题,关注该函数本身即可,如果是在整个测试用例中写该函数,需要在类中声明,如下:

class MyClass
{
private:
    char *m_pData;

public:
    MyClass(const char *pData = nullptr);
    MyClass(const MyClass &other);
    ~MyClass();
    MyClass &operator=(const MyClass &other); // 赋值运算符函数
};

赋值运算符函数常规(初级)写法

MyClass &MyClass::operator=(const MyClass &other)
{
    if (this == &other)
    {
        return *this;
    }

    delete[] m_pData;
    m_pData = nullptr;

    m_pData = new char[strlen(other.m_pData) + 1];
    strcpy(m_pData, other.m_pData);
    return *this;
}
  1. 输入参数为什么是常量引用?首先使用const声明对象只读,保证了在赋值运算的过程中函数不会对原对象进行修改,保证了安全性;其次使用&表示输入为引用类型,避免了按值传参时对拷贝构造函数的调用,减少了调用开销,可以提高代码效率。
  2. 为什么函数返回类型是引用?函数返回引用,才能保证连续赋值(如a=b=c),如果返回类型是void,则不能实现连续赋值的功能。在类中this表示指向指向当前对象的指针,所以*this得到的就是当前对象的实例。
  3. 函数开头对赋值对象进行判断,如果是自赋值this==&other),则直接返回。
  4. 如果不是自赋值,则在分配新内存之前delete释放实例自身已有的内存避免出现内存泄漏

高级写法

相较于常规写法,高级写法实现了赋值运算符函数中的异常安全性。代码如下:

MyClass &MyClass::operator=(const MyClass &other)
{
    if (this != &other)
    {
        MyClass Temp(other);  // 调用拷贝构造函数

        char *tempPtr = Temp.m_pData;
        Temp.m_pData = m_pData;
        m_pData = tempPtr;
    }
    return *this;
}
  1. 高级写法与常规写法相同的地方是都会对自赋值情况进行判断
  2. 高级写法的主要高级之处在于,该写法通过创建临时实例,并通过一个临时变量交换临时实例与当前实例的目标值(m_pData),从而实现对当前实例进行赋值的同时还释放了实例自身原有的内存,避免了内存泄漏。在这个写法中,如果因为内存不足抛出了异常,但代码中并没有修改原来实例的状态,因此该实例的状态仍然是有效的。
  3. 该段代码在if判断语句里调用了拷贝构造函数,创建了一个临时实例,通过中间变量tempPtr让临时实例指向当前实例的m_pData,让当前实例指向临时实例的m_pData,由于临时实例来自于拷贝构造,所以此时临时实例的m_pData就是当前实例的目标m_pData。
  4. 由于该拷贝构造函数是在if判读语句里调用的,其生命周期就在这里if语句里,当if语句退出时,临时实例会自动调用析构函数释放内存,这是析构函数释放的内存实际上就是当前实例的m_pData指向的内存!

测试用例

#include <iostream>
#include <cstring>

using namespace std;

class MyClass
{
private:
    char *m_pData;

public:
    MyClass(const char *pData = nullptr);
    MyClass(const MyClass &other);
    ~MyClass();
    MyClass &operator=(const MyClass &other);

    void getData()
    {
        cout << m_pData << endl;
    }
};

MyClass::MyClass(const char *pData)
{
    m_pData = new char[strlen(pData) + 1];
    strcpy(m_pData, pData);
    cout << "构造函数被调用" << endl;
}

MyClass::MyClass(const MyClass &other)
{
    // 深拷贝
    m_pData = new char[strlen(other.m_pData) + 1];
    strcpy(m_pData, other.m_pData);
    cout << "拷贝构造函数被调用" << endl;
}

// 普通写法
// MyClass &MyClass::operator=(const MyClass &other)
// {
//     if (this == &other)
//     {
//         cout << "赋值运算符函数被调用" << endl;
//         return *this;
//     }

//     delete[] m_pData;
//     m_pData = nullptr;

//     m_pData = new char[strlen(other.m_pData) + 1];
//     strcpy(m_pData, other.m_pData);
//     cout << "赋值运算符函数被调用" << endl;
//     return *this;
// }

// 高级写法,实现了异常安全性
MyClass &MyClass::operator=(const MyClass &other)
{
    if (this != &other)
    {
        MyClass Temp(other);

        char *tempPtr = Temp.m_pData;
        Temp.m_pData = m_pData;
        m_pData = tempPtr;
    }
    return *this;
}

MyClass::~MyClass()
{
    delete[] m_pData;
    cout << "析构函数被调用" << endl;
}

int main(int argc, char const *argv[])
{
    MyClass a("Tony");
    MyClass b("Amy");
    MyClass c(a);
    a.getData();
    b.getData();
    c.getData();
    c = c = b = a;
    a.getData();
    b.getData();
    c.getData();

    return 0;
}

原文地址:https://blog.csdn.net/S13352784013/article/details/142977301

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!