《C++面向对象高效编程(第2版)》——3.4 赋值操作符

简介:

本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第3章,第3.4节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

3.4 赋值操作符

C++面向对象高效编程(第2版)
现在,让我们分析main程序中的语句:

a = b ;  // 将一个栈赋值给另一个栈
在该语句中,我们将对象b赋值给对象a,使用赋值操作符完成赋值操作。如果a和b都是简单的整数,无论a中的值是什么,编译器都会用b中的值将其擦写(overwrite)。就是这么简单。但是在该例中,a和b都是我们创建的对象,这意味着由我们负责赋值操作。我们知道TIntStack类对象之间如何进行赋值,并且能够实现赋值操作符。对于任何赋值操作符,都应注意以下几点:

(1)确保对象没有自我赋值(如a = a)。

(2)复用被赋值对象中的资源或销毁它。

(3)从源对象中将待复制内容复制到目的对象。

(4)最后,返回对目的对象的引用。

赋值操作符已在类中声明。TIntStack类的赋值操作符签名如下:

TIntStack& operator=(TIntStack& source) // ①
TIntStack& operator=(const TIntStack& source) // ②```
下面是赋值操作符的实现:

TIntStack& TIntStack::operator=(const TIntStack& source)
{
  // 检查自我复制(即a = a)。
  // source是被赋值的对象,&source给出对象source的地址,
  // ‘this’是被赋值对象的地址。
  if (this == &source) {
      cout << “Warning: Assignment to self.n”;
      return *this;
  }
   /*
    如果源对象栈中的元素数目小于或等于目的对象的大小,则没有任何问题。
    我们要做的就是复制栈中相应的元素和_count变量。
    但是,如果源对象栈中元素的数目大于目的对象的大小,则必须先删除目的对象_sp中内存,再为其分配新的内存(与源对象栈中元素的数目相等),然后复制所有元素。
   */
  if (source._count > this->_size) {
     // 源对象中元素的数目大于目的对象的大小,
     // 删除_sp中的内存并重新分配内存。
     // 见下文解释
      delete [] _sp;
      this->_size = source._size;
      _sp = new int [ this->_size ];
  }
    // 无论是小于等于还是大于的情况,都会用到以下代码遍历栈,
    // 并复制元素
    for (int i = 0; i < source._count; i++)
      this->_sp[i] = source._sp[i]; // 复制元素
    this->_count = source._count;
    return *this; // 见下文解释
}`
分析:赋值操作由程序员显式完成,系统(编译器)绝不会直接调用它。赋值是在两个现有对象之间执行的操作。如下语句:
`
a = b;`
当a和b都为对象时,上句可解析为:
`
operator=(b);`
换言之,左侧(left hand side,缩写为LHS)的对象调用成员函数operator=(),右侧(right hand side,缩写为RHS)操作数作为operator=()的参数。赋值操作右侧的对象(该例中为b)不能被修改,该操作的副作用(side effect)是:返回对左侧对象(该例中为a)的引用。因为不能修改右侧的对象(我们只能读取右侧对象并写入左侧对象),所以该对象应作为const实参传递给operator=()函数(如②所示)。然而,如果使用没有const参数的赋值操作符(如①所示),就可以被修改右侧的对象。我们并不推荐使用①这样的赋值操作符,因为类的用户并不希望在赋值操作中改变右侧的对象。由于a和b都是真正的现有对象,因此,将b赋值给a时,a中的值将被b中的值擦写(overwrite)。

记住:

如果我们在类中未提供赋值操作符,编译器会为类生成一个默认赋值操作符(编译器绝不会自动调用它,但确实会生成一个)。但是,这样的默认赋值操作符可能无法满足我们的要求,下一章将对此作详细介绍。

在任何赋值操作中,我们要做的就是将数据从源对象赋值到目的对象。一般而言,这很容易,但在某些情况下会有困难。在TIntStack的示例中,我们需要从源栈赋值到目的栈。如果源栈中的元素数目少于或等于目的栈中的可用空间,赋值操作便非常简单,只需复制相应的栈元素。但是,如果源栈中的元素数目多于目的栈所能持有的数目会怎样?我们不允许这样的操作发生,在打印出错误消息后将从赋值操作返回。不过,这样限制太大。或者,我们可以改变目的栈的大小,使其能容纳所有元素。如果我们接受后者的方案,就需要在目的栈上为源栈的元素分配与源栈大小相等的新内存。这样做之前,我们还要删除目的栈中_sp所指向的现有内存(无法扩展这个内存),这就是上面的赋值操作符中调用delete所完成的任务。一旦完成内存分配,我们就只需要复制相应的元素进栈即可(见图3-3和图3-4)。

(摘自前面的main程序)

TIntStack a(5);  // 自动对象(_auto object_),在退出main时被销毁 
TIntStack b(20);  // 另一个自动对象```
最后,我们返回对目的对象(也就是栈a)的引用。你可能会质疑,为什么要这样做?假设我们编写了一个级联赋值操作(cascaded assignment operation)如下:

`c = a = b; // 将b赋值给a,然后将结果再赋值给c`
赋值操作按从右到左的顺序执行1,此表达式即转换为:

`c = (a = b); // 首先将b赋值给a,然后将结果赋值给c`
警告:
既然可以写“a = b;”,那么也可以写

