版权声明
-------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org)
作者: 女儿叫老白 (白振勇)
转载请注明出处!
-------------------------------------------------------------------------------
课程目录:《C++老鸟日记》目录
本套课程属于:《C++跨平台开发干货》系列课程。
-------------------------------------------------------------------------------
引言:
----------------------------------------------------------------------------
前面的章节中,我们提到过运算符重载的问题,比如“让a=b成立有哪些办法”章节中我们讨论了重载操作符operator=。那么,运算符重载到底有什么意义呢?我们都知道,基本数据类型(plain)在进行运算时,比如对int类型、double类型进行加减乘除以及相互比较大小的操作编译器是默认提供支持的。但是对于用户自定义的类或结构体,执行加减乘除以及比较大小的操作就需要用户自己提供定义了,编译器是无法知道这些操作的具体含义的。今天我们来详细讨论一下运算符重载相关的知识。
正文:
----------------------------------------------------------------------------
小编经常用到的运算符重载有:=、==、>、<、<<、>>等。这些运算操作符重载经常会提供两个版本:全局版本和成员函数版本?那么,这两种版本在编码时有什么具体区别呢?
代码清单:
----------------------------------------------------------------------------
// 全局版本
// myclass.h
class CMyClass {
public:
CMyClass():m_nValue(0){}
~CMyClass(){}
friend QDataStream &operator<<( QDataStream &, const CMyClass &);
friend QDataStream &operator>>( QDataStream &, CMyClass &);
private:
int m_nValue;
};
QDataStream &operator<<( QDataStream &ds, const CMyClass &mc) {
ds << QVariant(mc.m_nValue);
return ds;
}
QDataStream &operator>>( QDataStream &ds, CMyClass & mc) {
QVariant var;
ds >> var;
mc.m_nValue = var.toInt();
return ds;
}
----------------------------------------------------------------------------
在全局版本中,我们看到,使用friend(友元)的方式定义了两个重载接口:
friend QDataStream &operator<<(CDataStream &, const CMyClass &);
friend QDataStream &operator>>(CDataStream &, CMyClass &);
为什么要用友元呢?因为在这两个接口内部,需要访问CMyClass的私有成员变量。当然,我们也可以把这些成员变量使用get接口封装,但是那样的话,在为这两个重载接口编写实现代码时会比较麻烦。
下面,我们看一下成员函数版本:
代码清单:
----------------------------------------------------------------------------
// 成员函数版本
// myclass.h
class CMyClass {
public:
CMyClass() :m_nValue(0) {}
~CMyClass() {}
QDataStream &operator<<(QDataStream &);
void operator>>(QDataStream &);
private:
int m_nValue;
};
QDataStream & CMyClass ::operator<<(QDataStream &ds) {
ds << QVariant(m_nValue);
return ds;
}
void CMyClass ::operator>>(QDataStream &ds) {
QVariant var;
ds >> var;
m_nValue = var.toInt();
}
----------------------------------------------------------------------------
上述代码是运算符<<、>>的成员函数版本。可以看出,这个版本中,函数的入口参数只有一个(QDataStream&),而全局版本中,入口参数有两个。
在小编看来,成员函数版本更方便,因为不需要声明友元,而且函数的入口参数少一个。那么,到底该选择成员函数版本还是全局版本呢?
Murray(注1)为在成员和非成员之间的选择提出了如下方针:
----------------------------------------------------------------------------
运算符 建议使用
----------------------------------------------------------------------------
所有的一元运算符 成员
= () [] -> ->* 必须是成员
+= -= /= *= ^= 成员
&= != %= >>= <<= 成员
所有其他二元运算符 非成员
----------------------------------------------------------------------------
前面的章节中,我们介绍过赋值运算符,但是有一个细节没有提到过,请看代码:
代码清单:
----------------------------------------------------------------------------
CMYClass& CMYClass::operator=(const CMYClass & right) {
if (this != &right) {
xxx = right.getxxx();
yyy = right.getyyy();
}
return *this;
}
----------------------------------------------------------------------------
在上述赋值构造函数的代码中,需要注意的是函数内第一行代码中的判断:
if (this != &right)
这也是所有赋值运算符重载时需要遵守的总原则:需要进行自赋值检测(self-asignment)。(注2)
我们来看一下下面的代码,看有何不同:
代码清单:
----------------------------------------------------------------------------
// 代码1
return CMyClass(a.i);
// 代码2
CMyClass mc(a.i)
return mc;
----------------------------------------------------------------------------
这两种编码方式执行效率一样吗?
答案是:不一样。
那么为什么呢?
在代码1中直接构造一个临时对象(通过调用构造函数的方式),并返回它。
在代码2中,发生了3件事,首先构造了一个对象,然后在函数结尾将该对象拷贝到函数返回值的内存中,最后在函数调用结束时调用该对象析构函数。(注3)
在代码1中,直接return对象的语法告诉编译器,我们这样做仅仅是为了返回数据,所以编译器会直接把临时对象构造在函数返回值的内存中。因为这不是真正创建一个对象它只是一个普通的构造函数调用,不会调用拷贝构造函数也不用调用析构函数(因为没有真正创建对象,而是直接把对象构造在了存放函数返回值的内存中)。这种方法效率是非常高的,常被成为返回值优化(注4)。
至此为止,我们心中可能有一个疑问,所有运算符都能重载吗?哪些运算符不能重载呢?
不能重载的运算符清单(注5):
1, operator. 这是对类成员访问的运算符,如果允许重载可能导致无法使用普通的方法访问类的成员,而只能用指针和operator->访问。
2, 成员指针间接引用operator->*,原因同上。
3, 求幂运算符operator**,C中没有,C++也未提供。
4, 不存在的运算符,即用户不能自定义当前C++语法中尚未定义的运算符。
除此之外,还有啥要注意的呢?运算符重载时有哪两点不能做:
1, 不能改变运算符的优先级
2, 不能改变运算符的参数个数
比如原来是一元运算符,不能重新定义为二元运算符。
结语:
----------------------------------------------------------------------------
本节,我们讨论了运算符重载的知识,我们知道了运算符重载时的全局版本与成员函数版本以及这两个版本的选择依据,知道了赋值运算符重载时需要遵守的总原则,了解了直接returan临时对象语法的执行效率高的原因,知道了哪些运算符不能重载,掌握了运算符重载时不能做的两件事。OK,知道了这么多,让我们开始编程吧。
参考资料
----------------------------------------------------------------------------
注1:
Rob Murray所著《C++ Strategies & Tacties》(Addison-Wesley), 1993,第47页,
《C++编程思想》两卷合订本中文版(P297),(美) Bruce Eckel Chuck Allison著。
注2、注3、注4:《C++编程思想》两卷合订本中文版(P289),(美) Bruce Eckel Chuck Allison著
注5:《C++编程思想》两卷合订本中文版(P295),(美) Bruce Eckel Chuck Allison著