赋值运算符重载(C++实现)详解

C++ 允许程序员重新定义标准运算符在与类对象一起使用时的工作方式。

正如前面所介绍的,复制构造函数的设计是为了解决包含指针的对象在通过按成员赋值的方式使用同一类的另一个对象的数据进行初始化时出现的问题。类似的问题也会出现在对象赋值中。

例如,创建一个 NumberArray 类,可以让程序如下定义该类的两个对象:

NumberArray first(3,10.5);
NumberArray second(5, 20.5);

现在,因为 C++ 允许赋值运算符(=)与类对象一起使用,所以,以下语句也是可以执行的:

first = second;

如果要将第一个对象 first 设置为与第二个对象 second 完全相同的值,此时,C++ 将再次执行按成员复制,从第二个对象复制成员到第一个对象中,这将导致两个对象中的指针指向同一内存。

由于默认的对象赋值遇到了与默认的复制构造函数相同的问题,所以有人可能会认为,程序员定义的复制构造函数也可以用来解决默认赋值所导致的问题,但事实并非如此。复制构造函数仅在创建对象并初始化对象时才起作用。特别是,复制构造函数不能在赋值中调用。

要理解初始化和赋值之间的区别,可以假设已经创建了 first 对象,然后来看以下语句:

NumberArray second = first; //复制构造函数被调用

该语句创建 second 对象,并使用 first 的值初始化它,这是一个初始化语句,它导致复制构造函数被调用来执行初始化。但是,以下语句则不一样:

second = first; //复制构造函数未被调用

该语句假定两个对象都是以前创建的,它仅仅是一个赋值语句,因此不会调用构造函数。

为了解决由对象的按成员赋值引起的问题,就需要修改赋值运算符的行为,以便在将其应用于具有指针成员的类的对象时,执行按成员赋值之外的其他操作。换句话说,就是要提供用于该类的对象的赋值运算符的不同版本。所以,这样做也可以说是要重载赋值运算符。

为给定的类重载赋值运算符的一种方式是定义一个名为 operator= 的运算符函数作为该类的成员函数。例如,要为 NumberArray 类执行此操作,则可以按如下所示编写该类的声明:
class NumberArray
{
    private:
        double *aPtr;
        int arraySize;
    public:
        void operator = (const NumberArray & right) ;//重载运算符
        NumberArray(const NumberArray &);
        NumberArray(int size, double value);
        ~NumberArray() { if (arraySize > 0) delete [ ] aPtr; }
        void print() const;
        void setValue(double value);
};
现在先来看一看函数头或原型,然后再看一看运算符函数本身是如何实现的。函数头的各个主要部分可以分解如下,如图 1 所示。

运算符重载 operator= 的函数头分解
图 1 运算符重载 operator= 的函数头分解

该函数的名称是 operator =。由于该运算符函数是类的实例成员,因此只能通过类的对象调用。通过它调用的类的对象被认为是赋值运算符的左操作数,而传递给该函数的形参被认为是赋值运算符的右操作数。

为了说明问题,现在假设在程序中定义了 left 和 right 两个对象:

NumberArray left(3, 10.5);
NumberArray right(5, 20.5);

为了把 right 的值赋给 left,可以通过 left 对象调用成员函数 operator =,并且将 right 对象作为形参传递给它:

left.operator=(right);

虽然可以用这种方式调用运算符函数,但编译器仍然允许使用更常用的表示法:

left = right;

注意,运算符函数的形参不必按引用传递,也不必声明为 const。在该示例中使用引用形参是出于提高效率的原因:引用参数避免了复制被传递作为形参的对象的开销。const 用于保护形参不被改变。

类赋值运算符的返回值

图 1 显示了该重载赋值运算符将返回对 NumberArray 的引用。这和 C++ 的内置赋值运算符的功能是一致的,它允许像下面这样的层叠赋值语句:

a = b = c;

层叠赋值语句之所以有效,是因为内置赋值运算符在起作用。在赋值操作执行之后,它返回其左侧操作数的引用。因此,在该语句中,表达式 b = c 导致 c 被赋值给 b,然后返回对 b 的引用,而这个返回的引用的值最后被赋给了 a。

类赋值运算符的实现

现在来思考一下赋值运算符的实现。首先要注意的是,如果语句像下面这样:

x = x;

那么这并不需要执行任何复制操作,结果就是一个对象被赋值给了它自己(像这样的赋值语句可能会在某些大型程序中出现)。可以通过检查赋值语句左侧对象的 this 地址是否和右侧对象的地址相同来测试其可能性,示例语句如下:

if (this ! = &right) { /* 复制对象...*/ }

赋值运算符函数通过删除分配给指定对象中的指针的内存开始。在此之后,它将以与该类的复制构造函数几乎相同的方式创建另一个对象的副本。函数的最后操作是通过引用返回赋值语句左侧的 *this 对象。以下就是该函数的代码:
NumberArray& NumberArray::operator=(const NumberArray &right)
{
    if (this != &right)
    {
        if (arraySize > 0)
        {
            delete [] aPtr;
        }
        arraySize = right.arraySize;
        aPtr = new double[arraySize];
        for (int index = 0; index < arraySize; index++)
        {
            aPtr[index] = right.aPtr[index];
        }
    }
    return *this;
}