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语言Easyx实现贪吃蛇详解

    C语言Easyx实现贪吃蛇详解

    这篇文章主要为大家详细介绍了基于easyx的C++实现贪吃蛇,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-10-10
  • stringstream操纵string的方法总结

    stringstream操纵string的方法总结

    下面小编就为大家带来一篇stringstream操纵string的方法总结。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12
  • 利用c++和easyx图形库做一个低配版扫雷游戏

    利用c++和easyx图形库做一个低配版扫雷游戏

    这篇文章主要介绍了用c++和easyx图形库做一个低配版扫雷游戏,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01
  • 如何用C++实现A*寻路算法

    如何用C++实现A*寻路算法

    寻路是游戏比较重要的一个组成部分。因为不仅AI还有很多地方(例如RTS游戏里操控人物点到地图某个点,然后人物自动寻路走过去)都需要用到自动寻路的功能。本文将介绍一个经常被使用且效率理想的寻路方法-A*寻路算法,并且提供额外的优化思路
    2021-06-06
  • C++20 格式化字符串的实现

    C++20 格式化字符串的实现

    本文主要讲述了C++20中新引入的std::format功能,该功能用于格式化字符串,提供了一种简洁、类型安全且灵活的方式来构建格式化字符串,文章从使用场景、格式化规则、自定义类型的格式化等方面进行了详细的介绍,感兴趣的可以了解一下
    2024-10-10
  • C++解决输出链表中倒数k个结点的问题

    C++解决输出链表中倒数k个结点的问题

    这篇文章主要给大家介绍了关于如何利用C++解决输出链表中倒数k个结点的问题,文中通过实例代码介绍的非常详细,对大家学习或者使用C++具有一定的参考学习价值,需要的朋友可以参考下
    2021-12-12
  • C语言实现交换排序算法(冒泡,快速排序)的示例代码

    C语言实现交换排序算法(冒泡,快速排序)的示例代码

    这篇文章主要为大家详细介绍了如何利用C语言实现交换排序算法(冒泡排序、快速排序),文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2022-07-07
  • 详解C++中常量的类型与定义

    详解C++中常量的类型与定义

    这篇文章主要介绍了详解C++中常量的类型与定义,使用#define与const来定义常量是C++入门学习中的基础知识,需要的朋友可以参考下
    2016-05-05
  • C语言中基础小问题详细介绍

    C语言中基础小问题详细介绍

    这篇文章详细介绍了C语言中基础小问题,有需要的朋友可以参考一下
    2013-10-10
  • C语言静态与动态通讯录的实现流程详解

    C语言静态与动态通讯录的实现流程详解

    这篇文章主要为大家介绍了C语言分别实现静态与动态的通讯录示例代码教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2021-11-11

最新评论