Boolan第一周笔记(一)复合赋值运算符和算术运算符究竟该重载成成员函数还是非成员函数
第一周侯捷老师以complex类为例讲解了不含指针的类的写法。讨论群里有一位同学提出了一个问题,大意是:对于侯老师给出的complex类(下面是部分代码),为什么+=设计为成员函数就是合理的,+设计为成员函数就是不合理的。我通过翻书和写代码得到的结论是:对于左数是complex类的对象的情况,+= 和 +作为成员函数或非成员函数都不影响对它们的调用,程序都可以正确运行,但是+=设计成成员函数,+设计成非成员函数更合理。原因是:在为自定义的类定义运算符时要尽量模仿内置类型的写法,这样做符合使用我们自定义类型的用户长期以来的习惯(用户怎样使用内置类型的运算符,现在就可以以同样的方式使用我们自定义的运算符)。这一点侯捷老师还有C++ Primer都提到过。
本文的讨论过程和结论并不保证完全正确(但代码肯定对),它只是我对这个问题做的探讨和尝试,欢迎有更好想法的朋友与我交流。
以下是跟这位同学的疑问有关的代码,来自侯捷老师 & Boolan C++工程师微专业。
#ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ class complex; complex& __doapl(complex* ths, const complex& r); class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } complex& operator += (const complex&); double real() const { return re; } double imag() const { return im; } private: double re, im; friend complex& __doapl(complex *, const complex&); }; inline complex& __doapl(complex* ths, const complex& r) { ths->re += r.re; ths->im += r.im; return *ths; } inline complex& complex::operator += (const complex& r) { return __doapl(this, r); } inline double imag(const complex& x) { return x.imag(); } inline double real(const complex& x) { return x.real(); } inline complex operator + (const complex& x, const complex& y) { return complex(real(x) + real(y), imag(x) + imag(y)); } inline complex operator + (const complex& x, double y) { return complex(real(x) + y, imag(x)); } inline complex operator + (double x, const complex& y) { return complex(x + real(y), imag(y)); } #endif // !__MYCOMPLEX__
先回答+=的设计。
首先,+=并不是非得写成成员函数。C++ Primer(中文第五版,第500页)有提到“复合赋值运算符并不非得是类的成员,不过我们还是倾向于把包括符复合赋值运算符在内的所有赋值运算都定义在类的内部“。就是说,把复合赋值运算符定义在类内是推荐使用的做法,但是并不严格要求必须这样做。这一点通过以下的程序可以得到证明。
1.复合赋值运算符作为类的成员函数:
//complex.h #ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ #include<iostream> using namespace std; class complex; complex& __doapl(complex* ths, const complex& r); class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } complex& operator += (const complex&); double real() const { return re; } double imag() const { return im; } private: double re, im; friend complex& __doapl(complex *, const complex&); }; inline complex& __doapl(complex* ths, const complex& r) { ths->re += r.re; ths->im += r.im; return *ths; } inline complex& complex::operator += (const complex& r) { cout << "complex& complex::operator += (const complex& r) " << endl; return __doapl(this, r); } #endif //__MYCOMPLEX__
//complex_test.cpp #include "complex.h" ostream& operator << (ostream& os, const complex& x) { return os << '(' << x.real() << ',' << x.imag() << ')'; } int main() { complex c1(2, 1); complex c2(4, 0); cout << "c1 + c2 =" << (c1 += c2) << endl; system("pause"); return 0; }
运行结果
2.符合赋值运算符不是类的成员
//complex.h #ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ #include <iostream> using namespace std; class complex; complex& __doapl(complex& ths, const complex& r); class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } double real() const { return re; } double imag() const { return im; } private: double re, im; friend complex& __doapl(complex&, const complex&); }; inline complex& __doapl(complex& l, const complex& r) { l.re += r.re; l.im += r.im; return l; } inline complex& operator += (complex& l, const complex& r) { cout << "complex& operator += (complex& l, const complex& r)"; return __doapl(l, r); } #endif //__MYCOMPLEX__
//complex_test.cpp #include "complex.h" ostream& operator << (ostream& os, const complex& x) { return os << '(' << x.real() << ',' << x.imag() << ')'; } int main() { complex c1(1,2),c2(2, 1); cout << (c1+=c2) << endl; system("pause"); return 0; }
运行结果
观察运行结果可知,+=是否作为类的成员函数都不影响对+=的调用。但是+=作为一种赋值运算符,最好使跟内置的赋值运算符一样写成成员函数。
接下来讨论对于开头的第一段代码,+运算符什么情况下需要写成成员函数,什么情况下需要写成非成员函数。
这段代码重载了三个+运算符,首先讨论下面这个。
inline complex operator + (double x, const complex& y) { return complex (x + real (y), imag (y)); }
C++ Primer(中文第五版 493页)提到,“当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象”。这个+的左侧是double,因此不可以写成complex类的成员函数。另外,double类型是内置类型,没有定义一个double 和一个复数的+运算符。所以我们需要自己写这个+,并把它写成complex的非成员函数。
下面的两个+运算符作为complex类的成员函数或非成员函数,程序都可以运行。
inline complex operator + (const complex& x, const complex& y) { return complex (real (x) + real (y), imag (x) + imag (y)); } inline complex operator + (const complex& x, double y) { return complex (real (x) + y, imag (x)); }
开头第一段代码就是把这两个+作为非成员函数,程序可以运行,没有错误。下面试试成员函数版本。
//complex.h #ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ #include <iostream> using namespace std; class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } double real() const { return re; } double imag() const { return im; } complex operator + (const complex&); complex operator + (double); private: double re, im; }; inline complex complex::operator + (const complex& y) { cout << "complex complex::operator + (const complex& y)" << endl; return complex(real() + y.real(), imag() + y.imag()); } inline complex complex::operator + (double y) { cout << "complex complex::operator + (double y)" << endl; return complex(real() + y, imag()); } #endif //__MYCOMPLEX__
//complex_test.cpp #include "complex.h" ostream& operator << (ostream& os, const complex& x) { return os << '(' << x.real() << ',' << x.imag() << ')'; } int main() { complex c1(2, 1); complex c2(4, 0); cout << (c1 + c2) << endl; cout << (c1 + 5) << endl; system("pause"); return 0; }
运行结果
把这两个+作为成员函数,程序也可以运行,没有错误。
那为什么+写成非成员函数更好呢?
C++ Primer(中文第五版 第493页)在谈到把重载运算符作为成员函数或非成员函数时,提到“具有对称性的运算符可能转换任意一端的运算对象,例如算术,相等性,关系和位运算符等,因此它们通常应该是普通的非成员函数”。接下来书中提到了string的 operator+的例子来说明这一点。string 的+重载成了非成员函数正是由于对称型运算符可能转换任意一端的对象。
string s = "world"; string t = s + "!"; string u = "hi" + s;
string 的+是连接两个string对象的,就是说左侧和右侧的对象都是string。因此无论+是重载成成员函数还是非成员函数,第二行的表达式 s+"!"都是对的(因为左侧是string, 右侧的const char *可以转换成string)。但是对于 “hi" + s 就不一样了,如果string 的+是成员函数, 就会严格要求+的左侧对象是string,那么这个表达式在运行时就找不到匹配的运算符。因此,显然的,+运算符作为非成员函数比作为成员函数更灵活,因此更好。
在本周课程的complex类里,左数是complex类型的两个+运算符写成非成员函数比写成成员函数更好,我推测正是因为对称性的运算符可能转换运算对象(也许是有其他的原因,但是我目前不知道,以后发现了再修改)。比如当用户想要计算c1(1, 0)和 c2 (2, 1)的和,但忘记输入c1的虚部时,就相当于输入了一个int和一个c2。 如果complex类把两个complex对象相加写成成员函数,int可以转换为double, double则可以转换成complex,这时c1 + c2就可以正常计算,就像上一段中"hi"和s相加发生的转换过程类似。
下面的代码可以验证当complex类把两个complex对象相加使用的+运算符写成非成员函数时,+左侧如果输入int, 该int可转换为complex。
//complex.h #ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ #include <iostream> using namespace std; class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } double real() const { return re; } double imag() const { return im; } private: double re, im; }; inline double imag(const complex& x) { return x.imag(); } inline double real(const complex& x) { return x.real(); } inline complex operator + (const complex& x, const complex& y) { cout << "complex operator + (const complex& x, const complex& y)" << endl; return complex(real(x) + real(y), imag(x) + imag(y)); } #endif //__MYCOMPLEX__
//complex_test.cpp #include "complex.h" ostream& operator << (ostream& os, const complex& x) { return os << '(' << x.real() << ',' << x.imag() << ')'; } int main() { int i = 1; complex c2(2, 1); cout << i + c2 << endl; system("pause"); return 0; }
运行结果
计算int + complex调用了complex + complex的函数,说明存在从int到complex的隐式转换。如果把+写成complex的成员函数,编译无法通过,计算 i+ c2(模仿忘记输入左数的虚部的情况)的时候会显示没有匹配的运算符。