【问题标题】:assignment operator vs. copy constructor C++赋值运算符与复制构造函数 C++
【发布时间】:2013-09-28 22:18:51
【问题描述】:

我有以下代码来测试我对 C++ 中基本指针的理解:

// Integer.cpp
#include "Integer.h"
Integer::Integer()
{
  value = new int;
  *value = 0;
}

Integer::Integer( int intVal )
{
  value = new int;
  *value = intVal;
} 

Integer::~Integer()
{
  delete value;
}

Integer::Integer(const Integer &rhInt)
{
  value = new int;
  *value = *rhInt.value;
}

int Integer::getInteger() const
{
  return *value;
}

void Integer::setInteger( int newInteger )
{
  *value = newInteger;
}

Integer& Integer::operator=( const Integer& rhInt )
{   
  *value = *rhInt.value;
  return *this;
}

// IntegerTest.cpp
#include <iostream>
#include <cstdlib>
#include "Integer.h"

using namespace std;

void displayInteger( char* str, Integer intObj )
{
  cout << str << " is " << intObj.getInteger() << endl;
}

int main( int argc, char* argv[] )
{
 Integer intVal1;
 Integer intVal2(10);

 displayInteger( "intVal1", intVal1 );
 displayInteger( "intVal2", intVal2 );

 intVal1 = intVal2;

 displayInteger( "intVal1", intVal1 );

 return EXIT_SUCCESS;
}

这段代码完全按照预期工作,它会打印出来:

intVal1 is 0

intVal2 is 10

intVal1 is 10

但是,如果我删除复制构造函数,它会打印出如下内容:

intVal1 is 0

intVal2 is 10

intVal1 is 6705152

我不明白为什么会这样。我的理解是,当分配给不存在的对象时使用复制构造函数。这里intVal1确实存在,为什么不调用赋值运算符呢?

【问题讨论】:

  • 对不起,我还没有阅读全文,所以不回答,而是评论。我相信观察到的现象与默认(自动生成的)复制构造函数不分配内存,而只是复制指针(是的,考虑到我还没有读过,我可能离题了;))这一事实有关。
  • intVal1 = intVal2;intVal1 上调用operator =,而不是复制构造函数。

标签: c++ pointers memory-management copy-constructor


【解决方案1】:

在赋值期间不使用复制构造函数。在将参数传递给 displayInteger 函数时,使用您的情况下的复制构造函数。第二个参数是传值,意思是复制构造函数初始化的。

您的复制构造函数版本对类拥有的数据执行深度复制(就像您的赋值运算符一样)。所以,一切都适用于您的复制构造函数版本。

如果您删除自己的复制构造函数,编译器会为您隐式生成一个。编译器生成的复制构造函数将执行对象的 shallow 复制。这将违反"Rule of Three" 并破坏您的类的功能,这正是您在实验中观察到的。基本上,第一次调用displayInteger 会损坏您的intVal1 对象,第二次调用displayInteger 会损坏您的intVal2 对象。之后你的两个对象都坏了,这就是第三个displayInteger 调用显示垃圾的原因。

如果将displayInteger的声明改为

void displayInteger( char* str, const Integer &intObj )

即使没有显式的复制构造函数,您的代码也会“工作”。但无论如何,忽略"Rule of Three" 并不是一个好主意。以这种方式实现的类要么必须遵守“三法则”,要么必须使其不可复制。

【讨论】:

  • 好的。我想我明白你在说什么。但是,如果我调试我的代码(没有我的复制构造函数)并在intVal1 = intVal2 行中断,并询问 intVal1 和 intVal2 的值,我分别得到 0 和 6299648。所以似乎只有 intVal2 受到了浅拷贝的影响。这是为什么呢?
  • @H H:这只是巧合或错觉。两个物体都受到了相同程度的损坏。只是巧合的是,以前属于intVal1 的内存保留了它以前的值。如果没有复制构造函数,您的程序会表现出未定义的行为。未定义的行为可以采取任何形式。甚至看起来一切都很好。
  • 好吧,我可以相信。我唯一的问题是 intVal = 0 和 intVal2 = 其他东西似乎是可重现的(每次运行都会改变)。当然,我只运行了 3 次。
  • 五法则:当使用 C++11 时(现在越来越流行),三法则被“五法则”取代。这意味着除了复制构造函数、析构函数、(复制)赋值运算符之外,现在还应该定义移动构造函数和移动赋值运算符。
【解决方案2】:

您遇到的问题是由默认的复制构造函数引起的,该构造函数复制指针但不将其与新分配的内存相关联(就像您的复制构造函数的实现一样)。当您按值传递对象时,将创建一个副本,并且当执行超出范围时,该副本将被破坏。来自析构函数的delete 使intVal1 对象的value 指针无效,使其成为悬空指针,取消引用会导致未定义的行为 .

调试输出可用于了解代码的行为:

class Integer {
public:

    Integer() {
      cout << "ctor" << endl;
      value = new int;
      *value = 0;
    }

    ~Integer() {
        cout << "destructor" << endl;
        delete value;
    }

    Integer(int intVal) {
      cout << "ctor(int)" << endl;
      value = new int;
      *value = intVal;
    } 

    Integer(const Integer &rhInt) {
      cout << "copy ctor" << endl;
      value = new int;
      *value = *rhInt.value;
    }

    Integer& operator=(const Integer& rhInt){   
      cout << "assignment" << endl;
      *value = *rhInt.value;
      return *this;
    }

    int *value;
};

void foo(Integer intObj) {
    cout << intObj.value << " " << *(intObj.value) << endl;
}

现在这段代码的输出:

Integer intVal1;
Integer intVal2(10);

foo( intVal1 );
foo( intVal2 );

intVal1 = intVal2;

foo( intVal1 );

是:

演员
ctor(int)
复制 ctor
0x9ed4028 0
析构函数
复制 ctor
0x9ed4038 10
析构函数
作业
复制 ctor
0x9ed4048 10
析构函数
析构函数
析构函数

这表明在按值传递对象时使用了复制构造函数。但是,这里需要注意的重要一点是函数返回时调用的析构函数。如果您删除复制构造函数的实现,则输出为:

演员
ctor(int)
0x8134008 0
析构函数
0x8134018 10
析构函数
分配
0x8134008 135479296
析构函数
析构函数
析构函数

显示第一个副本调用delete 的指针(指向0x8134008)与稍后的第三个副本使用的相同,其中该悬空指针指向的内存已被使用。

【讨论】:

    【解决方案3】:

    想想这个电话:

    displayInteger( "intVal1", intVal1 );
    

    您正在将intVal1 的副本创建到displayIntegerintObj 参数中:

    void displayInteger( char* str, Integer intObj )
    {
      cout << str << " is " << intObj.getInteger() << endl;
    }
    

    该副本将指向与intVal1 相同的int。当displayInteger返回时,intObj被销毁,这将导致int被销毁,intVal1中的指针指向一个无效对象。到那时,如果您尝试访问该值,所有赌注都将关闭(A.K.A. 未定义行为)。 intVal2 也会发生类似的事情。

    在更一般的层面上,通过删除复制构造函数,您违反了三法则,这通常会导致这类问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-01
      • 2011-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多