条款03:尽可能使用const

指针与const:

如果要定义某指针或数据为常量不允许改变:

cpp 复制代码
const char* p;              //数据是const,数据不允许被改变
char* const p;              //指针是const,指针不允许被改变
const char* const p;        //数据与指针都是const,数据与指针都不可以被改变

记忆法: const在星号左边修饰数据,const在星号右边修饰指针

以及如下两个语句的功能是相同的,不需要对此产生困惑:

cpp 复制代码
onst char* pw;             //都表示指向常量char的指针
char const* pw;

迭代器与const

迭代器在功能上相当于指向某类型T的指针 T*

因此,如果想定义某迭代器指向一个常数,使用const_iterator是不可以的,这样只相当于定义一个迭代器为一个常量(T* const),例如:

cpp 复制代码
const std::vector<int>::iterator it = v.begin(); //注意,此声明只表示迭代器本身是常量        
*it = 10;                                        //编译通过,迭代器是常量,但数据可以被修改
++it;                                            //编译失败!因为const迭代器不允许被改变!

解决方法,使用const_iterator:

cpp 复制代码
std::vector<int>::const_iterator it = v.begin();  //使用了const_iterator类型
*it = 10;                                         //编译失败,数据不允许被改变!
++it;                                             //编译通过,迭代器本身可以被改变

令函数返回一个常量值,往往可以降低因客户端而造成的意外(尽量使用const可以帮助调试)

cpp 复制代码
const Rational operator*(const Rational &lhs, const Rational &rhs);

这样可以避免

cpp 复制代码
if ((a * b) = c)这样的错误。

const成员函数

给成员函数使用const关键字是非常重要的,它可以让接口更加直观,直接告诉用户这个函数是不是只读(Read only),会不会改变某变量。

更重要的是,用const修饰的对象只能调用const修饰的成员函数,因为不被const修饰的成员函数可能会修改其他成员数据,打破const关键字的限制。因此,需要同时声明有const和没有const的成员函数,例如:

cpp 复制代码
const char& operator[](size_t pos) const;
char& operator[](size_t pos);

对于某自定义的类Text:

cpp 复制代码
Text t("Hello");
const Text ct("Hello");

std::cout<<t[0];            //调用了不加const修饰的索引操作符
std::cout<<ct[0];           //调用了const版本, 但如果只有不加const的操作符,将会报错discard qualifier
t[0] = 'x';                 //成立,但注意此索引操作符必须声明为引用才可以支持赋值操作
ct[0] = 'x';                //错误!常量不能被修改   

成员函数的常量性(Constness):const成员函数承诺不修改对象的任何成员变量
C++标准对成员函数"常量性"的规定是数据常量性(bitwise constness),即不允许常量对象的成员数据被修改。C++编译器对此的检测也十分简单粗暴,只检查该成员函数有没有给成员数据的赋值操作。

但如下情形,即使修改了某个数据,也可以通过编译器的检测:

cpp 复制代码
const Text ct("Hello");        //构造某常量对象
char* pc = &ct[0];             //取其指针
*pc = 'K';                     //通过指针修改常量对象,编译不会报错,结果为"Kello"

数据常量性还有另一个局限性,例如:

cpp 复制代码
class Text{
  public:
    std::size_t length() const;
  private:
    char* pText;
    std::size_t length;
    bool lengthValid;
....
};

std::size_t Text::length() const{
  if(!lengthValid){                      //做某些错误检测
    length = std::strlen(pText);         
    lengthValid = true;                   
  }

  return length;                         //这行才是代码核心
}

在这段代码中,length()函数要做某些错误检测,因此可能会修改成员数据。即使真正的功能核心只是返回字符长度,编译器依然认为你可能会修改某些成员数据而报错。

因此,更好的方法是逻辑常量性(Logical constness),即允许某些数据被修改,只要这些改动不会反映在外,例如,以上问题可以用mutable关键字来

解决:

cpp 复制代码
mutable std::size_t length;
mutable bool lengthValid;

这样成员函数length()就可以顺利通过编译。
此外注意, 除mutable之外,静态成员(static)也可以被const成员函数修改。
在定义常量与非常量成员函数时,避免代码重复

可能大家会有所困惑,既然两个版本的成员函数都要有,为什么又要避免重复?

