【c++】条款3:尽可能使用const

1、 const的一件奇妙事情是,它允许你指定一个语义约束(也就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束。它允许你告诉编译器和其他程序员某值应该保持不变,只要这是事实,你就该确实说出来,因为说出来可以获得编译器的相助,确保这条约束不被违反。
2、关键字const多才多艺,你可以在classes外部修饰global或namespace作用域中的常量、或修饰文件、函数、或区块作用域中被声明为static的对象。你也可以用它修饰classes内部的static和non-static成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都是const

char greeting[] = "Hello";
char* p = greeting;
const char* p = greeting;
char* const p = greeting;
const char* const p = greeting;

3、const语法虽然变化多端,但并不莫测高深。如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。如果被指物和指针两者都是常量。如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数类型是一样的。

void f1(const Widget* pw);
void f2(Widget const * pw);

两种形式都有人用,你应该试着去习惯它们。
4、STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针,声明
迭代器为const就像声明指针为const一样(即声明为一个T* const指针),表示这个迭代器不得不指向不同的东西,但它所指的东西的值是可以改动的。如果你希望迭代器所指的东西不可被改动,所以你需要的是const_iterator

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter;
std::vector<int>::const_iterator citer=vec.begin();
*citer = 10;
++citer;

5、const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身。(如果是成员函数)产生关联。令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数的operator*声明式:

class Rational{...};
const Rational operator*(const Rational& lns,const Rational& rhs);

这里为什么返回一个const对象,原因是如果不这样客户就能实现这样的暴行:

Rational a,b,c;
...
(a*b)=c;

这样的错误也许是无意中的,但这明显是不符合正常语法的。

ifa*b=c)

如果a和b都是内置类型,这样的代码直截了当就是不合法。而一个“良好的用户自定义类型”的特征是它们避免无端的与内置类型不兼容,因此允许对两值乘积作赋值动作也就没有意思了。将operator*的回传值声明为const可以预防那个“没意思的赋值动作”,那就是该那么做的原因了。至于const参数,没有什么特别新颖的概念,它们不过就像local const对象一样,你应该在必要的使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。只不过多打六个字符,却可以避免恼人的错误,像是“==”写成“=”的错误,一如稍早所述。
6、const成员函数:
将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上,这一类成员函数之所以重要,基于两个理由,第一,它们使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而那个函数不行,很是重要。第二,它们使“操作const对象”称为可能。这对编写高效代码是个关键,改变c++程序效率的一个根本方法就是以引用方式传递对象,而此技术可行的前提是,我们有const成员对象可用来处理取得的const对象。
许多人漠视一个事实:两个成员函数如果只是常量性不同,可以被重载。这实在是一个重要的c++特性。考虑以下class,用来表现一大块文字,

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];
const TextBlock std("world");
std::cout<<ctb[0];

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

void print(const TextBlock& ctb)
{
    std::cout<<ctb[0];
    ...

}

只要重载operator[]并对不同的版本给予不同的返回类型,就可以令const和non-const TextBlocks获得不同的处理

std::cout<<tb[0];
tb[0]='x';
std::cout<<ctb[0];
ctb[0]='x';

注意上述错误只因operator[]调用动作自身没问题,错误起因于企图对一个“由const版之operator[]”返回的const char&类型实施赋值动作。也请注意,non-const operator[]的返回类型是个reference to char,不是char,下面这样的句子就无法通过编译tb[0] = ‘x’;那是因为,如果函数的返回类型是个内置类型,那么改动函数返回值从来就不合法,纵使合法,c++以by value返回对象这一事实意味着被改动的·其实是tb.text[0]的一个副本,不是tb.text[0]自身,那不会是你想要的行为。
7、让我们为哲学思辨喊一次暂停。成员函数如果是const意味着什么?这有两个流行概念,bitwise constness和logical constness
bitwise const阵营的人相信,成员函数只有在不更改对象之任何成员变量时才可以说是const。也就是它不更改对象中的任何一个bit这样的好处是很容易侦测违反点:编译器只需要寻找成员变量的赋值动作即可。bitwise constness正是c++对常量性的定义,因此成员函数不可以更改对象内任何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
    {
        return pText[position];

    }
    private:
    char* pText;
};

这个class不适当地将operator[]声明为const成员函数,而该函数却返回一个reference指向对象内部值。假设暂时不管这个事实,请注意,operator[]实现代码并不更改pText.于是编译器很开心的为operator[]产出目标码。它是bitwise const,所有编译器都这么认定。但是看看它允许发生什么事。

const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';

