假如你正在检阅程式码,有位程序员写下这样的一个class,其中有些不好的风格,还有些错误。你可以找出多少个?哟能力修正它们吗?
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;
};
【解答】
这个class有许多问题,甚至比我将明白告诉你的还多。此题重点主要强调class的写作技术(像是operator<<的标准型式是什么?以及operator++应该成为一个member吗?之类的问题),而不在强调其介面设计的贫乏。
不过我还是要以一个或最有用的评语启开序幕:既然标准库已经有一个complex class,我们何必再写一个Complex class?标准库的那个版本并不为以下版本所苦,它由工业最顶尖的人选花费数年完成的。在标准库面前应该保持谦逊,并尽量复用它。
设计准则:尽量重用代码——特别是标准库,而不是老是想着自己撰写。这样不但比较快,也比较容易,比较安全。
或许,更正上述Complex的最佳方法就是避免使用这个class,改用std::complex template。话虽这么说,还是让我们仔细看整个class并修正其中的错误吧,首先是constructor:
1.以下这个constructor允许发生隐式转换(implicit conversion)。
Complex(double real,double imaginary = 0)
:_real(real),_imaginary(imaginary)
{}
由于第二个参数是默认值,此函数可被视为单一参数的constructor,并允许一个double转换为一个Complex。在本例中这也许是好的,但一如我们在条款6所见,这样的转换可能并非总是想要的。一般而言让你的constructors成为一个explicit是个好主意,除非你审慎地决定允许隐式转换(关于隐式转换在条款19条有更多说明)。
设计准则:小心隐式转换所带来的隐藏的临时对象,避免这东西的的一个好办法就是尽可能让constructors成为explicit,并避难编写转换操作符。
2.operator+效率不高。
void operator+(Complex other)
{
_real = _real + other._real;
_imaginary = _imaginary + other._imaginary;
}
为了提高效率,参数应该以by reference to const 的方式传递。
设计准则:尽量以by const&(而非by value)的方式传递。
此外,在上述两行中,“a = a + b”应该重新写为“a += b”。这样做并不会带给你大量的效率提升(在本例中),因为我们只是对double进行加法而已,如果是类类型的进行加法,效率的改善十分明显。
设计准则:尽量写“a op= b;”而不要写成“a = a op b;”(其中op代表任何运算符)。这样不比较清楚,通常也比较有效率。
为什么operator+=比较有效率呢?因为它直接作用于左边的对象进行操作,并且传回一个reference,而不是临时对象。至于operator+则必须传回一个临时对象。要知道为什么,请考虑如下的标准库的实现:
//对象类型是T
T& T::operator+=(const T& other)
{
//...
return *this;
}
const T operator+(const T& a,const T& b)
{
T temp(a);
temp += b;
return temp;
}
注意运算符+和+=的关系,前者应该根据后者来实现的,这样不但可以简化程序(代码比较容易编写),也符合一致性(这两个操作符将做相同的事而且在维护过程中大可能出现分歧)。
设计准则:如果你提供了某个运算符的标准版(例如operator+),那么应该提供相同操作符的赋值版本(例如,operator+=),并且选择后者来实现前者。同时也请总是保存op和op=之间的自然关系(其中op代表任何的运算符)。
3.operator+不应该是member function。
void operator+(Complex other)
{
_real = _real + other._real;
_imaginary = _imaginary + other._imaginary;
}
如果operator+是一个member function,一如本例说的,那么当你决定允许其他类型隐式转换成Complex时,operator+可能无法以很自然的形式工作。具体的说,当你欲对Complex objects加上数值时,只能写“a = b + 1.0”而无法写成“a = 1.0 + b”。因为member operator+要求以一个Complex(而非一个const double)作为其左值。如果你希望你的使用者能够很方便的位Complex objects加上doubles,提供两个重载版本的的operator+(const Complex&,double)和operator+(double,const Complex&)是合理的想法。
设计准则:使用以下准则决定一个运算符是member function或应该是nonmember function:
- 一元运算符应该是members。
- = () [] 和->必须是members。
- assignment版的运算符(+= -= /= *=)都必须是members。
- 其他所有二元运算符都应该是nonmembers。
4.operator+不应该修改this的值,它应该传回一个临时对象,内含相加总和。
void operator+(Complex other)
{
_real = _real + other._real;
_imaginary = _imaginary + other._imaginary;
}
注意临时对象应该是“const Complex”(而非仅是Complex),以避免这样的使用“a + b = c”。
5.如果你定义了op,也应该定义op=。本例之中你应该定义出operator+=,因为你定义了operator+,并应该以前者来实现后者。
6.operator<<不应该成为一个member function。
void operator<<(ostream os)
{
os << "(" << _real << "," << _imaginary << ")";
}
请在看一次operator+的类型讨论。此外,参数应该是(ostream&,const Complex&)。注意,nonmember operator<<通常应该以一个member function(往往是virtual)为基础实现。后者负责真正的工作,常取名为Print()。
此外,对于一个真正的operator<<,你应该做的某些事情,像是检查stream的格式化标记。关于这一部分,请查看你喜欢的标准库方面或iostreams的书籍。
7.更深一层,operator<<的传回类型应该是“ostream&”并应该传回一个reference,代表stream,以便允许串链式输出动作。运用这种方式,使用者便可在程序中以极自然的方式像“cout << a << b;”这样地使用你的operator<<。
设计准则:总是在operator<<和operator>>函数中返回stream references。
8.前置递增返回类型错误。
Complex operator++()
{
++real;
return *this;
}
前置递增应该传回一个reference to non-const。本例应该是Complex&。这使程序代码的操作更加直觉,并避免不必要的效率损耗。
9.后置递增返回类型错误。
Complex operator++(int)
{
Complex temp = *this;
++_real;
return temp;
}
后置递增应该传回一个const值——本例而言应该是const Complex。一旦不允许返回值修改,我们便可以阻止像“a++++”这样的问题代码。因为那样的动作结果并不如一个天真的使用者所想象。
10.后置递增应该以前置递增来实现。对于后置递增的规范形式见条款6。
程序准则:为了一致性,请总是以前置递增来实现后置递增。某则用户会很惊讶结果,并往往令人不高兴。
11.避免使用保留名。
private:
double _real,_imaginary;
是的,十分普及的书籍如Design Patterns的确在变量前面加下划线,但是请不要这么做。标准库中保留了某些以下划线开头的标识符给编译器使用,其中的规则难以记住——对你以及对编译器撰写者而言,所以你最好完全的避免使用下划线。我比较喜欢的命名规则是为member加上一个后下划线。
这就是全部的讨论。以下是一份修正后的版本,其中并未涵盖先前没有指出的设计和程序风格。
class Complex
{
public:
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_;
};
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);
}