`a = a;  // 语法上正确,但是逻辑上错误!`
这样的表达式可能会通过指针和引用(别名)直接或间接地发生,我们的实现必须检查是否出现这种情况。因此,必须核实源地址和目的地址是否相同。如果赋值发生在相同的对象之间,则不会进行任何操作,只返回对目的对象的引用。这就是上面示例的实现中,进行自我赋值检查所完成的工作。

如果没有进行自我赋值检查,可能会导致严重问题。考虑以下包含指针数据成员的TString类:

class TString {
    public:
       TString (const char* sp) {
         _data = new char[strlen(sp) + 1];
          strcpy(_data, sp);
       }
       TString & operator=(const TString & assign);
    private:
       char* _data; // 字符串指针
};
TString& TString::operator=(const TString& assign)
{
    // 错误代码,未检查自我赋值  
  delete [] this->_data; // ①
   data = new char[strlen(assign.data) + 1]; // ②
   strcpy(this->_data, assign._data);
   return *this;
}
TString x1(“Text string1”);
x1 = x1;`
以上operator=的实现并不安全,其中的①,删除的是x1._data。接着在②中,我们又试图使用assign._data。因为左侧的对象(*this)与右侧的对象(assign)是同一个对象,即assign._data与x1._data相同。所以,我们搬起石头砸自己的脚。当执行②时,该程序可能崩溃,或者在别的地方出现问题。因此,在赋值操作的实现中,必须进行自我赋值检查。
image

图3-4

将b赋值给a非常简单,之前的示例中已介绍过赋值如何工作。我们希望将赋值(b赋值给a)的结果再赋值给c。为使其正常工作,第1个赋值操作应返回a的值(或对a的引用)。否则,从a到c的第2次赋值就会出问题。通常,在C和C++中,我们只知道使用赋值操作的结果,却并不了解内部的实现。例如,正是因为赋值操作的副作用,才使得以下代码可以正常工作:

int i = 10, j = 20; // 此处无赋值操作
int k;
cout << (i = j) << endl; // 将j赋值给i,并打印赋值结果①。

if (k = j) { // 将j赋值给k,如果结果不为0,执行if体内部的语句②。
   // 省略此处代码
}```
在①中,完成赋值后返回i的值,并打印该值。与此类似,在②中,将j赋值给k,完成赋值后,返回k的值并与0进行比较。

为了让以上赋值都能正常工作,必须确保在赋值操作完成后,该赋值操作返回对a对象的引用。

`Pascal:`

在类似Pacal的语言中,以下的赋值操作没有副作用:

`a := b;`
在赋值后无法返回a的值。因此,以下语句

`c := a := b;`
无法在Pascal(以及类似的语言,如Modula-2)中使用。

思考:

对比复制构造函数和赋值操作符中的代码,会发现很多相似的地方。它们都将现有对象复制到另一个对象中。但是,你能否发现两处代码的主要差别?换言之,复制构造函数和赋值操作符的代码有何区别?能否让它们共享实现?

为完成TIngStack的示例,其余的成员函数实现如下:

void TIntStack::Push(int what)
{
   if (_count < _size) {  // 如有更多空间储存元素
    _sp[_count] = what;  // 储存元素
    _count++;  // 递增count
   }
   else {
    cout << “Stack is FULL. Cannot Push value” << what << endl;
   }
}
int TIntStack::Pop()
{
   if (_count <= 0) { // 栈为空
     cout << “Stack is EMPTYn”;
     exit (1);  // 如果失败如何报错?可在此处抛出异常。
   }
   _count--;
   return _sp[_count];
}
unsigned TIntStack::HowMany() const
{
   return _count;
}`
1此处用到操作符重载和操作符结合律,将在第8章中介绍。在本章讨论中,只需记住赋值从右向左结合。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

相关文章
|
1月前
|
C++
在C++语言中赋值运算符
在C++语言中赋值运算符
9 0
|
1月前
|
存储 C++
C/C++中位操作符(&,|,^,~)的详解使用
C/C++中位操作符(&,|,^,~)的详解使用
|
3天前
|
存储 编译器 C++
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
【C++成长记】C++入门 | 类和对象(中) |拷贝构造函数、赋值运算符重载、const成员函数、 取地址及const取地址操作符重载
|
16天前
|
存储 人工智能 BI
【C++面向对象】C++银行卡管理系统(源码+论文)【独一无二】
【C++面向对象】C++银行卡管理系统(源码+论文)【独一无二】
|
24天前
|
设计模式 存储 算法
【C++ 函数调用操作符】探究C++中的函数调用操作符 基础到高级应用
【C++ 函数调用操作符】探究C++中的函数调用操作符 基础到高级应用
273 0
|
24天前
|
算法 IDE Java
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
39 0
|
24天前
|
算法 安全 程序员
【C++14 新特性 透明操作符】透视C++14透明操作符Functors:深入理解与实践
【C++14 新特性 透明操作符】透视C++14透明操作符Functors:深入理解与实践
40 3
|
29天前
|
设计模式 负载均衡 算法
C/C++发布-订阅者模式世界:揭秘高效编程的秘诀
C/C++发布-订阅者模式世界:揭秘高效编程的秘诀
66 1
|
30天前
|
存储 安全 编译器
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
76 0
|
30天前
|
存储 程序员 C++
C/C++ 内存分配 new 操作符:剖析new操作符的实现机制和使用技巧
C/C++ 内存分配 new 操作符:剖析new操作符的实现机制和使用技巧
23 0