C++中为什么char**不能转换成const char**? 探索C语言和C++中const的美丽故事

本文涉及对C和C++特性比较深入的探讨, 并不适合新手阅读, 仅仅希望解决问题的请跳转最后一节寻找解决方案。对于本文大部分内容, 需要读者至少清晰知道int* constconst int* 类型的区别, 并能读懂形如const int **const *const **的多层const类型, 确保不会被绕晕, 对此概念不清淅的可以阅读该回答

g++编译通过的样例:

/*sample1*/
char p2;
const char p1 = p2;
/*sample2*/
char * p2;
const char* p1 = p2;
/*sample3*/
char p2;
const char& p1 = p2;

g++编译错误的样例:

char ** p2;
const char** p1 = p2;

1. 什么? g++编译错误? 我gcc第一个不服!

当我看到我将int**类型赋值给const int**类型的代码被g++狠狠打上一个error之后,我第一反应是, 这怎么可能不行?把一个不带const的赋值给带const的, 不是理所当然应该编译通过吗?我脑洞大开想起了gcc, 希望gcc能站在我这边给予我赞同。于是我使用gcc编译了完全相同的这两行代码, 编译通过!我很感动, 但是我没法忽略gcc给我留下温柔的警告:

warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]

和g++的error进行对比如下:
在这里插入图片描述
这一刻我意识到, 这个行为在编译器眼里并不是一个简单的规则, 不是一个简单的对与错。在这两行代码背后, 或许还有着更复杂的原因, 一个美丽的故事。

2. const 的层次

多重指针+多重const是一个一听就很晕的概念, 为了清晰地描述这类概念, 我先引入const的层次的概念。

《C++primer》将int const * int类型中左边的const称为顶层const, 右边的const称为底层const,但没有涉及多重指针的相关概念。对于多重指针,我认为依旧按照这个描述来定义层次关系是没问题的。这样的层次关系可用以下拓扑表示。

  • int const * constconst int * const
    在这里插入图片描述

  • const char * const *
    在这里插入图片描述

  • char * const * const
    在这里插入图片描述
    下文中,只要该拓扑结构不同,一律描述为const层次结构不同

3. C语言之指向常量的指针

在C语言语境下的const, 是一个很冷门的话题, 因为用的人真的不多, 唯一比较常见的用处是传递指针的时候禁止修改该地址的内容。但是对于为什么用得少, 一直还没有一个概念。在做了几个实验, 看了些资料后, 终于明白了其中特别重要的一个原因, 叫做“设计缺陷”。

3.1. 两种情况下的警告

  1. 其他层的const相同,将上一层有const的指针赋值给上一层没有const的指针。(即指向const的指针赋值给不指向const的指针)

    /*sample1*/
    const char *const *p1;
    const char **p2 = p1;
    /*sample2*/
    const char* p1;
    char* p2 = p1;
    

    使用gcc编译通过,给出明确的警告

     warning: initialization discards ‘const’ qualifier from pointer target type 
    

    在这种情况下,gcc明确地告诉我们我们这样做的后果是使这个指针指向的变量失去const属性,可能被修改。

  2. 除了[1]中提到的情况外,以及除了“两个最底层const不同”的多重指针相互赋值(值传递, 编译通过)的情况外, 出现的其他“const层次结构不同”的多重指针相互赋值的情况。最典型的例子莫过于我们标题中给出的样例。

    /*sample1*/
    const char** p1;
    char** p2 = p1;
    /*sample2*/
    const char*const* p1;
    char**p2 = p1;
    /*sample3*/
    const char*const* p1;
    char**p2 = p1;
    

    使用gcc编译通过,给出模糊的警告

    warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
    

    sample1中, 明显看出该赋值操作使**p1失去了它的const属性, 可能被修改。但是gcc并没有告诉我们是丢失了const属性, 而是模糊地给出"不兼容"的警告。这是为什么呢?

4. C语言挖下的大坑

这个时候,回顾一下我们的题目中无法在C++编译通过的示例

