【问题标题】:What does compiler do when returning an object返回对象时编译器会做什么
【发布时间】:2014-07-25 11:41:12
【问题描述】:

我有以下名为 DATA 的类。

enum DATATYPE{DATATYPE_CONSTANT=0, DATATYPE_NUMBER,DATATYPE_STRING, DATATYPE_MATRIX, DATATYPE_OBJECT};

struct DATA //A Data container for the variable
{
    DATA(DATATYPE type,int row=0,int col=0)
    {
        m_str=0;m_number=0;

        m_DataType=type;

        if(type==DATATYPE_NUMBER) m_number=new double;
        if(type==DATATYPE_STRING) m_str=new string("");

        cout<<"In constructor"<<endl;
        //if(type==DATATYPE_MATRIX) m_matrix= new MatrixXd(row,col);
    }
    ~DATA()
    {
        if(m_str) m_str->clear();
        if(m_number) {delete m_number; m_number=0;}

        std::cout<<"In Destructor"<<std::endl;
        //if(m_matrix) {delete m_matrix; m_matrix=0;}
    }

    DATA(const DATA& other)
    {
        m_number=other.m_number;
        m_str=other.m_str;
        m_DataType=other.m_DataType;

        cout<<"In copy constructor"<<endl;
    }

    DATA& operator=(const DATA& other)
    {
        m_number=other.m_number;
        m_str=other.m_str;
        m_DataType=other.m_DataType;

        cout<<"In operator="<<endl;

        return *this;
    }

    DATATYPE GetType()
    {
        return m_DataType;
    }

    double* GetNumber()
    {
        return m_number;
    }

    void SetNumber(const double& val){*m_number=val;}


    string* GetString()
    {
        return m_str;
    }

private:
    DATATYPE m_DataType;
    string* m_str;
     //MatrixXd* m_matrix;
    double* m_number;
};

我有以下测试:

DATA GetData();

int main()
{
    cout<<"Before GetData call"<<endl;
    DATA dat=GetData();
    //DATA dat2=dat;
    cout<<*(dat.GetNumber())<<endl;
    cout<<"After Get Data call"<<endl;

    cout << "Exiting main" << endl;
    return 0;
}


DATA GetData()
{
    cout<<"In Get Data"<<endl;
    DATA ret(DATATYPE_NUMBER);
    double d=5;
    ret.SetNumber(d);
    cout<<"Exiting GetData"<<endl;
    return ret;
}

运行测试后输出为:

GetData 调用之前

在获取数据中

在构造函数中

正在退出 GetData

5

Get Data 调用后

退出主程序

在析构函数中

我有以下问题:

  1. 当我调用DATA dat=GetData(); 时,它既不调用构造函数、复制构造函数也不调用相等运算符。 dat 对象是如何构造的。从GetData返回时编译器究竟做了什么?

  2. 对于DATA 结构或一般的聚合数据类型,使用new 初始化成员变量总是一个好主意吗?当我初始化说 DATA *d=new DATA(DATATYPE_NUMBER) 时,成员变量会发生什么?内存泄漏是否更容易出错?

【问题讨论】:

  • #1。 #2:不,使用常规变量,而不是指针。您对类所做的任何副本现在都会导致内存泄漏(实际上,您首先制作的任何对象也会导致泄漏)。
  • 3.尝试切换到 C++11 模式并使用范围枚举。此外,更少的大写。
  • 也学习使用命名空间。 using namespace std; 是一个交易破坏者。
  • 整个代码实际上是在一个命名空间中;但是,为了方便起见,我更喜欢从命名空间中“提取”它。

标签: c++


【解决方案1】:

问题 1

当我调用DATA dat=GetData(); 时,它既不调用构造函数、复制构造函数也不调用相等运算符。 dat 对象是如何构造的。从GetData返回时编译器究竟做了什么?

答案这是一种称为返回值优化 (RVO) 的结果。你可以阅读更多关于他们的信息here

在 g++ 中,您可以使用标志 -fno-elide-constructors 禁用 RVO。如果您对代码执行此操作,您将看到来自复制构造函数的消息。

问题 2

对于DATA 结构,或者一般的聚合数据类型,用new 初始化成员变量总是一个好主意吗?当我初始化说 DATA *d=new DATA(DATATYPE_NUMBER) 时,成员变量会发生什么?内存泄漏是否更容易出错?

