条款20: GotW#4 CLASS技术(Class Mechanics)

本文深入探讨了一个C++复数类的错误与不当设计,并提供了详细的修正方案。从避免隐式转换到提高运算符效率,文章覆盖了成员函数与非成员函数的选择、运算符重载的正确实现方式,以及如何遵循设计准则以达到代码的高效、清晰与安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

假如你正在检阅程式码,有位程序员写下这样的一个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);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值