Effective C++读书笔记之三:尽可能使用const

Item 03:Use const whenever possible

如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被植物和指针两者都是常量。

STL迭代器以指针为根据塑造出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样,如:
std::vector<int>vec;
const std::vector<int>::iterator iter = vec.begin();//iter的作用就像一个T*const
std::vector<int>::const iterator cIter = vec.begin();//cIter的作用就像一个const T*

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
举个例子,考虑有理数operator*的声明式:
class Rational{......};
const Rational operator* (const Rational & lhs,const Rational & rhs);

如果不返回一个const对象客户就可能实现这样的暴行:
Rational a,b,c;
...
(a*b)=c; //在a*b的结果上调用operator =

很多程序员可能在无意中这么做,只因为单纯的打字错误:

if(a*b=c)...//其实只是想做一个比较动作!

一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容,因此允许对两值乘积做赋值动作也就没什么意思了。将operator*的回传值声明为const可以预防那个“没意思的赋值动作”。


const 成员函数

将const实施于成员函数的目的,是为了确认该成员函数可以作用于const对象。

许多人漠视一件事实:两个成员函数如果只是常量性不同,可以被重载,考虑如下一段代码:
class TextBlock
{
public:
	...
	const char & operator[](std::size_t position) const//operator[]for const 对象
	{
		return text[position];
	} 
	char & operator[](std::size_t position)//operator[]for non-const对象 
	{
		return text[position];
	}
private:
	std::string text;
}
TextBlock的operator[]可以被这么用:

TextBlock tb("Hello");
std::cout<<tb[0];//调用non-const TextBlock::operator[]

const TextBlock ctb("World");//调用const TextBlock::operator[]
std::cout<<ctb[0];

附带一提,真实程序中const对象大多用于passed by pointer to constpassed by reference to const的比较结果。上述ctb的例子太过造作,下面这个比较真实:
void print (const TextBlock &ctb)
{
	std::cout<<ctb[0];
	... 
}
只要重载operator[]并对不同的版本给予不同的处理返回类型,就可以令const和non-const获得不同的处理。

请注意,non const operator[]的返回值类型是一个reference to char,不是char。否则下面的句子就无法通过编译:

tb[0]='x';

这是因为,如果函数的返回值是个内置类型,那么改动函数返回值从来就不合法。纵使合法,C++以by value返回对象这一事实意味着被改动的其实是tb.text[0]的一个副本,不是其自身,那不是你想要的行为。

成员函数为const意味着什么?这有两个流行的概念:bitwise constness 和 logical constness

bitwise constness阵营的人相信,成员函数只有在不更改对象之任何成员变量时才可以说是const。然而不幸的是很多成员函数虽然不十分具备const性质却能通过bitwise测试。具体地说,一个更改了“指针所指物"的成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise constness 不会引发编译器异议。如下:

class CTextBlock
{
public:
	...
	char& operator[](std:size_t position) const//bitwise constness声明,但其实并不妥当
	{
		return pText[position];
	} 
private:
	char* pText;	
};

这个class不恰当地将其operator[]声明为const成员函数,而函数却返回一个reference指向对象的内部值。由于operator[]的实现并不更改pText,于是编译器很开心地认为它是bitwise constness,但是看看它会发生什么事:

const CTextBlock cctb("Hello");
char* pc=&cctb[0];//调用const operator[]取得一个指针,指向cctb的数据
                 //我认为这里应该是char*pc =cctb[0],欢迎大家一起讨论

*pc='J';//cctb 现在有了"Jello"这样的内容 

你创建一个常量对象并赋予初值,而且只对它调用const成员函数。但你终究还是改变了它的值。

这就是所谓的logical constness。这一派的拥护者主张,一个const成员函数可以修改它所处理对象内的某些bit,但只有在客户端侦测不出的情况下才得如此。例如你的CTextBlock class可能高速缓存文本区块的长度以便应付询问:

class CTextBlock
{
public:
	...
	std::size_t length() const;
private:
	char *pText;
	std::size_t textLength; //最近一次计算的文本区块长度
	bool lengthIsValid;   //目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
	if(!lengthValid)
	{
		textLength = std::strlen(pText);
		lengthIsValid = true; //错误!在const成员函数内不能赋值给textLength和lengthIsValid 
	}
	return textLength;
}

length修改了textLength和lengthIsValid,这两笔数据被修改对const CTextBlock来说可以接受,但是编译器不同意,它坚持bitwise constness,怎么办呢?解决方法很简单:利用C++写一个与const相关的摆动场:mutable。mutable释放掉non-static成员变量的bitwise constness约束:

class CTextBlock
{
public:
	...
	std::size_t length() const;
private:
	char *pText;
	mutable std::size_t textLength; 
	mutable bool lengthIsValid;   //这些成员变量可能总是会被修改,即使是在const成员函数内 
};
std::size_t CTextBlock::length() const
{
	if(!lengthValid)
	{
		textLength = std::strlen(pText);//现在,可以这样 
		lengthIsValid = true; //也可以这样 
	}
	return textLength;
}

在const和non-const成员函数中避免重复

对于”bitwise-constness 非我所欲”的问题,mutable是个解决办法,但它不能解决所有的const相关难题。假设TextBlock内的operator[]不单只是返回一个reference指向某字符,也执行边界检验、志记访问信息、甚至可能进行数据完善性检验。如下:

class TextBlock
{
public:
	...
	const char& operator[](std::size_t position) const
	{
		...   //边界检验
		...   //志记数据访问
		...   //检验数据完整性
		return text[position]; 
	}
	char& operator[](std::size_t position)
	{
		...   //边界检验
		...   //志记数据访问
		...   //检验数据完整性
		return text[position]; 
	}
private:
	std::string text;
};

你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等令人头疼的问题吗?你真正应该做的是实现operator[]的机能一次并实现它两次。这促使我们将常量性移除。如下:

class TextBlock
{
public:
	...
	const char& operator[](std::size_t position) const
	{
		...   //边界检验
		...   //志记数据访问
		...   //检验数据完整性
		return text[position]; 
	}
	char& operator[](std::size_t opsition)
	{
		return
			 const_cast<char&>(//将op[]返回值的const移除 
			 	static_cast<const TextBlock&>(*this)//为*this加上const 
				 	[position]);//调用const op[] 
		
	}
};

这份代码有两个转型动作。我们打算让non-const operator []调用其const兄弟,但non-const operator[]内部若只是单纯调用operator[],会递归调用自己。为了避免无穷递归,我们必须明确指出调用的是const operator[]。因此这里将*this从其原始类型TextBlock&转换为const TextBlock&,这使接下来调用operator[]时得意调用const版本。第二次则是从const operator []的返回值中移除const。

至于反向做法——令const版本调用non-const版本以避免重复——那并不是你应该做的事。因为const成员函数承诺绝不改变其对象的逻辑状态而non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象呗改动了。

请记住:
1.将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
2.编译器强制实施bitwise constness,但你编写程序是应该使用“概念上的常量性”。
3.当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值