C++深度探索运算符重载和返回值优化

 更新时间:2022年04月24日 11:55:01   作者:小裘HUST  
这篇文章主要介绍了C++运算符重载及编译器返回值优化,C++当中除了函数可以重载之外,其实运算符也是可以重载的,下面一起来详细了解吧

  今天遇到的是内存释放错误的问题。原因是没写拷贝构造函数,奇怪的是我之前也没写确实能正常工作的,今天深究了一下发现是编译器做了返回值优化。

问题背景

  编译环境还是针对C6455 DSP,为了做一些简单的图像直方图的处理,并且尽可能不用模板类,我自己写了一个简单的类用来存放带长度信息的数组,并且可以做一些简单的运算。重载了减法运算符,从而可以对两个直方图求差。

具体问题

  当类中有那种需要动态分配空间的成员的时候,要记得提醒自己重载拷贝构造函数和赋值运算符,这两者缺一不可。为了举个例子,我定义了下面这个CData类。

class CData
{
private:
    int *m_pData;
    unsigned int m_nLen;
public:
    CData();
    CData(unsigned int nLen);
    /* There is no copy constructor at the very begining */
    CData(const CData &data);
    ~CData();
    int &at(unsigned int i);
    int &at(unsigned int i) const;
    CData operator-(const CData &data);
    CData &operator=(const CData &data);
    CData &operator-=(const CData &data);
};

  在CData中,有两个成员,分别是一个int类型的指针和一个unsigned int类型的变量代表数据的个数。

  我重载了一个减法运算符,为的是将两个CData类型的数据长度相同的对象中的每个对应位置的int类型数据直接相减。而在调用减法运算的时候势必会用到拷贝构造函数和赋值运算符,因此也需要对它们重载。

  顺带一提的是,在上面我还声明了一个“-=”运算符的重载,为的是突出“-=”在一定场合会更加高效,但因为我在实际应用中不能直接覆盖“被减数”中的数据,所以不能用“-=”运算符,只能通过减法运算符来实现。

CData CData::operator-(const CData &data)
{
    cout << "entering subtract" << endl;
    assert(m_nLen == data.m_nLen);
    CData ret(data.m_nLen);
    for (int i = 0; i < m_nLen; i++){
        ret.at(i) = at(i) - data.at(i);
    }
    return ret;
}

  减法运算重载,返回值是一个类的对象。函数返回后,这个类的对象就已经被析构了,所以在它被析构之前一般会先调用一个拷贝构造函数,将这个类的对象拷贝给一个新的临时的对象。默认拷贝构造函数采用浅拷贝,这在遇到类中包含指针类型的成员时很可能造成对同一块内存区域的重复释放。所以我们需要重载拷贝构造函数,为指针成员申请新的内存空间。

CData CData::&operator=(const CData &data)
{
    cout << "entering assign" << endl;
    assert(m_nLen == data.m_nLen);
    for (int i = 0; i < m_nLen; i++)
        at(i) = data.at(i);
    return *this;
}

  减法运算返回的临时对象如果不给新的对象接收,它就没了。所以我们还需要重载赋值运算符,用来接收这个临时对象中的数据,这一临时对象被接收后,也会被析构。分析整个过程可以看到,从减法运算返回的类对象,其中的数据先要被复制到一个临时对象里,再要从临时对象复制到真正的赋值目标对象里,浪费了很多时间。

  有的编译器就会对这样的情况进行优化,省略对拷贝构造函数的调用,直接赋值。

  我今天遇到的问题是因为,我之前不知道在这个过程中会调用拷贝构造函数,所以没有重写拷贝构造函数。意外的是,之前我这么做也是能用的,但我现在觉得不管怎样还是得把拷贝构造函数写上,不能偷懒。

测试代码

  为了便于验证这一过程,我写了一部分测试代码,可以用g++编译。

