美文网首页C++程序员技术文
Item 03:尽可能使用const

Item 03:尽可能使用const

作者: 0d339158f1a6 | 来源:发表于2016-04-11 16:21 被阅读180次
    苏芳

    Item 03: Use const whenever possible


    关键字const

    const允许你指定一个语义约束(也就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束。如果你想让某值保持不变,就应该明确说出来,因为说出来就可以获得编译器的帮助,确保这条约束不被违反。

    关键字const多才多艺。你可用它在classes外部修饰globalnamespace作用域中的常量,或修饰文件函数、或区块作用域中被声明为static的对象。你也可以用它修饰classes内部的staticnon-static成员变量。面对指针,你也可以指出指针自身指针所指物,或两者都(或都不)是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);        //f1获得一个指针,指向一个常量的Widget对象
    void f2(Widget const *pw);        //f2也是
    

    两种形式都有人用,你应该试着习惯它们。

    const面对STL迭代器

    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
    
    const std::vector<int>::const_iterator cTter = vec.begin();     //cIter的作用像个const T*
    *cTter = 10;                                                    //错误!*cIter是cosnt
    ++cTter;                                                        //没问题,改变cIter。
    

    const面向函数声明

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

    函数返回值

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

    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) ...            //其实是想做一个比较动作!
    

    如果a和b都是内置类型,这样的代码就是不合法。而一个“良好的自定义类型”的特征是它们应该和内置类型兼容,因此允许对两值乘积做赋值动作就没什么意思,将operator*的回传值声明为const就可以预防那个“没意思的赋值动作”。

    函数形参

    对于const参数,它们不过就像local const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const,只不过多打6个字符而已。

    const成员函数

    将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这类成员之所以重要,两个理由:

    • 它们使class接口比较容易被理解,因为,得知哪个函数可以改动对象内容而哪个不函数不行,这一点是非常重要的。
    • 它们使“操作const对象”成为可能。

    两个成员函数如果只是常量性不同,可以被重载

    基于成员函数是否为const,可以重载一个成员函数;同样地,基于一个指针形参(或者引用形参)是否为指向const,可以重载一个函数。const对象只能使用const成员。非const对象可以使用任一成员,但非const版本是一个更好的匹配
    ————《C++ Primer》第四版 P442

    bitwise constness 和 logical constness

    成员函数如果是const意味着什么?这里有两个流行概念:bitwise constness(又称physical constness)和logical constness

    bitwise constness

    bitwise constness阵营的人相信,成员函数只有在不更改对象的任何成员变量时才可以说是const。也就是说它不更改对象内的任何一个bit。bitwise constness正是对C++常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

    不幸的是许多成员虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise constness不会引发编译器异议。

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

    operator[]实现代码并不更改pText。于是编译器很开心地为operator[]产生出目标码。它是bitwise const,所有编译器都这么认定,但是,这个class不适当地将其operator[]声明为const成员函数,而该函数却返回一个reference指向对象内部值,于是就:

    const CTextBlock cctb("Hello");        //声明一个常量对象。
    char* pc = &cctb[0];                //调用const operator[]取得一个指针,指向cctb的数据。
    *pc = 'J';                            //cctb现在有了“Jello”这样的内容。
    

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

    logical constness

    这一派拥护者主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。

    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成员函数内不能赋值给textLength
            lengthIsValid = true;                //和lengthIsValid
        }
        return textLength;
    }
    

    length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两项成员修改对const CTextBlock对象而言虽然可接受,但编译器不同意。它们坚持bitwise constness。怎么办?
    解决办法很简单: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 (!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::siez_t position) const
        { 
            ...            //边界检验
            ...            //日志数据访问
            ...            //检验数据完整性
            return pText[position];
        }
        char& operator[](std::siez_t position)
        { 
            ...            //边界检验
            ...            //日志数据访问
            ...            //检验数据完整性
            return pText[position];
        }
    private:
        char* pText;
    };
    

    你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等令人头疼的问题吗?你真正应该做的是operator[]的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们将常量性转除

    令non-const operator[]调用其cosnt兄弟是一个避免代码重复的安全做法——即使过程中需要一个转型动作。

    class TextBlock{
    public:
        ...
        const char& operator[](std::siez_t position) const    //不变
        { 
            ...
            ...
            ...
            return pText[position];
        }
        char& operator[](std::siez_t position)                //现在只调用const op[]
        { 
            return const_cast<char &>(                        //将op返回值的const转除
                static_cast<const TextBlock&>(*this)          //为*this加上const
                    [position]                                //调用const op[]
            );
        }
    private:
        char* pText;
    };
    

    这里共有两次转型:第一次用来为*this添加const,第二次是从coant operator[]的返回值中移除const。
    添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),所以我们使用static_cast。移除const的那个动作只可以使用const_cast完成。

    虽然这个代码看起来不怎么美观,但是实现了“避免代码重复”的效果,为了达到这个目标而写出如此难看的语法是否值得,只有你自己决定,但“运用const成员函数实现出其non-const孪生兄弟”的技术是值得了解的。更值得了解的是,反向做法——令const版本调用non-const版本,这是一个错误行为。


    NOTE

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

    相关文章

      网友评论

        本文标题:Item 03:尽可能使用const

        本文链接:https://www.haomeiwen.com/subject/yleilttx.html