C++开发初级


1087 浏览 5 years, 3 months

4.3 处理复制以及赋值

版权声明: 转载请注明出处 http://www.codingsoho.com/

处理复制以及赋值

回顾前面,如果没有编写复制构造函数或者赋值运算符,C++将自动生成。编译器生成的方法递归调用对象数据成员的复制构造函数或者赋值运算符。然而对于原始类型,例如int、double以及指针,只是提供表层(或者按位)复制或者赋值:只是将数据成员从源对象直接复制或者赋值到目标对象。当为对象动态分配内存时,这样做会引发问题。例如,在下面的代码中,当s1传递给函数printSpreadsheet()时,复制了电子表格s1以初始化s:

#include "Spreadsheet.h"

void printSpreadsheet(Spreadsheet s)
{
  // code omitted for brevity
}

int main()
{
  Spreadsheet s1(4, 3);
  printSpreadsheet(s1);

//  Spreadsheet s1(2, 2), s2(4, 3);
//  s1 = s2;

  return 0;
}

代码取自 Spreadsheet\SpreadsheetTest.cpp

Spreadsheet包含了一个指针变量:mCells。spreadsheet的表层复制向目标对象提供了一个mCells指针的副本,但是没有复制底层的数据。最终结果是s以及s1都有一个指向同一数据的指针,如下图所示。

如果s修改了mCells所指的内容,这一改动也会在s1中表现出来。更糟糕的是,当函数print-Spreadsheet()退出的时候,会调用s的析构函数,从而释放mCells所指的内存。下图显示了这一状况。

现在s1拥有的指针所指的内存不再有效,这就是所谓的悬挂指针(dangling pointer)。令人难以置信的是,当使用赋值的时候情况会变得更糟,假定您编写了下面的代码:

Spreadsheet s1(2,4), s2(4,3);
s1=s2;

当两个对象都构建之后,内存的布局如下图所示。

当执行赋值语句后,内存布局如下图所示。

现在,不仅s1以及s2中的mCells都指向同一内存,而且s1前面所指的内存被遗弃。这就是在赋值运算符中首先释放左边引用的内存然后进行深层复制的原因。

您已经看到,依赖C++默认的复制构造函数或者赋值运算符未必是个好主意。

提示:无论什么时候在类中动态分配了内存,都应该编写自己的复制构造函数以及赋值运算符,以提供深层的内存复制。

Spreadsheet的复制构造函数

下面是Spreadsheet类中复制构造函数的声明:

class Spreadsheet
{
 public:
  Spreadsheet(const Spreadsheet& src);
};

下面是复制构造函数最初的定义:

Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
  mWidth = src.mWidth;
  mHeight = src.mHeight;

  mCells = new SpreadsheetCell* [mWidth];
  for (int i = 0; i < mWidth; i++) {
    mCells[i] = new SpreadsheetCell[mHeight];
  }

  for (int i = 0; i < mWidth; i++) {
    for (int j = 0; j < mHeight; j++) {
      mCells[i][j] = src.mCells[i][j];
    }
  }
}

注意复制构造函数复制了所有的数据成员,包括mWidth以及mHeight,而不仅是指针数据成员。复制构造函数中其余的代码对mCells动态分配的二维数组进行了深层复制。在此不需要删除己有的mCells,因为这是一个复制构造函数,因此在this对象中还没有mCells。

Spreadsheet的赋值运算符

下面是包含赋值运算符的Spreadsheet类的定义:

class Spreadsheet
{
 public:
  Spreadsheet& operator=(const Spreadsheet& rhs);
};

下面是Spreadsheet类中赋值运算符的实现以及一些解释。注意当对象赋值的时候已经被初始化过了,因此,必须在分配新的内存之前释放已分配的动态内存。可将赋值运算符视为析构函数以及复制构造函数的结合体。当给对象赋值的时候,基本上是给予对象新的生命(或者数据)。

在任何赋值运算符中,第一行都应该是自赋值检测:

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
  // check for self-assignment
  if (this == &rhs) {
    return *this;
  }
}

自赋值检测不仅是为了效率,而且是为了正确性。如果删除了前面的自赋值检测,自赋值的时候代码很可能会崩溃,因为代码的第二步删除了左边对象的mCells,然后将右边的mCells复制到左边。如果是自赋值,两边相同,因此复制的时候会访问悬挂指针。
由于这是一个赋值运算符,被赋值对象的mCells已经被初始化过,因此要将这些单元格释放。

  // free the old memory
  for (int i = 0; i < mWidth; i++) {
    delete [] mCells[i];
  }
  delete [] mCells;
  mCells = nullptr;