#include <assert.h>
#include <iostream>
using namespace std;
class CData
{
private:
    int *m_pData;
    unsigned int m_nLen;
public:
    CData(): m_nLen(0), m_pData(nullptr)
    {
        cout << "construct without arg" << endl;
    }
    CData(unsigned int nLen): m_nLen(nLen)
    {
        cout << "construct with arg" << endl;
        m_pData = new int[nLen];
        assert(m_pData != NULL);
    }
    CData(const CData &data)
    {
        cout << "copy construct" << endl;
        m_nLen = data.m_nLen;
        m_pData = new int[data.m_nLen];
        assert(m_pData != nullptr);
        for (int i = 0; i < m_nLen; i++){
            at(i) = data.at(i);
        }
    }
    ~CData()
    {
        cout << "destruct" << endl;
        if(m_pData){
            delete[] m_pData;
            m_pData = nullptr;
        }
    }
    int &at(unsigned int i)
    {
        assert(i < m_nLen);
        return *(m_pData + i);
    }
    int &at(unsigned int i) const
    {
        assert(i < m_nLen);
        return *(m_pData + i);
    }
    CData operator-(const CData &data)
    {
        cout << "entering subtract" << endl;
        assert(m_nLen == data.m_nLen);
        CData ret(data.m_nLen);
        for (int i = 0; i < m_nLen; i++){
            ret.at(i) = at(i) - data.at(i);
        }
        return ret;
    }
    CData &operator=(const CData &data)
    {
        cout << "entering assign" << endl;
        assert(m_nLen == data.m_nLen);
        for (int i = 0; i < m_nLen; i++)
            at(i) = data.at(i);
        return *this;
    }
    CData &operator-=(const CData &data)
    {
        cout << "entering subtract and assign" << endl;
        assert(m_nLen == data.m_nLen);
        for (int i = 0; i < m_nLen; i++)
            at(i) -= data.at(i);
        return *this;
    }
};
int main()
{
    cout << "---------- start construct ----------" << endl;
    CData dataA(10);
    CData dataB(10);
    CData dataC(10);
    cout << "---------- start subtract ----------" << endl;
    dataC = dataA - dataB;
    cout << "---------- start subtract and assign ----------" << endl;
    dataA -= dataB;
    cout << "---------- end of program ----------" << endl;
    return 0;
}

  g++编译的程序,返回类对象的时候,默认是会省略拷贝构造函数的,加上“-fno-elide-constructors”的编译选项后可以让它不省略。

  上图是在加上了“-fno-elide-constructors”编译选项后的运行结果,可以看到执行了两次析构,对应减法重载函数内的对象和拷贝构造函数生成的临时对象被析构。

  而没有“-fno-elide-constructors”选项的时候,就只会有一次析构,因为没有调用拷贝构造函数。

  如果加了这个选项,但是没有重写拷贝构造函数,就会在第二次析构的时候产生重复释放内存的问题。

到此这篇关于C++深度探索运算符重载和返回值优化的文章就介绍到这了,更多相关C++运算符重载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++的类型转换(强转)你了解吗

    C++的类型转换(强转)你了解吗

    这篇文章主要为大家详细介绍了C++的类型转换,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-02-02
  • C++ TensorflowLite模型验证的过程详解

    C++ TensorflowLite模型验证的过程详解

    这篇文章给大家介绍了C++ TensorflowLite模型验证的过程,测试代码,主要是RunInference()和read_file(),详细操作过程跟随小编一起看看吧
    2021-08-08
  • C++ const的各种用法详解

    C++ const的各种用法详解

    const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。本文主要谈谈const的用法,感兴趣的同学可以参考阅读
    2023-04-04
  • C语言 枚举类型(Enum)详解及示例代码

    C语言 枚举类型(Enum)详解及示例代码

    本文主要介绍C语言 枚举类型,这里提供了详细的相关资料及示例代码,以便大家学习参考,有兴趣的小伙伴可以参考下
    2016-08-08
  • 详解c++实现信号槽

    详解c++实现信号槽

    这篇文章主要为大家介绍了c++实现信号槽,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2021-12-12
  • C++日期类的实现日期计算器举例详解

    C++日期类的实现日期计算器举例详解

    这篇文章主要给大家介绍了关于C++日期类实现日期计算器的相关资料,我们要考虑日期的增加和减少,自增和自减,以及两个日期类的比较,以及当前日期类的日期显示和用户的输入输出,需要的朋友可以参考下
    2024-05-05
  • C++服务器和客户端交互的项目实践

    C++服务器和客户端交互的项目实践

    本文主要介绍了C++服务器和客户端交互的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • C语言实现输出1000以内的所有完全数

    C语言实现输出1000以内的所有完全数

    这篇文章主要介绍了C语言实现输出1000以内的所有完全数,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • C语言 makefile学习及实现实例

    C语言 makefile学习及实现实例

    这篇文章主要介绍了C语言 makefile学习及实现实例的相关资料,需要的朋友可以参考下
    2017-03-03
  • Opencv实现最小外接矩形和圆

    Opencv实现最小外接矩形和圆

    这篇文章主要为大家详细介绍了Opencv实现最小外接矩形和圆,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05

最新评论