• 《Effective C++》 rule 03: Use const whenever possible


    const 可以修饰哪些东西?

    • const 可以修饰全局 (global) 或命名空间 (namespace) 或类外部的常量.

    • const 也可以修饰文件、函数或作用域中(block scope)被声明为 static 的对象.

    • const 还可以修饰类内部的静态 (static) 和非静态 (non-static) 成员变量.

    • const 与指针结合时,const 既可以修饰指针本身,也可以修饰指针所指对象.

      • 当 const 在 * 左边时,表示指针指向的对象是常量.
      • 当 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(指针本身和指针所指对象都是常量)
      

      PS:有时 const 不一定写在对象类型之前,而是在对象类型和 *之间,这也能用来表示指针所指对象是常量.

      例如:

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

      这两种写法等价,都表示 pw 指向的是一个 const Widget* 对象.

    STL 迭代器与指针

    迭代器的作用就像是一个 T* 指针!

    有一个需要注意的地方:声明一个迭代器为 const 时,实际上是相当于 T* const 指针.

    • 即,指针自身是常量,而指针所指对象实际上是可以改变的!

    而如果我们想表示指针所指对象为常量,则需要使用 const _iterator.

    例:

    std::vector<int> vec;
    ...
    
    // 相当于 T* const
    const std::vector<int>::iterator iter = vec.begin();		// 即 iter 本身是常量
    *iter = 10;		// 没问题,修改迭代器指向的对象
    ++iter;			// 错误:迭代器是个常量
    
    // 相当于 const T*
    std::vector<int>::const_iterator cIter = vec.begin();		// 即 iter 所指对象是常量
    *cIter = 10; 	// 错误:iter 所指对象是常量
    ++cIter;		// 没问题,修改迭代器指向的对象
    

    尽量多使用 const

    对于函数返回值,如果没有修改参数或局部变量的需要,就应该将它的返回值声明为 const

    这样的目的主要是为了安全,防止程序员因为失误对不该修改的函数返回值、参数或对象进行修改.

    const 成员函数

    Q:为什么要有 const 成员函数?

    A:原因有二.

    1. 这样可以知道函数可以改变对象内容.
    2. 这样可以 "操作 const 对象".

    一个重要的 C++ 特性

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

    也就是说,对于同样的一种“操作”,可以有处理 const 对象和 non-const 对象的两套逻辑.

    看一个例子,有个类 TextBlock,用来表示一段文字:

    class TextBlock {
    public:
        ...
        const char& operator[](std::size_t position) const {	// 处理 const 对象的 operator[]
            return text[position];
        }
        char& operator[](std::size_t position) {			   // 处理 non-const 对象的 operator[]
            return text[position];
        }	
    private:
        std::string text;
    };
    

    对于 operator[],可以这么调用:

    TextBlock tb("Hello");
    std::cout << tb[0];				// 调用 non-const 对象版本的 operator[]
    const TextBlock ctb("World");
    std::cout << ctb[0];			// 调用 const 对象版本的 operator[]
    

    还有一种更常见的调用 const 对象版本函数/运算符的例子:

    void print(const TextBlock& ctb) {		
        std::cout << ctb[0];		// 调用 const 对象版本的 operator[], 即 const TextBlock::operator[]
    }
    

    重载的两个版本的函数,主要的区别就是,处理 const 对象的版本的函数不可以修改对象(很好理解,因为对象是 const 的嘛):

    std::cout << tb[0];				// 没问题,读一个 non-const 对象
    tb[0] = 'x';				    // 没问题,修改一个 non-const 对象
    std::cout << ctb[0];			// 没问题,读一个 const 对象
    ctb[0] = 'x';					// 有问题!不能修改一个 const 对象
    

    这里还有一个要注意的点:non-const operator[] 的返回值类型是 char& 而不是 char.

    • 如果返回值类型是 char,则语句 tb[0] = 'x'; 无法通过编译.
      • 因为 char 是内置类型,如果函数的返回值是个内置类型,那么修改函数返回值是不合法的!
    • 因为我们要修改对象,所以得传递引用(pass by reference),否则只是修改一个副本,没法起到修改对象的作用.

    bitwise constness (physical constness) 和 logical constness

    bitwise constness: 成员函数只有不修改对象的任何成员变量(当然 static 成员变量除外)时才可以声明为 const.

    也就是说,bitwise constness 没有修改对象中的任何一个 bit (所以叫 bitwise constness).

    这其实也是 const 本身的定义. 即没有对修饰的对象内的任何 non-static 成员变量进行修改.

    What's wrong with bitwise constness?

    bitwise constness 有个问题.

    考虑这样一种情况:

    • 类中有个指针对象,一个 bitwise constness 的成员函数没有修改这个指针对象.
    • 不过,这个指针指向的对象不属于这个类.
    • 因此,在这个 bitwise constness 的成员函数中有可能会对这个指针所指对象进行了修改.
    • 不过这也是完全合法的(编译通过),因为理论上这个指针所指对象并不属于这个类(只有这个指针属于中这个类),而这里只是对指针所指对象进行修改(而没有修改指针本身),因此是完全 ok 的.

    例子:

    class CTextBook {
    public:
        ...
        char& operator[] (std::size_t position) const {		// bitwise const 声明
            return pText[position];
        }
     private:
        char* pText;
    }
    

    注意,这里的返回值是 char&,是个引用,所以有可能会修改指针所指对象的值.

    • 然而,这里并没有修改 pText 这个指针,因此编译器认为这个成员函数声明为 const 没有问题.

    而正如刚才所说,这里是可以修改指针所指对象的值的:

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

    语法上,上面的代码并没有问题. 指针 pc 并没有修改,只修改了 pc 指向的对象 cctb, 而 cctb 并不属于 CTextBook,所以 operator[]是符合一个 const 成员函数的要求滴~

    但是逻辑上,这里还是产生了一些 "意外的修改". 于是乎便有了 logical constness

    logical constness

    logical constnessbitwise constness 的不同在于: logical constness 不在乎是否(物理上)修改了类对象的 bits, 它的关注点在逻辑上是否有 "产生修改" (不管修改的是什么).

    • 换句话说,logical constness 允许(物理上)修改类对象,而主要保证逻辑上不要产生错误的修改.

    考虑刚才的 CTextBook 例子,我们可能有这样一个需求:缓存文本的长度,并根据缓存的长度去读这段文本.
    那么有如下代码:

    class CTextBook {
    public:
        ...
        std::size_t length() const;
    private:
        char* pText;
        std::size_t textLength;			// 缓存的已读的文本的长度
        bool lengthIsValid;				// 判断当前长度是否有效(文本是否还可以继续读)
    };
    
    std::size_t CTextBook::length() const {
        if (!lengthIsValid) {
            textLength = std::strlen(pText);
            lengthIsValid = true;
        }
    }
    

    当然,这段代码会报错,因为尝试在 一个 const 成员函数 CTextBook::length() 内修改类 CTextBook 的两个成员变量 textLengthlengthIsValid.

    解决办法是:使用 mutable 关键字释放掉 non-static 成员变量的 bitwise constness 约束:

    class CTextBook {
    public:
        ...
        std::size_t length() const;
    private:
        char* pText;
        // 声明:即使在 const 成员函数内,这两个成员变量也有可能会被修改
        mutable std::size_t textLength;			
        mutable bool lengthIsValid;				
    };
    
    std::size_t CTextBook::length() const {
        if (!lengthIsValid) {				// 在 const 成员函数内修改 mutable 成员变量,没问题
            textLength = std::strlen(pText);
            lengthIsValid = true;
        }
    }
    

    这里,(物理上)修改了类对象的 non-static 对象,而保证了逻辑上没有错误的修改,这就是 logical constness.

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

    前面提到了:"两个成员函数,如果只是常量性(constness)不同,是可以被重载的!"

    那么对于重载的这两个函数,里面可能有大量的重复代码:

    class TextBook {
    public:
        ...
        cosnt char& operator[] (std::size_t position) const {
            ...				// 边界检查(bounds checkint)
            ...				// 访问日志数据(log access data)
            ...				// 校验数据完整性(verify data integrity)
            return text[position];
        }
        char& operator[] (std::size_t position) {
            ...				// 边界检查(bounds checkint)
            ...				// 访问日志数据(log access data)
            ...				// 校验数据完整性(verify data integrity)
            return text[position];
        }
    private:
        std::string text;
    };
    

    显然,这里对于 constness 不同的函数的重载导致了很多代码重复,这会带来很多问题——比如:

    • 不好维护
    • 代码量膨胀
    • 编译时间增长

    解决办法:casting away constness

    一句话概括:在处理 非const 对象版本的函数中调用处理 const 对象版本的函数

    • 这样,同样的代码只写了一份.

    上面的例子可以改成这样:

    class TextBook {
    public:
        ...
        // 处理 const 对象版本的函数还是原来的样子
        cosnt char& operator[] (std::size_t position) const {
            ...				// 边界检查(bounds checkint)
            ...				// 访问日志数据(log access data)
            ...				// 校验数据完整性(verify data integrity)
            return text[position];
        }
        // 处理 非const 对象版本的函数调用 const char& operatorp[]
        char& operator[] (std::size_t position) {
            /*
            	这里的 const_cast 的作用是为了去调用 const op[]
       			而 static_cast 的作用则是消除了 调用的 op[] 的 constness
            */
            return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
        }
    private:
        std::string text;
    };
    

    这样,通过将 const_caststatic_cast 结合,就实现了用 const 成员函数实现 非const 成员函数,避免了代码重复!

    总结

    • 尽量多使用 const,const 可以帮助编译器检查一些错误用法(比如不该被修改的数据等).
    • const 可以施加在任何作用域内的对象、函数参数、函数返回类型、成员函数.
    • 编译器在语法上做的检查时 bitwise constness,而实际编写程序时我们应该用 logical constness(必要时使用 mutable).
    • 当 const 成员函数和 non-const 成员函数有着实质上等价的实现时,为了避免代码重复,可以使用 const_cast 结合 static_const. 从而在 非const 成员函数中调用 const 成员函数.
  • 相关阅读:
    【luogu3768】简单的数学题【杜教筛】【欧拉函数】
    【bzoj3589】动态树【树链剖分】【线段树】
    【bzoj4386】[POI2015]Wycieczki【矩阵快速幂】【倍增】
    【bzoj2186】[Sdoi2008]沙拉公主的困惑 【线性筛】【容斥原理】
    【bzoj3884】上帝与集合的正确用法 【欧拉函数】
    【bzoj4417】[Shoi2013]超级跳马 【矩阵快速幂】
    【bzoj3435】【uoj#55】[WC2014]紫荆花之恋 【动态树分治】【平衡树】
    【bzoj3681】Arietta 【网络流】【主席树】【启发式合并】
    【bzoj1532】[POI2005]Kos-Dicing 【网络流】【二分】
    【bzoj1565】[NOI2009]植物大战僵尸 【网络流】【最大权闭合子图】
  • 原文地址:https://www.cnblogs.com/linrj/p/16163555.html
Copyright © 2020-2023  润新知