char ** p2;
const char** p1 = p2;

就像我们简单粗暴地认为将不带const的变量赋值给带const的变量是合乎逻辑的一样。最初的C标准指定者也犯下了一个大错, 就是允许了这样的行为(看来大神们也和会和我翻一样的错啊)。然而在标准即将发行的时候有人发现,这种赋值方式会导致一个神奇的漏洞,人们可以利用这个漏洞随意修改另一个const变量。以下骚气的代码来自《C primer plus》

const int n = 13; /*我是一个与世无争的const变量*/
int *p1;
const int **pp2 = &p1; /* int**类型 -> const int**类型*/
*pp2 = &n;      /*const int*类型 -> const int*类型*/
*p1 = 10;       /*看起来没毛病...不对,与世无争的 n 怎么被修改了?*/

在发现了这个惊天大bug之后, 大神们没来得及深究背后的逻辑关系, 情急之下只好禁用(给出警告)了大部分不同const层次结构的变量之间的相互赋值,比如上面介绍到的一些情况。由于没查清楚真正的原因,所以对于该类型的赋值,编译器一律给出一个含糊其辞的incompatible警告,把这个巨大的坑留给可怜的开发者自己处理。

至此, 我们已经解决了问题的一半, C语言的故事帮助我们弄明白了char**类型变量为何不能赋值给const char**变量。但是我们还没有实现我们的目的, 因为无论我们如何修改我们的代码, 也没有办法在gcc不发出警告的情况下实现我们为双重指针添加最顶层const限定的目的。也正是因为C语言的const非常鸡肋, 所以存在感很低, 大家用得非常少。
在这里插入图片描述

5. 老爸的坑儿子填, 且看C++如何填坑

《C primer plus》中简单地提到C++和C语言中const类型的区别, 其中主要特点就是C++的类型审查更加严格。毕竟C语言给的全是warning, 起码C++会给error。但是在上一节中, C语言很不负责任地留下了一个没有研究清楚的问题, 那就是这堆incompatible的警告里面, 到底哪些行为是应该被允许的, 哪些是不应该被允许的?

聪明的C++语言设计者们深入研究了这个坑后发现以下规律并以此为基准对C++标准进行了制定。

只有出现"在某一层const后面跟着一层非const(不考虑最底层)"的情况下, 才会导致出现第4节中的bug。 也就是说, 在这种情景下多重指针的const要从右往左填满。

这里应该还有严格的数学证明, 我作为一个离散数学全忘了的彩笔就暂且跳过证明这一部分吧。

根据我们"发现"的这个规律, 我们可以轻而易举地将我们的代码修改成g++喜欢的样子并写出各种各样疯狂的代码。在这里我们代入业务情景:

  1. 定义了一个二维数组, 将它传给printArray()`函数的时候希望限定它不要被修改。
    • 传值
      在这里插入图片描述
    • 传引用
      在这里插入图片描述
  2. 定义了一个三维数组…
    1. 传引用
      在这里插入图片描述
      2.传值…
  3. … …

6. 所以到底应该怎么写呢?

在上一节, 我们给出了非常装逼的解决方案, 同时也是最本质的解决方案, 但是我没把它写在最后一节, 是因为它并不是一个适合用于工程中的写法。接下来, 让我们暂且忘掉前面装逼的代码。回到我们的业务需求上, 看一下最适当的解决方案。Talk is cheap, show you the code!

  • 定义了一个二维数组, 将它传给 printArray()函数的时候希望限定它不要被修改。
  1. 传值。
    在这里插入图片描述

  2. 传引用。
    在这里插入图片描述


从C/C++的故事中走出来, 我不禁感慨:历史给他们套上沉重的镣铐,他们戴着镣铐跳舞,却依旧无所不能,永远不停地追逐着计算机世界中所剩不多的那一些浪漫。在他们身上,即使是缺陷,也是这样美丽的。

这大概,就是世界上最好的语言 (狗头)

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页