其实在这里指的是函数的实现要避免重复。试想某函数既要检查边界范围,又要记录读取历史,还要检查数据完整性,这样的代码复制一遍,既不显 得美观,又增加了代码维护的难度和编译时间。因此,我们可以使用非常量的函数来调用常量函数。

cpp 复制代码
const char& operator[](std::size_t pos) const{....}
char& operator[](std::size_t pos){
  return
    const_cast<char&>(                              //const_cast去掉const关键字,并转换为char&
      static_cast<const Text&>(*this)[position];    //给当前变量加上const关键字,才可以调用const操作符
  );
}

为了避免无限递归(如果存在const调用非const,则const调用非const和非const调用const就会生成无限循环)调用当前非常量的操作符,我们需要将(*this)转换为const Text&类型才能保证安全调用const的操作符,最后去掉const关键字再将其返回,巧妙避免了代码的大段复制。

但注意,如果使用相反的方法,用const函数来调用non-const函数,就可能会有未知结果,因为这样相当于non-const函数接触到了const对象的数据,就可能导致常量数据被改变。

只能在非const取消重复,不能在const调用非const

对const成员函数另一种方式解释:

对于成员函数的const,有两个概念:bitwise constness和logical constness。
bitwise constness: 指的是成员函数只有在不更改对象之任何成员变量才可以说是const。也就是说它不更改对象内的任何一个bit。不幸的是许多成员函数虽然不十足具备 const性质却能通过bitwise测试。例如,一个更改了"指针所指物"的成员函数虽然不能算法const,但如果只有指针率属于对象,那么此函数不会引发编译器意义,这导致反直观结果。

cpp 复制代码
class CTextBlock
{
public:
char &opeartor[](std::size_t position) const //bitwise const声明,但其实不适当
private:
char *pText;
};
cosnt CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J';

你创建一个常量对象设以某值,而且只对它调用const成员函数。但你还是改变了它的值。

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

cpp 复制代码
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);
    lengthIsValid = true;
  }
  return textLength;
}

length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两笔数据被修改对于const CTextBlock对象虽然可接受,但编译器不同意。它们坚持bitwise constness。

解决方案很接单:把它们设置成mutable。
const和non const防止重复

cpp 复制代码
const char &operator[](std::size_t position) const
{
  ...   //边界检验
  ...   //日志记录访问记录
  ...   //检验数据完整性
  return text[position];
}

char &operator[](std::size_t position)
{
  ...  //边界检验
  ...  //日志记录访问记录
  ...  //检验数据完整性
  return text[postion];
}

这里用non const函数调用const函数的方法来避免代码重复,这种方式并不推荐。

用const调用non const的方法是不合适的,因为non const方法有可能修改数据,而const调用之后,就可能也会修改数据,不符合函数的const语义。
总结:

指针,迭代器,引用,本地变量,全局变量,成员函数,返回值都可以使用const来实现数据只读的目的,const是C++一个非常强大的特性。除此之外,它还能帮助加快调试过程

即使编译器使用数据常量性的标准,我们编程的时候应该采用逻辑常量性,对相关不可避免更改的成员数据加上mutable关键字来修饰

当有大段复制代码出现在const和non-const的成员函数中,可以使用non-const函数来调用const函数来避免复制

相关推荐
Rabitebla2 小时前
【C++】手撕日期类——运算符重载完全指南(含易错点+底层逻辑分析)
java·c语言·开发语言·数据结构·c++·算法·链表
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(九):线程池实现(附代码示例)
linux·运维·服务器·c++·学习·架构
tankeven2 小时前
C++ 学习杂记01:C++ vector 容器详细
c++
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(八):<策略模式>日志系统的封装实现
linux·运维·服务器·c++·学习·策略模式
盐焗鹌鹑蛋2 小时前
【C++】string模拟实现
c++
特种加菲猫2 小时前
C++进阶:模板深度解析与继承机制初探
开发语言·c++
旖-旎2 小时前
递归(快速幂)(5)
c++·算法·力扣·递归
大江东去浪淘尽千古风流人物5 小时前
【cuVSLAM】GPU 加速、多相机、实时视觉/视觉惯性 SLAM设计优势
c++·人工智能·数码相机·ubuntu·计算机视觉·augmented reality
自信1504130575911 小时前
重生之从0开始学习c++之模板初级
c++·学习