作者:Hub Sutter
译者:plpliuly
/*此文是译者出于自娱翻译的GotW(Guru of the Week)系列文章第3篇,原文的版权是属于Hub Sutter(著名的C++专家,"Exceptional C++"的作者)。此文的翻译没有征得原作者的同意,只供学习讨论。——译者
*/
#4 类的结构(Class Mechanics)
难度:7.5/10
你对定义一个类牵涉到的具体细节熟悉多少?这次GotW不仅讨论一些写一个类时很容易犯的错误,也要讨论怎样使你写的类具有专业风格.
问题:
假设你在看一段代码.其中包含如下一个类定义.这个类定义中有几处风格差劲,还有几处是的的确确的错误.你可以找到几处,该怎样修改?
class Complex {
public:
Complex( double real, double imaginary = 0 )
: _real(real), _imaginary(imaginary) {};
void operator+ ( Complex other ) {
_real = _real + other._real;
_imaginary = _imaginary + other._imaginary;
}
void operator<<( ostream os ) {
os << "(" << _real << "," << _imaginary << ")";
}
Complex operator++() {
++_real;
return *this;
}
Complex operator++( int ) {
Complex temp = *this;
++_real;
return temp;
}
private:
double _real, _imaginary;
};
答案:
序
这个类定义中出现的需要修改或改进的地方远不止我们在下面要提到的几处.我们提出这个问题的目的主要是为了讨论类定义的构成(比如,"<<操作符的标准形式是什么样的","+操作符是不是应该定义为成员函数?"),而不是讨论哪个接口的设计是不是太糟糕.但是,我还是想首先提一下属于后方面的建议,作为第0个值得修改的地方:
0.既然标准库中已经有复数类为什么还要自己定义一个Complex类呢?(而且,标准库中定义的类不会有下面提及的任何一个问题,是由业界中具有多年经验的最优秀的人编写.还是谦虚一点去直接重用这些类吧)
[忠告]尽量直接使用用标准库算法.那会比你自己动手写一个要快捷,而且不易出错.
class Complex {
public:
Complex( double real, double imaginary = 0 )
: _real(real), _imaginary(imaginary) {};
1.风格问题:这个构造函数可以当作单参数函数使用,这样就可能作为一个隐式转换函数使用.但是可能并不是所有时候发生的隐式转换都是你所期望的.
[忠告]当心不易察觉的类型转换操作在你并不希望的时候发生.一个好的避免办法就是:可能的话就用explicit修饰词限制构造函数.
void operator+ ( Complex other ) {
_real = _real + other._real;
_imaginary = _imaginary + other._imaginary;
}
2.风格问题:从效率上讲,参数应该是const&类型,而且"a=a+b"形式的语句应该换成"a+=b"的语句.
[准则]尽量使用const&参数类型代替拷贝传值类型参数.
[忠告]对于算术运算,尽量使用"a op= b"代替"a = a op b".(注意有些类--比如一个别人写的类--可能通过操作符的重载改变了op和op=之间的关系)
3.风格问题:+操作符不应该定义为成员函数.如果被定义成如上述代码中的成员函数,你就只能用"a=b+1"形式的语句而不能用形如"a=1+b"的语句.这样,你就需要同时提供operator+(Complex,int)和operator+(int,Complex)的定义.
[准则]在考虑把一个操作符函数定义为成员函数还是非成员函数的时候,尽量参照下面的原则:(Lakos96:143-144;591-595;Murray93:47-49)
- 一元(单参数)操作符函数应该定义成成员函数
- =,(),[],和->操作符应该定义成成员函数
- +=,-=,/=,*=,(等等)应该定义成成员函数
- 其余的操作符都应该定义成非成员函数
4.错误:+操作符不应该改变对象自身.它应该返回包含相加结果的临时对象.请注意,为了防止类似"a+b=c"的操作,这个操作符函数返回类型应该是"const Complex"(而不仅仅是"Complex").
(实际上,上述的代码更象定义+=操作符函数的代码)
5.风格问题:一般来讲,当定义了op的操作符函数,就应该同时定义了op=操作符.因此,这儿也应该定义+=操作符.上面的代码就应当是+=的定义(但是返回类型需要修改,参看下面的讨论).
void operator<<( ostream os ) {
os << "(" << _real << "," << _imaginary << ")";
}
(注意:对于一个真正的<<操作符,你应该检查stream的当前格式标志以支持一般的用法(译者:iostream通过设置标志来对输出格式进行控制).请参考你喜欢的STL的书籍了解细节)
6.错误:操作符<<不应该定义成成员函数(参看上面讨论),而且参数应该是"(ostream&,const Complex&)".注意,正如James Kanze指出,也尽量不要把它定义成友元函数!最好把它定义成通过调用一个"print"的public成员函数来工作.
7.错误:这个操作符函数应该返回"ostream&",而且应该以"return os"结束.这样就可以支持将多个输出操作链接起来(比如"cout << a << b;").
[准则]操作符<<和>>都应该返回stream的引用.
Complex operator++() {
++_real;
return *this;
}
8.风格问题:前自增应该返回Complex&,以便调用者可以更直接的操作.(译者:其实我觉得这不仅仅是风格的问题.因为按照前自增的标准定义,应该支持"++++a"的语法,而且两次前自增都应该是对a对象的自身操作,如果返回Complex类型,那第二次前自增调用的是临时对象的前自增操作.)
Complex operator++( int ) {
Complex temp = *this;
++_real;
return temp;
}
9.风格问题:后自增应该返回"const Complex".这可以防止形如"a++++"的用法.这句话可不会象某些人想当然那样会连续对a对象作两次自增操作.
10.风格问题:如果通过前自增操作来实现后自增操作符函数将会更好.(译者:将"++_real;"替换为"++(*this);")
[准则]尽量通过前自增操作来实现后自增操作.
private:
double _real, _imaginary;
};
11.风格问题:尽量避免使用以下划线开头命名变量.我曾经也很习惯这样定义变量,就连著名的"Design Patterns"(Gamma et al)中也是这样.但是因为标准库的实现中保留了很多下划线开头标识符,如果我们要用下划线开头定义自己的变量就得记住全部已经保留的标识符以免冲突,这太难了.(既然使用下划线作为成员变量的标志容易跟保留标识符冲突,那我就在变量结尾加下划线)
好了.最后,不考虑其他一些上面没有提到的设计和风格上的缺陷,我们可以得到下面的经过修正的代码:
class Complex {
public:
explicit Complex( double real, double imaginary = 0 )
: real_(real), imaginary_(imaginary) {}
Complex& operator+=( const Complex& other ) {
real_ += other.real_;
imaginary_ += other.imaginary_;
return *this;
}
Complex& operator++() {
++real_;
return *this;
}
const Complex operator++( int ) {
Complex temp = *this;
++(*this);
return temp;
}
ostream& print( ostream& os ) const {
return os << "(" << real_
<< "," << imaginary_ << ")";
}
private:
double real_, imaginary_;
friend ostream&
operator<<( ostream& os, const Complex& c );
};
const Complex operator+( const Complex& lhs,
const Complex& rhs ) {
Complex ret( lhs );
ret += rhs;
return ret;
}
ostream& operator<<( ostream& os,
const Complex& c ) {
return c.print(os);
}
-----
(结束)