Effective C++读书笔记之二

条款3:尽可能使用const

      首先来看const的多才多艺:

        * char greeting[] = "Hello" ;

        * char* p = greeting ;  // non-const pointer,non-const data

        * const char* p = greeting // non-const pointer,const data

        * char* const  p = greeting // const pointer,non-const data

        *const char* const p = greeting // const pointer,const data

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

         注意,以下两种写法是一样的:

         void f1(const Widget* pw) ;

         void f2(Widget const* pw) ;

         STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西是可以改变的。如果你希望迭代器所指的东西不可改动(即希望STL模拟一个const T*指针),你需要的是const_iterator ;

std::vector<int> vec ;
...
const std::vector<int>::iterator iter = vec.begin() ;// iter的作用就像个T* const
*iter = 10 ;// 没问题,改变iter所指物
++iter ; // 错误,iter是const
std::vector<int>::const_iterator cIter = vec.begin() ;// cIter的作用像个const T*
*cIter = 10 ;// 错误,*cIter是const
++cIter ; // 没问题,改变从cIter

        const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。

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

class Rational{......} ;
const Rational operator*(const Rational& lhs, const Rational& rhs) ;
        许多程序员第一次看到这个声明时不免斜着眼说为什么要返回一个const对象?原因是如果不这样,客户就能是现在这样的暴行:


        Rational* a, b, c ;

        ...

        (a * b) = c ;// 在 a * b 的成功品上调用operator=

        我不知道为什么会有人想到对两个数字的乘积再做一次赋值,但我知道许多程序员会在无意识中那么做,只因为单纯的打字错误(以及一个可被隐式转换为bool的类型):

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

        如果a和b都是内置类型,这样的代码直截了当就是不合法。而一个“良好的用户自定义类型”的特征是他们避免无端地与内置类型不兼容,因此允许对两值乘积做赋值动作也就没有什么意思了。将operator*的回传值声明为const可以预防那个“没意思的赋值动作”,这就是该那么做的原因。

       对于函数的参数,除非你有需要改动参数或local对象,否则请把它们声明为const。虽然多写了6个字符,但是却可以省下恼人的错误,想是 想键入 ==确意外键成=的错误。

        const成员函数基于以下两个理由导致它的存在:

        * 它使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。

        * 它们使操作 const 对象成为可能。这对编写高效代码是个关键,因为改善C++程序效率的一个根本办法是以 pass by reference-to-const方式传递对象,而此技术是可行的前提是,我们有const成员函数可用来处理取得的const对象。

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 ;
};
TextBlock的operator[]s可以被这么使用:

TextBlock tb("Hello") ;

std::cout<<  tb[0]// 调用non-const TextBlock::operator[]

const TextBlock ctb("World") ;

std::cout << ctb[0] ;// 调用的是const TextBlock::operatror[]

       附带一提的是,这是程序中const对象大多用于passed by pointer-to-const 或 passed by reference-to-const的传递结果。上述的ctb例子太过造作,下面这个比较真实:

void print(const TextBlock& ctb)
{

std::cout << ctb[0] ;// 调用const TextBlock::operator[]

}

        让我们为哲学思辨再一次暂停。成员函数如果是const意味什么呢?这有两个流行概念bitwise constness和logical constness。

         bitwise const 阵营的人相信,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是说它不更改对象内的任何一个bit。这种论点的好处就是很容易侦测违反点:编译器只需寻找成员变量的动作即可。bitwise constness正是C++对常量性的定义,因此 const成员函数不可以更改对象内任何non-static成员变量。

          不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但 如果只有指针(而非其所指物)隶属于对象,那么称此函数 为bitwise const不会引发编译器异议。这导致反直观结果。假设我们有一个TextBlock-like class,它将数据存储为char*而不是string,因为它需要和一个不认识string对象的C API沟通:

class CTextBlock
{
public:
	char& operator[](std::size_t position)const // bitwise const声明
	{
		return pText[position] ;             // 但其实不适当
	}
private:
	char* pText ;
};
        这个class不适当地将其operator[]声明为const成员函数,而该函数却返回一个reference指向对象内部值。假设暂时不管这个事实,请注意,operator[]实现代码并不更改pText。于是编译器很开心地认为operator[]产生目标码。它是bitwise const,所有编译器都这么认为。但看看它允许发生什么事:


const CTextBlock cctb("Hello") ;// 声明一个常量对象。

char* pc = &cctb[0] ;// 调用const operator[]取得一个指针指向cctb的数据。

*pc = 'J';// cctb现在有个“Jello”这样的内容。这是错误,会报错。

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


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

      length的实现当然不是bitwise const,因为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 ;// 这些成员变量可能总是会被更改,即使在const成员函数内
	mutable bool lengthIsValid ;
};
std:size_t CTextBlock::length()
	{
		if (!lengthIsValid)
		{
			textLength = std::strlen(pText) ;// 现在可以这样,
			lengthIsValid = true ;// 也可以这样。
		}
		return textLength ;
	}

        有的时候,当一个函数会有两个版本(const和non-const)是,应尽量避免代码的重复,换句话说是要尽量利用代码的共享,例如,用一个函数的const版来调用non-const版,可尽量节省代码的用量:

class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
...
return TEXT[position] ;
}
};
char& operator[](std::size_t position)
{
	return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]) ;
}


        在这里大家可以看到,non-const类型的operator函数调用const类型的operator函数。

        就一般守则而言,转型(casting)是一个糟糕的想法,尽量避免转型。但咋本例中,const operator[]完全做掉了non-const版本该做的一切,唯一的不同是其返回类型一个const资格修饰。在这种情况下将返回值的const转除是安全的,因为不论谁调用non-const operator[]都一定首先有个non-const对象,否则就不能调用non-const函数。所以non-const operator[]调用其const兄弟是一个避免代码重复的安全做法--即使过程中需要一个转型动作。

         如你所见,这份代码有两个转型动作,而不是一个。我们打算让non-const operator[]调用其const兄弟,但non-const oper[]内部若只是单纯调用operator[],会递归调用自己。那会大概...唔...进行一百万次。为了避免无穷递归,我们必须明确指出调用的是const operator[],但C++缺乏直接的语法可以那么做。因此这里将*this从其原始类型TextBlock&转型为const TextBlock&。是的,我们使用转型操作为它加上const。所以这里共有两次转型:第一次用来为*this添加const,第二次则是从const operator[]的返回值中移除const。

         添加const的那一次转型是安全的,所以我们使用static-cast。移除const的那个动作只可以由const_cast完成,没有其他选择(就技术而言其实是有的;一个C-style转型也行得通,但那种转型很少是正确的选择)。
             更值得了解的是,反向做法---令const版本调用non-const版本以避免重复---并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内部调用了non-const函数,就是冒了这个风险:你曾经承诺不改动的那个对象被改动了。这就是为什么“const 成员函数调用non-const成员函数”是一种错误行为:因为对象有可能因此而被改动。实际上若要令这样的代码通过编译,你必须使用一个const_cast将*this身上的const性质解放掉,这是乌云罩顶的清晰前兆。反向调用(也就是我们先前使用的那个)才是安全的:non-const成员函数本来就可以对其对象做任何动作,所以其中调用一个const成员函数并不会带来风险。这就是为什么本例以static_cast作用于*this的原因:这里并不存在const相关风险。

        注意:const函数调用non-const函数是非常危险的,但non-const变量转化为const类型是非常安全的,用static-cast。

                     在这里请注意,函数和变量有两种不同的说法。为了避免混淆,现总结如下:non-const函数调用const函数,non-const变量转化为const变量,移除const特性需借用const_cast。

请记住:

          * 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。

          * 编译器强制实施bitwise-constness,但你编写程序时应该使用“概念上的常量性”。

          * 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

 





 




 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值