Effective C++读书笔记(2)

    这几天一直在忙换工作的事情,没有保证好更新的效率,既然决定了就不能拖拖拉拉的,从今天开始要保证3天一篇的速度,好了,让我们继续Effective C++的学习吧。

建议 03 : 尽可能的使用const关键字

Use const whenever possible

    这条建议应该被作为信条来使用,在阅读Amazon Alexa和google Grpc源码的过程中,我发现一流大厂对这一点的执行的相当好,及时不考虑它在语法层面上的保护作用(只读),它还能非常好的告诉阅读你代码的人,这个对象/函数是只读的,它不应当被修改。它是非常有效的语义约束,不仅是对编译器,同时它也约束了开发者作出不必要的修改,既然某个对象应该是只读的,那么我们为什么不给他加个const进一步确保它不被修改呢?编译器会对所有const修饰的对象进行编译时检查(compile-time checking),确保约束没有被违反。

    首先我们来总结一下const可以使用的场景。const几乎可以用来修饰一切你能想的到的对象,包括:

  • 常量(global or in the namespace)
  • 文件
  • 函数
  • 区块作用域(block scope)中被声明为static的对象
  • 类内部成员变量
  • 指针以及指针指向

关于指针和指向的const语义是非常常见的(并且在面试中可能会被问到),如:

char strs[] = "Hello World!";
char* p = strs; //普通指针
const char* p = strs; //一个指向字符常量的指针  (non-const pointer, const data)
char* const p = strs;//一个字符指针常量  (const pointer, non-const data)
char const *p = strs;//一个指向字符常量的指针 (non-const pointer, const data)
const char* const p = strs;//一个指向字符常量的指针常量 (const pointer, const data)

看起来是有一点点绕,但是只要观察const在解引用符(*)的左边还是右边就可以判断是指针常量还是指向常量了
如果const在解引用符的左边代表指针指向一个常量值,如果const在解引用符的右边则代表指针是一个常量,指向不可修改,如果解引用符的左右都有const修饰就代表这个指针是一个指向常量的指针常量。请用下面的这句来速记:

左值右向

在左边代表指向的值不可修改,在右边代表指向不可修改。

根据习惯,在进行修饰常量指针时(const data)有的程序员习惯将const写在类型左边:

void func(const char* p); 

有的习惯将const写在类型右边解引用符左边:

void func(char const *p);

这两种写法在功能上是一样的,没有本质的区别。

一些情况下,我们经常想要遍历一个容器,我们可以使用STL的迭代器来完成遍历,但是我们不希望引起容器值的改变,这个时候我们可以使用const_iterator来进行迭代过程:

std::vector<int> vec;
..

std::vector<int>::const_iterator iter = vec.begin();
for (iter; iter != vec.end(); ++iter) {
    std::cout << *iter << std:endl;
    ...//do something like print
}

书中还介绍到了如果要使得迭代器指向不可修改应该这样声明

std::vector<int> vec;
... 
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;  //true 指向内容可以被修改
++iter;// 报错,指向不可修改

这种情况比较少,既然用到了迭代器,不让迭代器移动不太可能,但是写法还是需要注意一下。

一般情况下我们在进行迭代器移动的时候用++iter,而不用iter++,因为++iter是左值,已经是个对象,iter++是右值,是个引用对象的表达式,返回一个临时变量,效率要低一些。

返回值const化的情况

竟然有大佬给我评论了,有点小激动。之前的描述有问题,一般情况下都不会对函数返回值进行const化,下面可能举出需要返回值const修饰的情况:

  • 函数返回值本就应该是常量的
  • 为了让重载运算符符合逻辑,运算符重载加const 约束 对 a+b+c 这样的运算没有影响,因为a+b 运算的结果是const ,但对其只是只读操作,会创建一个新的 A 类返回。
  • 通过函数创建指向常量的指针,如果通过函数来创建常字符串,除了在main 函数中约束之外,也可以在函数返回类型中约束
  • 满足对const成员函数的调用,const类型的对象,不能调用自身的非const成员函数,但是可以调用自己的const成员函数
  • const 成员函数的返回类型是引用时候,需要加const 约束

一般情况下,让函数返回一个只读值,可以降低客户错误照成的意外或者未知BUG,并且还可以保证安全性和高效性,如在有理数的运算符重载实现中,将返回值修饰为只读,可以避免用户对返回值做一些奇怪的操作产生意外结果。

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

//如进行这种操作

Rational a, b, c;
...
(a * b) = c; //这种情况少见

if (a * b = c)...// 少打了一个等号导致判断结构不符合

碰到这种问题的时候可能排错会比较浪费时间,但是如果返回值声明成const,就可以从根源上断绝这种错误的发生。

const 成员函数

将成员函数声明为const有两个好处,第一,接口使用人员可以很好的了解到这个接口是否会改变对象内容。第二,const对象使得“操作const对象”成为可能,所谓的“操作const对象”就是尽量避免pass by value造成的拷贝开销,而是替代于const对象传递(reference-to-const),而为了实现这种方式,前提就是存在const成员函数来处理const对象。

关于成员函数的const性质有两个概念:bitwise constnesss(physical constness) 和 logical constness

bitwise const代表成员函数不直接修改对象内的任何一个bit,这种概念的好处是编译器容易检查违反点,只需要检测成员变量的赋值语句就可以了。这也正是目前C++所采用的,但是有一个问题,就是虽然函数内不会对对象进行任何修改,但是其返回值或者动作间接的导致了修改的发生,如返回了一个非const的值,外部通过修改这个值达到了修改对象的作用,如:

const CTextBlock cctb("hello"); //声明常量对象
char* pc = &cctb[0];            //调用const operator[] 取得对应下标指针 (假设操作符已经重载)
*pc = 'H';                      //还是对对象进行了修改

所以还有另外一种概念 logical constness,这种概念允许对象部分内容在const成员函数中被修改,如存在一个文本编辑器它要获取当前文本长度,那么它其实并没有修改文本内容,但是文本长度可能实时在改变,此时我们声明了一个:

std::size_t tl;
std::size_t Textpanel::length() const {
   ...
   tl = std::strlen(text); //错误 const内不允许对成员变量赋值
   return tl;
}

此时我们可以使用mutable来创建一个const相关的摆动场来允许tl可修改,只需要将tl声明成:

mutable std::size_t tl;

就可以被修改了。

虽然编译器采用的是bitwise const,但是在实际编程中我们应该使用logical constness来进行开发。

当类的non-const版本的成员函数和const版本的实现等价时,为了减少重复代码,可以采用const_cast<T&>和static_cast<const C&>来进行对const版本的Non-const拓展:

class TextBlock{

public:
const char& operator[] (std::size_t position) const {
   ...
   return text[position];
}

char& operator[] (std::size_t position) {
   return
   cons_cast<char&>(
     static_cast<const TextBlock&>(*this)[position]
     );
}
};

总结:

  • 将某些东西声明为const可以帮助编译器侦测出错误用法.const可以被用于修饰任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
  • 编译器强制使用bitwise constness,但是我们在进行开发的时候应该使用概念上的常量性(conceptual constness)
  • 当const和non-const成员函数有着相同的实现时,可以在non-const版本中调用const版本并转换减少代码重复。
    原文作者:CoreDump
    原文地址: https://segmentfault.com/a/1190000014455340
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