前言

之前在学习C++的时候,对于const等相关概念了解的不是很清楚,最后在使用时就很痛苦😖
这次彻底的总结一下const相关的知识点

阅读变量声明

实际上,const位于变量声明的不同位置,会产生不同的作用。因此,首先我们需要学会如何阅读变量的声明语句,从而了解变量的具体类型,获取const修饰的对象,才能知道其作用。

这里我们依照由右至左规则,可以有效的分解变量的类型。

整个由右至左规则如下所示:

  1. 从变量名称开始
  2. 向右依次列出所有的关键词,直到遇到不匹配的右括号或者结束停止
  3. 向左依次列出所有的关键词,直到遇到不匹配的左括号或者结束停止
  4. 如果因为括号停止,则跳出括号,重新执行步骤2.

    这里我们举一个比较有挑战性的例子

    1
    char (*(*x())[])();

下面给出按照上述分析的步骤

  • 根据步骤1,找到变量名称x,即 char (*(*x())[])();
  • 根据步骤2
    1. 找到(),即 char (*(*x())[])();,因此为无参函数
    2. 右边遇到不匹配右括号,结束
  • 根据步骤3
    1. 找到*,即 char (*(*x())[])();,因此返回值为指针
    2. 左边遇到不匹配左括号,结束
  • 根据步骤4,跳出括号,即 char (*(*x())[])();
  • 根据步骤2
    1. 找到[],即char (*(*x())[])();,因此其指针指向一个数组
    2. 右边遇到不匹配右括号,结束
  • 根据步骤3
    1. 找到*,即char (*(*x())[])();,因此该数组元素都是指针
    2. 左边遇到不匹配左括号,结束
  • 根据步骤4, 跳出括号,即char(*(*x())[])();
  • 根据步骤2
    1. 找到(),即char(*(*x())[])();,因此该指针是指向无参函数的指针
    2. 右边完结,结束
  • 根据步骤3

    1. 找到char,即char (*(*x())[])();,因此函数的返回值为char类型
    2. 左边遇到完结,结束

    综上可知,该变量是一个无参函数,其返回值是一个指针,该指针指向一个数组,数组中的元素都是指针,这些指针指向返回值为char的无参函数。

const性质

const表示修饰的值不能改变,这就是该关键词的性质。

那么实际上,根据上面的规则,我们很容易完全理解const的种种特性

  1. const type a;/type const a;
    const用来修饰type类型,也就是变量a的值不能进行改变

  2. const type *a;/const type &a;
    根据前面的规则,这是一个指针/引用,其指向/引用一个const type类型——也就是其指向/引用的值不能修改,但其本身并没有要求

  3. type * const a;
    根据前面的规则,这是一个const对象,该对象是一个指向type的指针——也就是该指针不能重新指向新的对象,但是其指向的值可以进行任意的修改

  4. const type * const a;
    根据前面的规则,这是一个const对象,该对象是const type *类型,即该对象是一个指针,指针指向const type类型——即该指针既不能重新指向新的对象,也不可以修改其指向的值。

    总的来说,基本就是按照前面的规则解释变量类型,其中const修饰的对象的值不能改变

顶层const和底层const

顶层const

对于对象本身是一个常量,不可更改对象本身的值,但在对像是指针或引用的情况下,可能修改其指向或引用的值,则称为顶层const,如int * const p1;

底层const

如果对象是指针或引用,其指向或引用的对象的值不可更改,则称为底层const,如const int &r1;

差别

在使用时,顶层const底层const差别十分大,这里简单说一下常见的差别

  1. 赋值、初始化

    • 对于顶层const,由于const可以兼容非const,因此对于赋值对象是否为常量不影响操作,如下所示

      1
      2
      3
      int *p1 = nullptr, *const p2 = nullptr;

      int *const p3 = p1, *const p4 = p2; //有效,const兼容非const的p1
    • 对于底层const,虽然const可以兼容非const,但是如果在底层const的情况下也进行兼容,可能会修改const类型的值,如下所示

      1
      2
      3
      4
      5
      const int val = 0;
      int *p1 = nullptr;
      int const **p2 = &p1; //如果底层const可以被兼容,则&p1为指向变量的指针,p2为指向常量的指针,则p2可以兼容&p1
      *p2 = &val; //此时p1指向了&val
      *p1 = 1; //由于p1是指向变量的指针,因此其值可以进行改变,从而修改了常量val的值

      为了避免上面情况的发生,这里规定底层const不能兼容非const,如下所示

      1
      2
      3
      int const val = 0;
      int const &r1 = val, *const p1 = &val; //有效,其底层const类型一致,顶层const可以兼容
      int * const *p2 = &val; //无效,因为其底层const不一致:p2底层指向变量,而&val底层是常量

注意点

这里需要注意一下typedef关键字。即通过typedef关键字,其生成新的基本数据类型,并不是简单的宏替换。这点在const中特别关键,如下

1
2
3
4
5
typedef char *pstring


const char * p1;
const pstring p2; //相当于pstring const p2;

如果仅仅将typedef当作简单的宏替换,那么p2的定义进行展开就是p1,其是一个指向常量char的指针,即p1的值可以进行任意的修改,但是p1指向的值不能进行修改
但实际上,我们需要将pstring当作一个基本数据类型看待,即这是一个常量pstring类型,即p2不能随意更改其值;而由于pstring是指针类型,实际上我们仍然可以修改其指向的值。
可以看到,两种理解方法,得出来的结论完全相反。