• Effective C++条款03:尽可能使用const(Use const whenever possible)



    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


    条款03:尽可能使用const

      const 语法虽然变化很多,但并不高深莫测,如果关键字 const 出现在星号左边,表示被指向的变量为常量,如果出现在星号右边,则表示指针本身是常量,因为指针也是变量嘛。关于 const 关键字的更多的知识可以参考C++ const关键字,这篇文章。

    1、const 与 STL迭代器

      STL 迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为 const 就像声明指针为 const 一样。

    • 如果你希望通过迭代器获取值但是不能改变值,并且迭代器指针可以进行移动,你需要的是“const_iterator”类型的迭代器
    std::vector<int> vec;
    ...
    const std::vector<int>::iterator iter = vec.begin(); // iter就像个T* const           
    *iter = 10; //正确,改变iter所指物
    ++iter;    //错误,iter是const
    
    • 1
    • 2
    • 3
    • 4
    • 5

    同理:

    • 如果你希望可以通过迭代器修改值,并且改动迭代器的位置,那么可以使用iterator类型的迭代器
    • 如果你希望可以通过迭代器修改值,但是希望迭代器指针不能进行移动。那么可以在"iterator"迭代器类型的前面加上一个const
    • 如果你既不希望通过迭代器改变值,迭代器指针也不能进行移动,那么可以在“const_iterator”前面加上const

      现在,你再回头看,就能够发现,咿,没错,就和const+指针用法是一样的。

    2、const 在函数中的应用

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

    • 在函数返回值中的应用

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

    class Rational{...};
    const Rational operator*(const Rational& lhs, const Rational& rhs);
    
    • 1
    • 2

      返回一个 const 类型可以防止以下的错误情况发生:
    ① 一个有理数的乘积是不可能改变的,因此赋值操作也没有意义,所以我们将operator*的返回值设置为const:

    Rational a,b,c;
    ...
    (a*b)=c;//将a*b的结果重新赋值了一个值
    
    • 1
    • 2
    • 3
    • 在函数参数中的应用

      对于 const 参数,没有特别新颖的概念,你应该在必要使用它们的时候使用它们,除非你需要改动参数。

    3、const 成员函数

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

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

    • ②它们使**“操作const对象”成为可能。这对编写高效代码是个关键(见条款20,改善C++程序效率的一个根本办法是以const引用**方式传递对象,而这个技术的前提是,我们有const成员函数可用来处理const对象)

      需要注意到的是,两个成员函数如果只是常量性不同可以被重载。即,const成员函数与非const成员函数可以构成重载

      例如下面的class,用来表现一大块文字:

    class textBlock
    {
    public:
        char& operator[](std::size_t position){
            return text[position];
        }
        const char& operator[](std::size_t position)const {
            return text[position];
        }
    private:
        std::string text;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    textBlock tb("Hello");
    std::cout << tb[0];  //调用非const版本的operator[]
    tb[0] = 'x';  //正确
    	
    const textBlock ctb("World");
    std::cout << ctb[0];  //调用const版本的operator[]
    ctb[0] = 'y';  //错误
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      const对象大多被用于常量指针形式或常量引用的形式传递给函数,因此也可以作为一个函数传参的例子:

    void print(const textBlock& ctb)
    {
    	std::cout << ctb[0];  //调用const版本的operator[]
    }
    
    • 1
    • 2
    • 3
    • 4

    4、bitwise constness与logical constness阵营

      const成员函数的使用由两种流行的概念:bitwise constness(又称physical constness)和logical constness

    bitwise constness(位常量)
      bitwise constness阵营的人认为,成员函数只有在不更改对象任何成员变量(static除外)时才可以说是const。

    class CTextBlock
    {
    public:
        //没有改变成员变量的值
        char& operator[](std::size_t position)const {
            return pText[position];
        }
    private:
        char *pText;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    logical constness(逻辑常量)

      logical constness阵营的人认为,const虽然不可以在const函数中改变成员变量(static除外)的值,但是允许在某些不知情的情况下改变了类中成员变量的值。

      例如上面CTextBlock类的的operator[]函数内部虽然没有改变成员变量的值,但是可以通过其返回值进行更改

    CTextBlock cctb("Hello");
     
    char *p= cctb[0]; //取得其返回值
    p = 'J'; //改变了
    
    • 1
    • 2
    • 3
    • 4

      到这里,你就可以对bitwise constness与logical constness的观点了解的很清晰了,前者就是站在不更改对象内的任何一个bit一方的,这种论点只需编译器寻找成员变量的赋值动作就能被反驳,后者则是站在能够修改对象内的某些bits一方的。

    5、mutable关键字

      上面bitwise constness思想导致不能在const成员函数内更改任何成员变量(static除外)的值。但是有些成员变量时可以改变的,怎么解决呢?答案是使用mutable关键字。

      例如下面的length const函数,我们可以在其中更改两个成员变量的值:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

      对于"bitwise constness"非我所欲的问题,mutable是个解决办法,但它并不能解决所有 const 难题。举个例子:

      改变上面的TextBlock类,实际代码中operator[]中不仅仅返回一个字符的引用,还可能在之前执行边界检测、志记数据访问、检验数据完整性等,因此代码量会增加。

      可以看到这样的代码比较臃肿,当然你也可以把执行边界检测、志记数据访问、检验数据完整性等操作封装于一个private成员函数中,然后使用operator[]调用这个函数,但你依旧重复了一些代码,如函数调用,两次 return 语句等。

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

      但真正需要实现的是:operator[]的机能一次并使用它两次(将两种类型的operator[]封装为一种)。也就是说,你必须令其中一个调用另一个。这促使我们将常量性移除。

      下面我们将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&>(                 //将operator[]返回值中的const移除
                static_cast<const TextBlock&>(*this)  //为*this加上const
                    [position]                        //调用const operator[]
            );
        }
    private:
        std::string text;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    两次类型转换:

      第一次:static_cast,我们在non-const operator[]中调用const operator[],但是non-const operator[]若只是单纯调用operator[],会递归调用自己(无限循环),因此,我们需要明确指出调用的是const operator[],这里使用static_cast将*this将TextBlock&换换位const TextBlock&类型

      第二次:const_cast,从const operator[]的返回值中移除const
    虽然这样的语法比较奇特,不容易理解,但是对于“避免代码重复”来说,这样的损失是值得的

      为什么不在const成员函数中调用non-const版本:因为编译器不允许,在const成员函数中是不允许调用non-const成员函数的。

    7、小结

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

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    八皇后问题的Java实现
    伦敦银一手是多少?
    什么是边缘计算网关?
    里P7告诉你,接口测试真的很简单,有手就行
    计算机网络ISO七层网络模型及TCP
    概念解析 | 功率放大器与低噪声放大器:一场关于信号放大的对比
    GoLand 2023.2.3(go语言开发)
    基于 HarmonyOS 的 HTTPS 请求过程开发示例(ArkTS)
    lv11 嵌入式开发 ARM体系结构理论基础2
    JVM GC 调优命令看这一篇就够了
  • 原文地址:https://blog.csdn.net/CltCj/article/details/127895849