这其中当然不该有任何错误:你创建一个常量对象并设以某值,而且只对它调用
const成员函数。但你终究还是改变了它的值。
这种情况导出所谓的logical constness.这一派拥护者主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才的如此。例如你的CTextBlock class有可能高速缓存(cache)文本区块的长度以便应付询问:

class CTextBlock
{
    public:
    ...
    std::size_t length(0 const;
    private:
    char* pText;
    std::size_t textLength;
    bool lengthIsValid;
};
std::size_t CTextBlock::length()const
{
    if(!lengthIsValid)
    {
        textLength=std::strlen(pText);
        lengthIsValid = true;
    }
    return textLength;
}

8、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;
    mutable bool lengthIsValid;
};
std::size_t CTextBlock::length()const
{
        if(!lengthIsValid)
        {
            textLength = std::strlen(pText);
            lengthIsValid = true;

        }
        return textLength;
}

在const和non—const成员函数中避免重复:
对于“bitwise-constness非我所欲”的问题,mutable是个解决办法,但它不能解决所有
的const相关难题。举个例子,假设TextBlock内的operator[]不单只是返回一个reference指向某字符,也执行边界检验、志记访问信息、甚至可能进行数据完善性检验,把所有这些同时放进const和non-const operator[]中,导致这样的怪物:

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;
}

你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等令人头疼的问题吗?当然啦。将边界检验……等所有代码移到另一个成员函数(往往是个private)并令两个版本的operator[]调用它,是可能的,但你还是重复了一些代码,例如函数调用、两次return语句等等。
你真正该做的是实现operator[]的机能一次并使用它两次。也就是说,你必须
令其中一个调用另一个,这促使我们将常量性排除就一般守则而言,转型是一个糟糕的想法,我将贡献一整个条款来谈这码事,告诉你不要这样做,然而代码重复也不是什么令人愉快的经验。本例中const operator[]完全做掉了non-const版本该做的一切,唯一的不同是其返回类型多了一个const资格修饰。这种情况下如果将返回值的const转除是安全的,因为不论谁调用
non-const operator[]都一定首先有个non-const对象,否则就不能调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免代码重复的安全做法-即使过程中需要一个转型动作。下面是代码,稍后有更详细的解释:

class TextBlock
{
    public:
    ...
    const char& operator[](atd::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兄弟,但non-const operator[]内部若只是单纯调用operator[],
会递归调用自己。那会大概进行一百万次,为了避免无穷递归,我们必须明确指出调用得是const operator[],但c++缺乏直接的语法可以这么做。因此这里将*this从其原始类型TextBlock&转型为const TextBlock.是的,我们使用转型为它加上const!所以这样共有两次转型,第一次用来为*this添加const(这使接下来调用operator[])
时得以调用const版本),第二次则是从const operator[]的返回值中移除const添加const的那一次转型强迫进行了一次安全转型,所以我们使用static_cast.移除const的那个动作只可以藉由const_cast完成,没有其他选择(就技术而言其实是有的:一个C-style转型也行的通,那种转型很少是正确的抉择。如果你不熟悉static_cast或const_cast)至于其他动作,由于本例调用的是操作符,所以语法有一点点奇特,恐怕无法赢得选美大赛,但却有我们渴望的“避免代码重复”效果,因为它运用const operator[]实现出non-const 版本。为了到达那个目标而写出如此难看的语法
是否值得,只有你能决定,但“运用const成员函数实现出其non-const 孪生兄弟”的技术是值得了解的。更值的了解的是,反向做法—-令const版本调用non-const版本以避免重复—并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内调用mon-const_cast函数,就是冒了这样的风险,你曾经承诺不改动的那个对象被改动了。这就是为什么“const成员函数调用non-const成员函数”是一种错误行为:因为对象有可能因此被改动。实际上若要令这样的代码通过编译,你必须使用一个const_cast将*this身上的const性质解放掉,这是乌云罩顶的清洗前兆,反向调用才是安全的,non-const成员函数本来就可以对其对象作任何动作,
所以在其中调用一个const成员函数并不会带来风险。这就是为什么本例以static_cast作用于*this的原因:这里并不存在const相关风险。
9、本条款一开始就提醒你,const是个奇妙而非比寻常的东西。在指针和迭代器上
;在指针、迭代器及references指涉的对象身上:在函数参数和返回类型身上;
在local变量身上;在成员函数身上,林林总总不一而足。const是个威力强大的
助手。尽可能使用它。你会对你的作为感到高兴。
请记住:
(1)将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象,
函数参数、函数返回类型、成员函数本体。
(2)编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”
(3)当const和non-const成员函数有着实质等价的实现时,令non-const 版本调用const版本
可避免代码重复。

猜你喜欢

转载自blog.csdn.net/flowing_wind/article/details/80575435
今日推荐