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

    xiaoxiao2024-03-29  9

    本节书摘来自异步社区出版社《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相同。所以,我们搬起石头砸自己的脚。当执行②时,该程序可能崩溃,或者在别的地方出现问题。因此,在赋值操作的实现中,必须进行自我赋值检查。

    图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章中介绍。在本章讨论中,只需记住赋值从右向左结合。本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)