回答

里面有三个问题。

答案 2.1

该问题的答案是“取决于您的应用程序”。对于一些人来说,将对象作为成员数据是有意义的,而对于另一些人来说,拥有指向对象的指针是有意义的。当你使用指向对象的指针时,你必须遵循Rule of Three,它已经变成了Rule of Five in C++11

答案 2.2

成员变量的初始化与您使用时一样:

Data d = DATA(DATATYPE_NUMBER);

答案 2.3

使用动态内存有好处,但也有缺点。每当您使用动态内存分配时,您都会输入更容易出错的代码。您必须担心它潜在的​​不良副作用:

  1. 悬空指针。
  2. 指针丢失。
  3. 访问超出分配的内存。

【讨论】:

  • 谢谢!我会尝试 [-fno-elide-constructors]。我之前没有听过 David Rodriguez 建议的“复制省略”,我也会看看。
【解决方案2】:

当我调用 DATA dat=GetData();它既不调用构造函数、复制构造函数也不调用相等运算符。 dat 对象是如何构造的。从 GetData 返回时编译器究竟做了什么?

等号运算符:即使看起来像是在赋值,但事实并非如此。语法T t = u; 不包含任何赋值,而是复制构造。

constructor:在函数内部调用构造函数,然后返回一个copy,然后copycopy /em> 覆盖目标对象。除非它不是。该语言允许copy-elision,这意味着允许编译器通过将三个对象(函数内的ret、返回的对象和main 内的dat)放在同一个中来删除副本内存位置。

我不确定确切的细节有多少重要或有多少帮助,但在大多数 ABI 中(据我所知),一个按值返回的函数被编译器转换为一个函数,该函数接受一个指向所在位置的指针该对象将存活。

T f(int x) {
   T tmp(x);
   return tmp;
}
int main() {
   T t = f(1);
}

转化为:

void f(void *__ret, int x) {
   new (__ret) T(x); // constructor call
   return;                               // return does not *copy*
}
int main() {
   [[uninitialized]] T t;                // space is reserved, no construction
   f(&t, 1);
}

这就是复制省略的作用方式,f 在返回值(参数)之上创建了tmp 对象,main 中的调用者放置了t 和@987654332 的返回值@ 在同一个内存位置上。

对于 DATA 结构,或者一般的聚合数据类型,使用 new 初始化成员变量总是一个好主意吗?当我初始化说 DATA *d=new DATA(DATATYPE_NUMBER) 时,成员变量会发生什么?内存泄漏是否更容易出错?

你告诉我。但在您回答之前,请注意您的 DATA 类型存在内存泄漏。

【讨论】:

  • 我可以看到内存泄漏是由于没有删除字符串m_str。您可以建议阅读有关复制省略的好资源吗?
  • @macroland:我过去写过两篇介绍性文章,herehere。它们是高级别的,不会详细介绍不同的 ABI 和调用约定。从高级的角度来看,语言,你只需要知道它发生,编译器如何实现它实际上是一个实现细节,尽管这个答案中的转换是它的胆量。
  • 罗德里格斯:感谢您的链接。我已经用 CodeBlocks 编译了上面的代码,大概它默认使用-fno-elide-constructorsDATA 的相同代码在使用 Eclipse CDT 编译时表现得非常不同,当从完全改变程序行为的函数返回时,我可以跟踪这些步骤,即复制构造函数的使用。我开始为m_number 获得像 E-208 这样的值。捕捉错误非常棘手,但我只是用m_number=0 初始化而不是new double
【解决方案3】:

使您的构造函数显式并删除 DATA(enum,int,int) 上的默认参数。当 GetData() 使用单个参数构造对象时,您很可能会得到编译器生成的默认构造函数。

您可以通过将 m_str 和 m_number 放在一个联合中而不是单独的成员变量中来节省内存:

DATA {
...

union U {
   string* m_string;
   double* m_number;
}
    DATATYPE m_DataType;
    U m_data;
}

当成员很大或可以为性能延迟构造时,使用成员指针既很好也很常见。但是您必须格外小心以避免内存泄漏。 OTOH,如果成员很小,则常规成员变量的便利性通常是最好的。

【讨论】:

  • 代替联合,boost::variant 会是更好的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-01
相关资源
最近更新 更多