zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++实操 - 赋值运算符

C++ 运算符 赋值 实操
2023-09-11 14:22:08 时间

类的赋值运算符是允许你使用= 将一个实例分配给另一个实例。比如说:

  MyClass c1, c2;

  c1 = c2;  // assigns c2 to c1

实际上,一个赋值运算符的函数签名有好几种:

(1) MyClass& operator=( const MyClass& rhs );

(2) MyClass& operator=( MyClass& rhs );

(3) MyClass& operator=( MyClass rhs );

(4) const MyClass& operator=( const MyClass& rhs );

(5) const MyClass& operator=( MyClass& rhs );

(6) const MyClass& operator=( MyClass rhs );

(7) MyClass operator=( const MyClass& rhs );

(8) MyClass operator=( MyClass& rhs );

(9) MyClass operator=( MyClass rhs );

注:rhs表示right-hand side,表示运算符右边的运算对象。

这些签名在返回类型和参数类型上有区别。

虽然返回类型可能不是很重要,但是选择参数类型的选择是至关重要的。

(2), (5), 和(8)通过非const参考的方式传递参数。

这种方法不被推荐。因为这些签名的问题是以下代码将无法编译:

MyClass c1;

c1 = MyClass( 5, 'a', "Hello World" );  // assuming this constructor exists

这个就是前面拷贝构造函数里提到的,临时对象作无法通过非常引用方式传递。

这是因为这个赋值表达式的右手边是一个临时的(未命名的)对象,而C++标准禁止编译器通过非静态引用参数传递一个临时对象。

这使得我们只能通过值或常量引用来传递右边的对象。

尽管看起来通过常量引用传递参考传递比通过值传递更高效,但我们将在后面看到由于异常安全的原因,对源对象做一个临时的拷贝是不可避免的,进而通过值传递允许我们少写几行代码。

什么时候需要写一个赋值运算符函数?

首先,你应该明白,如果你没有声明一个赋值运算符,编译器会隐含地生成一个。

这个隐式赋值运算符对源对象的每个数据成员进行赋值,这个和拷贝构造函数一样。

例如,使用上面的类,编译器提供的赋值运算符函数完全等同于:

  MyClass& MyClass::operator=( const MyClass& other ) {

      x = other.x;

      c = other.c;

      s = other.s;

      return *this;

  }

一般来说,任何时候你需要写你自己的自定义拷贝构造函数时,你也需要写一个自定义的赋值运算符,一般是成员变量包含了原始指针,需要深拷贝的情况。

所以说拷贝构造函数和赋值构造函数关系紧密,类似双生。

需要注意的是,默认的拷贝构造函数对每个成员都调用其拷贝构造函数,而赋值操作符对每个每个成员都调用其赋值操作符。

异常安全的代码是什么意思?

在此插播一个关于异常安全的小故事,因为程序员们经常误以为异常处理就是异常安全。

一个修改了一些 "全局 "状态的函数(例如,一个引用

参数,或修改其实例的数据成员的成员函数)被称为是异常安全的。

实例的成员函数),如果它在异常发生时能使全局状态得到很好的定义,就可以说是异常安全的。

在函数中的任何时候被抛出的异常,如果它能使全局状态得到很好的定义,则被称为异常安全。

这到底是什么意思?好吧,让我们举一个相当牵强的(也很老套)的例子。这个类包装了一个用户指定的数据类型的数组。它有两个数据成员:一个指向数组的指针和一组数组中的元素。


  template< typename T >

  class MyArray {

      size_t  numElements;

      T*      pElements;





    public:

      size_t count() const { return numElements; }

      MyArray& operator=( const MyArray& rhs );

  };

上面这是一个模板类,那么,给这个模板类定义一个赋值运算符:

template<>

  MyArray<T>::operator=( const MyArray& rhs ) {

      if( this != &rhs ) {

          delete [] pElements;

          pElements = new T[ rhs.numElements ];

          for( size_t i = 0; i < rhs.numElements; ++i )

              pElements[ i ] = rhs.pElements[ i ];

          numElements = rhs.numElements;

      }

      return *this;

  }

但这行代码可能有问题:

pElements[ i ] = rhs.pElements[ i ];

这行代码可能会抛出一个异常。

这一行对类型T调用了operator=,这个类型可能是一些用户定义的类型,其赋值运算符可能会抛出一个异常,也许是内存不足(std::bad_alloc)的异常,或其他用户定义的类型的程序员创造的异常。

