Effective C++条款03——让自己习惯C++(尽可能使用const)

const 的一件奇妙事情是,它允许你指定一个语义约束(也就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。只要这(某值保持不变)是事实,你就该确实说出来,因为说出来可以获得编译器的襄助,确保这条约束不被违反。

关键字const多才多艺。你可以用它在classes外部修饰global或namespace(见条款2)作用域中的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。你也可以用它修饰classes内部的static和non-static成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是const:

char greeting[] = "Hello";             
char* p = greeting;                    // 非const指针,非const数据
const char* p = greeting;              // 非const指针,const数据
char* const p = greeting;              // const指针,非const数据
const char* const p = greeting;        // const指针,const数据

const语法虽然变化多端,但并不莫测高深。如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数类型是一样的:

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

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

std::vector vec;
// ...
const std::vector::iterator iter =         // iter的作用像个T* const
    vec.begin();
*iter = 10;                                     // 正确,可以修改指向的值
++iter;                                         // 错误,不能修改指针本身,指针是const

std::vector::const_iterator cIter =        // cIter的作用像个const T*
    vec.begin();
*cIter = 10;                                    // 错误,指针指向的值不可修改
++cIter;                                        // 正确,指针本身可以修改

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

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

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

许多程序员第一次看到这个声明时不免斜着眼睛说,唔,为什么返回一个const对象?原因是如果不这样客户就能实现这样的暴行:

Rational a, b, c;
// ...
(a * b) = c;                // 在a*b的成果上调用operator=

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

if (a * b = c) 
// ...

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

清晰、明了

const成员函数

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使class 接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C++程序效率的一个根本办法是以pass byreference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。

许多人漠视一件事实:两个成员函数如果只是常量性( constness)不同,可以被重载。这实在是一个重要的C++特性。考虑以下class,用来表现一大块文字:

class TextBlock {
public:
    // ...
    const char& operator[](std::size_t position) const    // const对象
    { return text[position]; }
    char& operator[](std::size_t position)                // 非const对象
    { return text[position]; }

private:
    std::string text;
};

TextBlock的operator[]可被这么调用

TextBlock tb("hello");
std::cout << tb[0];              // 调用非const的operator
const TextBlock ctb("world");
std::cout << ctb[0];             // 调用const的operator

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

void print(const TextBlock& ctb)        // 此ctb是const
{
    std::cout << ctb[0];                // 调用的是const的operator
    // ..
}

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

std::cout << tb[0];                // 没问题
tb[0] = 'x';                       // 没问题
std::cout << ctb[0];               // 没问题
ctb[0] = 'x';                      // 有问题,写一个const

注意,上述错误只因operator[]的返回类型以致,至于operator[]调用动作自身没问题。错误起因于企图对一个“由const版之operator[]返回”的cornst char&施行赋值动作。

也请注意,non-const operator[]的返回类型是个reference to char不是char。如果operator[]只是返回一个char,下面这样的句子就无法通过编译:

tb[0] = 'x';

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

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

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

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

private:
    char* pText;
};

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

const CTextBlock cctb("hello");        // 声明常量对象
char* pc = &cctb[0];                   // 调用const operator[]

*pc = 'J';                             // cctb现在是"Jello"

这其中当然不该有任何错误:你创建一个常量对象并设以某值,而且只对它调用const成员函数。但你终究还是改变了它的值。

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

class CTextBlock {
public:
    // ...
    std::size_t length() const;

private:
    char* pText;
    std::size_t textLength;
    bool lengthIsValid;
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);            // 错误,在const函数中赋值
        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;                 // 加mutable
    mutable bool lengthIsValid;
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);            // 正确
        lengthIsValid = true;
    }
    return textLength;
}

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

就是两个函数实现一模一样,差了个const修饰,代码重复了。

const类型的函数正常写,非const函数需强转,使用const_cast<>去掉const属性(反向不能)

class TextBlock {
public:
    // ...

    const char& operator[](std::size_t position) const
    {
        // 正常写

        return text[position];
    }

    char& operator[](std::size_t position)
    {

        return
            const_casr(
                static_cast(*this)
                    [position]
            );
    }
};

请记住

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

你可能感兴趣的:(Effective,C++,c++,开发语言,Effective,C++,学习,keep,studying)