1、可重载的运算符
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里@代表了被重载的运算符。函数参数表中参数的个数取决于两个因素:
1)运算符是一元的(一个参数)还是二元的(两个参数)。
2)运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数——此时该类的对象用作左侧参数)。
虽然几乎所有C中的运算符都可以重载,但运算符重载的使用是相当受限制的。特别是不能使用C中当前没有的运算符(例如用**代表求幂),不能改变运算符的优先级,不能改变运算符的参数个数。
一元运算符:+, -, ~, &, !, ++(), ++(int), --(), --(int)。
二元运算符:+, -, *, /, %, ^, &, |, <<, >>, +=, -=, *=, /=, %=, ^=, &=, |=, >>=, <<=, ==, !=, <,>, <=, >=, &&, ||。
1.1 自增和自减
重载的++和--运算符可能会引起混淆,因为我们总是希望能根据他们出现在所作用对象的前面(前缀)还是后面(后缀)来调用不同的函数。
- #include <iostream>
- using namespace std;
- //Non-member functions:
- class Integer
- {
- long i;
- Integer *This()
- {
- return this;
- }
- public:
- Integer(long ll = 11) :i(ll)
- {}
- friend Integer& operator++(Integer &a);
- friend Integer operator++(Integer &a, int);
- friend ostream& operator<<(ostream& os, const Integer &a);
- };
- //Global operators:
- Integer& operator++(Integer &a)
- {
- a.i++;
- return a;
- }
- Integer operator++(Integer &a, int)
- {
- return Integer(a.i++);
- }
- ostream& operator<<(ostream& os, const Integer &a)
- {
- return os << a.i;
- }
- //Member functions
- class Byte
- {
- unsigned char b;
- public:
- Byte(unsigned char bb = 0) :b(bb)
- {}
- Byte operator=(const Byte &rhs)
- {
- //Handle self-assignment
- if (&rhs == this)
- {
- return *this;
- }
- b = rhs.b;
- return *this;
- }
- Byte& operator++()
- {
- b++;
- return *this;
- }
- Byte operator++(int)
- {
- return Byte(b++);
- }
- };
- void main()
- {
- Integer Int(10);
- Integer Int2 = Int++;
- Byte B('b');
- Byte B2 = B++;
- cout << Int2 << endl;
- }
可以看到operator=只允许作为成员函数(这将在后面解释)。
请注意在运算符重载中所有赋值运算符都有代码检测自赋值(self-assignment),这总是原则。在某些情况下,这是不需要的。例如,对于operator+=,我们总是习惯A+=A,让A与自己相加。检测自赋值最重要的地方是operator=,因为复杂的对象可能因为它而发生灾难性的后果。
2、参数和返回值
虽然在运算符重载时可以用任何需要的方式传递和返回参数,但在一些情况下所用的方式却不是随便选的。他们遵循一种合乎逻辑的模式,我们在大部分情况下都应该选择这种模式:
1)对于任何函数参数,如果仅需要从参数中读而不改变他,默认的应当作为const引用来传递它。当一个函数是一个类成员的时候,就转换为const成员函数。
2)返回值的类型取决于运算符的具体含义。如果使用该运算符的结果是产生一个新值,就需要产生一个作为返回值的新对象。
3)所有赋值运算符均改变左值。为了使赋值结果能用于链式表达式(如a=b=c),应该能够返回一个刚刚改变了左值的引用。虽然我们是从左向右读表达式a=b=c,但编译器是从右向左分析这个表达式,所以并非一定要返回一个非常量值来支持链式赋值。然而人们有时希望能够对刚刚赋值的对象进行运算,例如(a=b).func(),这是b赋值给a后调用func()。因此所有赋值运算符的返回值对于左值应该是非常量引用。
4)对于逻辑运算符,人们希望至少得到一个int的返回值,最好是bool返回值。
2.1 作为常量通过传值方式返回
作为常量通过传值方式返回,看起来有些微妙。现在考虑二元运算符+。假设在表达式f(a+b)中使用它,a+b的结果变为一个临时对象,这个对象用于对f()的调用中。因为它是临时的,自动被定为常量,所以无论是否使返回值为常量都没有影响。
然而,也可能发送一个消息给a+b的返回值而不是仅传递给一个函数。例如写表达式(a+b).g(),这里,通过设返回值为常量,规定了对于返回值只有常量成员函数才可以被调用。用常量是恰当的,这是因为这样可以使我们不用在对象中存储可能有价值的信息,而该信息很可能会被丢失。
2.2 返回值优化
通过传值方式返回要创建的对象时,应注意使用的形式。例如在operator++(int):
return Byte(b++);
乍看起来像是一个“对一个构造函数的调用”,其实并非如此。这个临时对象的语法,它是在说:“创建一个Byte对象并返回它”。据此我们可能认为如果创建一个有名字的局部对象并返回它结果将会是一样的。其实不然。如果如下编写,
Byte tmp(b++);
return tmp;
将发生三件事。首先,创建tmp对象,其中包括构造函数的调用。然后,拷贝函数把tmp拷贝到外部返回值的存储单元里。最后,当tmp在作用域的结尾时调用析构函数。
相反,“返回临时对象”的方式是完全不同的。当编译器看到我们这样做时,它明白对创建的对象没有其他需求,只是返回它,所以编译器直接把这个对象创建在外部返回值的内存单元。因为不是真正的创建一个局部对象,所以仅需要一个普通构造函数调用(不需要拷贝构造函数),且不会调用析构函数。这种方法不需要什么花费,因此效率是非常高的,但程序员要理解这些。这种方式常被称为返回值优化(return value optimization)。
3、不常用的运算符
还有一些运算符的重载语法有一点不同。
下标运算符operator[ ],必须是成员函数并且它只接受一个参数。因为他所所用的对象应该像数组一样操作,可以经常从这个运算符返回一个引用,所以它可以被方便的用于等号左侧。
运算符new和delete用于控制动态存储分配并能按许多种不同的方法进行重载,以后在进行讨论。
3.1 operator,
这个运算符在这里就不讨论了,似乎(不用怀疑)没有许多实际用途。
3.2 operator->
当希望一个对象变得像一个指针时,通常就需要用到operator->。由于这样一个对象比一般的指针有着更多的与生俱来的灵巧性,于是常被称作灵巧指针(smart pointer)。
如果想用类包装一个指针以便是指针安全,或是在迭代器(iterator)普通的用法中,这样做回特别有用。
指针间接引用运算符一定是一个成员函数。他有着额外的、非典型的限制:他必须返回一个对象(或对象的引用),该对象也有一个指针间接引用运算符;或者必须返回一个指针,被用于选择指针间接引用运算符箭头所指向的内容。
3.3 operator->*
operator->*是一个二元运算符,其行为与所有其他二元运算符类似。它是专为模仿成员指针行为而提供的。
在定义operator->*是要注意它必须返回一个对象,对于这个对象,可以用正在调用的成员函数为参数调用operator()。
Operator()的函数调用必须是成员函数,它是唯一的允许在它里面有任意个参数的函数。
要想创建一个operator->*,必须首先创建带有operator()类,这是operator->*将返回对象的类。该类必须获取一些必要的信息,以使当operator()被调用时,指向成员的指针可以对对象进行间接引用。
- #include <iostream>
- using namespace std;
- class Dog
- {
- public:
- int eat(int i)const
- {
- cout << "eat\n";
- return i;
- }
- int run(int i)const
- {
- cout << "run\n";
- return i;
- }
- typedef int (Dog::*PMF)(int) const;
- //operator->* must return an object
- //that has an operator()
- class FunctionObject
- {
- private:
- Dog *ptr;
- PMF pmem;
- public:
- //Save the object pointer and member pointer
- FunctionObject(Dog *p, PMF pmf) :ptr(p), pmem(pmf)
- {
- cout << "FunctionObject construction\n";
- }
- //Make the call using the object pointer
- int operator()(int i)const
- {
- cout << "FunctionObject::operator()\n";
- return (ptr->*pmem)(i);
- }
- };//class FunctionObject
- FunctionObject operator->*(PMF pmf)
- {
- cout << "operator->*" << endl;
- return FunctionObject(this, pmf);
- }
- };//class Dog
- void main()
- {
- Dog w;
- Dog::PMF pmf = &Dog::run;
- cout << (w->*pmf)(1) << endl;
- pmf = &Dog::eat;
- cout << (w->*pmf)(2) << endl;
- }
4、不能重载的运算符
1)成员选择operator.。点在类中对任何成员都有一定意义。但如果允许它重载,就不能用普通的方法访问成员,只能用指针和指针operator->访问。
2)成员指针间接引用operator->。
3)没有求幂运算符。这可以通过函数调用来实现。
4)不存在用户定义的运算符,即不能编写目前运算符集合中没有的运算符。
5)不能改变优先级规则
5、非成员运算符
在前面的一些例子里,运算符可能是成员运算符或非成员运算符,这似乎没有多大差异。这样就会出现一个问题:“应该选择哪一种?”。总的来说,如果没有差异,他们应该是成员运算符。这样做强调了运算符和类的联合。当做操作数是当前类的对象时,运算符会工作得很好。
但有时左侧运算符是别的类的对象。这种情况通常出现在为输入输出流重载operator<<和>>时。因为输入输出流是一个基本C++库,我们将有可能想为定义的大部分类重载运算符。
5.1 基本方针
运算符 | 建议使用 |
所有的一元运算符 = () [] -> ->* += -= /= *= ^= &= |= %= >>= <<= 所有其他二元运算符 | 成员 必须是成员 成员 非成员 |
6、重载赋值符
赋值符经常引起C++程序员的混淆。因为‘=’在编程中是最基本的运算符,是在机器层上拷贝寄存器。另外,当使用‘=’时也能引起拷贝构造函数调用:
MyType b;
MyType a = b;
a = b;
第2行定义了对象a。a是从现有的MyType对象创建的,所以会调用拷贝构造函数。
第3行就不同了.在等号左侧有一个初始化的对象。不用为一个已经存在的对象调用构造函数。在这种情况下,为a调用MyType::operator=,把出现在右侧的任何东西作为参数(可以有多种取不同右侧参数的operator=函数)。
我不想在这里再仔细讨论赋值操作符的行为和它包含指针的情况,读者可以阅读《C++编程思想 第一卷》。我只告诉读者operator=应该遵守下面的模式:
1)确保程序不是给自己赋值。如果是的话,跳到步骤6。(这是一种严格的优化)
2)给指针数据成员分配所需的新内存。
3)从原有的内存区向新分配的内存区拷贝数据。
4)释放原有的内存。
5)更新对象的状态,也就是把指向分配新堆内存地址的指针赋值给指针数据成员。
6)返回*this。
重要的是考虑异常安全,要保证直到所有的新增部件都被安全的非配到内存并初始化之前不要修改对象的状态。一个好的技巧是将步骤2和步骤3放到单独的函数中,这个函数常被叫做clone()。(原因将在讨论异常安全时给出)。
这个代码有点复杂,不过值得研究,在异常安全中会给出处理异常的情况。
- #include <new>
- #include <cstring>
- #include <cstddef>
- #include <iostream>
- using namespace std;
- class HasPointers
- {
- private:
- struct MyData
- {
- const char *theString;
- const int *theInts;
- size_t numInts;
- MyData(const char *pString, const int *pInts, size_t nInts)
- :theString(pString),
- theInts(pInts),
- numInts(nInts)
- {}
- };//Mydata
- MyData *theData;
- //Clone and clearup functions
- static MyData* clone(const char *otherString, const int *otherInts, size_t nInts)
- {
- char *newChars = new char[strlen(otherString) + 1];
- int *newInts;
- newInts = new int[nInts]; //分配新内存
- strcpy(newChars, otherString); //拷贝数据
- for (size_t i = 0; i < nInts; ++i)
- {
- newInts[i] = otherInts[i];
- }
- return new MyData(newChars, newInts, nInts);
- }
- static MyData* clone(const MyData *otherData)
- {
- return clone(otherData->theString, otherData->theInts, otherData->numInts);
- }
- static void clearup(const MyData *theData)
- {
- delete[](theData->theString);
- delete[](theData->theInts);
- delete theData;
- }
- public:
- HasPointers(const char *someString, const int *someInts, size_t numInts)
- {
- theData = clone(someString, someInts, numInts);
- }
- HasPointers(const MyData *someData) //拷贝构造函数
- {
- theData = clone(someData);
- }
- HasPointers& operator=(const HasPointers &rhs) //赋值操作符
- {
- if (this != &rhs)//1、判断是否为自己赋值
- {
- MyData *newData = clone(rhs.theData);//2、分配空间,3、拷贝数据
- clearup(theData);//4、清理原来数据
- theData = newData;//5、改变对象状态
- }
- return *this; //6、返回*this
- }
- ~HasPointers()
- {
- clearup(theData);
- }
- friend ostream& operator<<(ostream &os, const HasPointers &obj)
- {
- os << obj.theData->theString << ":";
- for (size_t i = 0; i < obj.theData->numInts; i++)
- {
- os << obj.theData->theInts[i] << " ";
- }
- return os;
- }
- };//HasPointers
- void main()
- {
- int someNums[] = { 1, 2, 3, 4 };
- size_t someCount = sizeof someNums / sizeof someNums[0];
- int someMoreNums[] = { 5, 6, 7 };
- size_t someMoreCount = sizeof someMoreNums / sizeof someMoreNums[0];
- HasPointers h1("hello", someNums, someCount);
- HasPointers h2("Goodbye", someMoreNums, someMoreCount);
- cout << h1 << endl;
- h1 = h2;
- cout << h1 << endl;
- }
6.1 自动创建operator=
因为将一个对象赋给另一个相同类型的对象是大多数人可能做的事请,所以如果没有创建type::operator=*(type),编译器将自动创建一个。这个运算符行为模仿自动创建的拷贝构造函数的行为。
7、引用计数
在上面的例子中,拷贝构造函数和operator=对指针所指向的内容做了一个新的拷贝,并由析构函数删除它。但是,如果对象需要大量的内存或过高的初始化,我们也许想避免这种拷贝。解决这个问题的方法称为引用计数(reference counting)。可以使一块存储单元具有智能,它知道有多少个对象指向它。拷贝构造函数或赋值运算意味着把另外的指针指向现在的存储单元并增加引用计数。消除意味着减小引用计数,如果引用计数为0则意味销毁这个对象。
但如果向这个对象执行写入操作将会如何呢?因为不止一个对象使用这个对象,所以当修改自己的对象时,也等于修改了其他人的对象。为了解决这个“别名”问题,经常使用另外一个称为写时拷贝(copy-on-write)的技术。在向这块单元写之前,应该确信没有其他人使用它。如果引用计数大于1,在写之前必须拷贝这块存储单元,这样就不会影响其他人了。
这里就不给出例子了,读者可以查看《C++编程思想 第一卷》,当然在《boost程序库完全开发指南》这本介绍C++准标准库的著作中有含量较高的引用计数型数组(ref_array)的完整实现,读者可以啃一啃。
8、自动类型转换
在C或C++中,如果编译器看到一个表达式或函数调用使用了一个不合适的类型,它经常会执行一个自动类型转换,从现在的类型到所要求的类型。
有时(读者可以查一查)通过构造函数自动转换类型可能会出现问题。为了避开这个麻烦,可以在前面加关键字explicit(只能用于构造函数)。