这段代码与析构函数相同。您必须在重新分配之前释放所有内存,否则就会造成内存泄漏。下面的步骤复制了内存:

  // copy the new memory
  mWidth = rhs.mWidth;
  mHeight = rhs.mHeight;

  mCells = new SpreadsheetCell* [mWidth];
  for (int i = 0; i < mWidth; i++) {
    mCells[i] = new SpreadsheetCell[mHeight];
  }

  for (int i = 0; i < mWidth; i++) {
    for (int j = 0; j < mHeight; j++) {
      mCells[i][j] = rhs.mCells[i][j];
    }
  }
  return *this;
}

完整代码如下

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
  // check for self-assignment
  if (this == &rhs) {
    return *this;
  }
  // free the old memory
  for (int i = 0; i < mWidth; i++) {
    delete [] mCells[i];
  }
  delete [] mCells;
  mCells = nullptr;

  // copy the new memory
  mWidth = rhs.mWidth;
  mHeight = rhs.mHeight;

  mCells = new SpreadsheetCell* [mWidth];
  for (int i = 0; i < mWidth; i++) {
    mCells[i] = new SpreadsheetCell[mHeight];
  }

  for (int i = 0; i < mWidth; i++) {
    for (int j = 0; j < mHeight; j++) {
      mCells[i][j] = rhs.mCells[i][j];
    }
  }
  return *this;
}

代码取自 Spreadsheet\SpreadsheetNoCopyFrom.cpp

注意这些代码看上去与复制构造函数类似。下一节将介绍如何避免这种重复代码。

赋值运算符完成了管理对象动态分配内存的“三大”例程:析构函数、复制构造函数以及赋值运算符。只要编写了其中一个方法,就必须全部编写这些方法。

提示:只要类动态分配了内存,就应该编写析构函数、复制构造函数以及赋值运算符

C++11包含一个名为移动语义(movesemantics)的新概念,移动语义需要一个移动构造函数以及一个移动赋值运算符.在某些情况下可以用来增强性能,后面将对此进行详细讨论。

复制构造函数与赋值运算符十分相似。因此,将通用任务放在辅助方法中通常会带来便利。例如,可在Spreadsheet类中加入copyFrom()方法,并重写复制构造函数以及赋值运算符,如下所示:

void Spreadsheet::copyFrom(const Spreadsheet& src)
{
  mWidth = src.mWidth;
  mHeight = src.mHeight;

  mCells = new SpreadsheetCell* [mWidth];
  for (int i = 0; i < mWidth; i++) {
    mCells[i] = new SpreadsheetCell[mHeight];
  }

  for (int i = 0; i < mWidth; i++) {
    for (int j = 0; j < mHeight; j++) {
      mCells[i][j] = src.mCells[i][j];
    }
  }
}

Spreadsheet::Spreadsheet(const Spreadsheet &src)
{
  copyFrom(src);
}
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
  // check for self-assignment
  if (this == &rhs) {
    return *this;
  }
  // free the old memory
  for (int i = 0; i < mWidth; i++) {
    delete [] mCells[i];
  }
  delete [] mCells;
  mCells = nullptr;

  // copy the new memory
  copyFrom(rhs);

  return *this;
}

代码取自 Spreadsheet\Spreadsheet.cpp

禁止赋值以及按值传递

当在类中动态分配内存时,如果只是想禁止其他人复制对象或者为对象赋值,只需将operator=以及复制构造函数标记为private。通过这种方法,当其他任何人按值传递对象时、从函数或者方法返回对象时或者为对象赋值时,编译器都会报错。下面的Spreadsheet类定义禁止赋值并按值传递:

class Spreadsheet
{
 private:
  Spreadsheet(const Spreadsheet& src);
  Spreadsheet& operator=(const Spreadsheet& rhs);
};

代码取自 SpreadsheetNoCopyAssign\Spreadsheet.h

您不需要提供private复制构造函数以及赋值运算符的实现。链接器永远不会查看它们,因为编译器不允许代码调用它们。当代码复制Spreadsheet对象或者对Spreadsheet对象赋值时,编译器将给出如下的消息:

'=' : cannot access private member declared in class 'Spreadsheet'

如果想让子类访问protected而不是private。以及复制构造函数,可以将这两个方法设置为派生类将在后面进行讨论。