如果它真的抛出,例如在复制10的第3个元素时,会发生什么?

好吧,函数调用栈返回,直到找到一个合适的处理程序。

同时,我们的对象的状态是什么?嗯,我们重新分配了我们的数组来容纳10个T,但是我们只成功复制了其中的两个。第三个失败了,剩下的七个甚至没有被尝试过被复制。此外,我们甚至没有改变numElements,所以它还是原来的元素个数。很明显,如果我们在这个时候调用 count() 返回元素个数,返回的数字是不对的。

但显然,MyArray的程序员从来没有想过让count()给出一个错误的答案。更糟糕的是,可能还有其他成员函数更加依赖numElements的正确性(甚至达到崩溃的程度)。

这个例子显然是一个等待爆炸的定时炸弹。

这个operator=的实现不是异常安全的:如果在函数的执行过程中出现了异常抛出,我们就无法知道对象的状态是什么。

我们只能假设它处于这样一种糟糕的状态(即它违反了它自己的一些不变性),以至于无法使用。如果该对象是处于不良状态,甚至不可能在不破坏程序的情况下销毁该对象或导致MyArray抛出另一个异常。

我们知道,编译器在运行析构器的同时会返回栈以寻找一个处理程序。如果在返回栈时抛出了一个异常。程序必然会不可阻挡地终止。

如何编写一个异常安全的赋值运算符?

推荐的方法是通过以下方式来写一个异常安全的赋值运算符复制-交换模式。什么是copy-swap模式?简单地说,它是一种两步骤的算法:首先制作一个副本,然后与副本进行交换。下面是我们的异常安全版本的operator=。


  template<>

  MyArray<T>::operator=( const MyArray& rhs ) {

      // First, make a copy of the right-hand side

      MyArray tmp( rhs );



      // Now, swap the data members with the temporary:

      std::swap( numElements, tmp.numElements );

      std::swap( pElements, tmp.pElements );



      return *this;

  }

注意,这里是要对每个成员进行std::swap,指针类型也可以交换成功。

但如果这里改成:

std::swap(tmp, *this);

那就会变成无线递归调用,直到程序异常后停止执行。

因为你这里定义的是赋值操作符,而swap里,也会调用到这个对象的赋值操作,如果直接传入自己的对象,虽然编译没问题,但执行的时候,就会变成递归调用了。

异常处理和异常安全之间的区别的重要性就在这里:我们并没有阻止异常的发生;事实上。

从rhs到tmp使用拷贝构造函数时就可能会发生,因为它将复制T的内容。

但是,如果复制结构确实抛出异常,注意*this的状态并没有改变,这意味着在异常发生时,我们可以保证

我们可以保证*this仍然是正常的,此外,我们甚至可以说,它保持不变。

但是,你说,那std::swap呢?它能不抛出吗?是的,也不是。默认的std::swap<>,定义在<algorithm>中,可以抛出,因为std::swap<>看起来像这样:

template< typename T >

  swap( T& one, T& two )

  {

      T tmp( one );

      one = two;

      two = tmp;

  }

第一行运行T的复制构造函数,它可以抛出;其余几行是赋值运算符,也可以抛出。

然而,如果你有一个类型T,使用默认的std::swap()可能导致T的复制构造函数或赋值运算符抛出异常,那么你就需要为你的类型提供一个不抛出的 swap() 重载。

因为swap()不能返回失败,而且你也不允许抛出异常。

你的swap()重载必须总是成功。 通过要求swap不抛出异常,上面的operator=是安全的:要么对象被完全复制成功,或者左手边的对象保持不变。

现在你会注意到,我们对operator=的实现在第一行代码中就做了一个临时拷贝。既然我们必须做一个拷贝,我们不妨让编译器自动完成,这样我们就可以修改函数的签名,以便通过值(即拷贝)而不是引用来获取右边的内容。而不是通过引用,这样我们就可以省去一行代码。

template<>

  MyArray<T>::operator=( MyArray tmp ) {

      std::swap( numElements, tmp.numElements );

      std::swap( pElements, tmp.pElements );

      return *this;

  }

参考:

Copy constructors, assignment operators, - C++ Articlesicon-default.png?t=L9C2http://www.cplusplus.com/articles/y8hv0pDG/