如何理解 C++ 顶层和底层 const 下复杂类型
建站提交历史文章,原文写作时间 2023 年 6 月 15 日。
前言
关于多级指针中的 const 修饰初学者都会觉得是一个很玄学的问题,我也是初学者,在深入理解后在此留下一些自己的理解,使用更多的示例与图示用这篇短文来展示理解的过程。同时也发现在这一块文章也不多,因此希望此文能对你有帮助。
top-level-const:顶层常量,指指针本身是常量。low-level-const:底层常量,指指针指向的量是常量。
本文将从 const 指针、const 数组指针两个角度带你理解顶层和底层常量修饰。
12 月 17 日更:最近在阅读 C++ Primer 11,其中也非常详细的介绍了顶层和底层 const。(书落实验室不便标注页码)
玄学 const 指针
1 | int *p; // 指向变量的指针变量 |
玄学数组指针
1 | int *p[4]; // 指向 int 的指针数组, sizeof(*p) == sizeof(int) |
如何理解 const 指针
我们以 const int * const p 来从编译器的角度解释这个变量声明是如何被解析的。
编译器是从右往左解析这串声明的。事实上很多的表达式对于编译器而言都是从右往左解析的,尽管大多数是从左往右,这里不展开。
- 首先,编译器知道我们声明了一个变量
p,但因为编译器从右往左解析,它目前不知道p是任意数据类型。 - 编译器解析到了第二个
const,因此p是一个const修饰类型。 - 解析到
*,因此const p是一个指针类型,但是目前我们只能说它是一个指针,但不知道它指向何种数据类型。可以认为是void * const p。 - 注意这时解析到
int时已经开始确定void * const p指向的内容了,即确定void的类型,这里确定了void * const p指向一个int类型。 - 最后解析到第一个
const,因此最后得到void * const p指向const int类型。
flowchart RL 1(void * const p) --> 2(const int)
看到这里,那么你应该也可以自己解释上面的一些其他例子了吧,后面还会举一些更复杂的例子。
const int * const ** const p
const int | * const | * | * const p (| 仅用于理解)
flowchart RL 1(void * const p) --> 2(void *) --> 3(void * const) --> 4(const int)
const int **** p
const int | * | * | * | * p (| 仅用于理解)
flowchart RL 1(void * p) --> 2(void *) --> 3(void *) --> 4(void *) --> 5(const int)
可见可以把 * 当作解释的分隔符从右往左解释变量的申明。
如何理解数组指针
首先我们需要知道, [] 的优先级高于 * ,并且 [] 是自左向右结合的
我们现在从 int (*p)[4] 入手解释如何理解数组指针,通过之前对于玄学指针的解释应该可以自行从编译器角度理解这段申明,下面我想从方法论的角度分析如何理解。
考虑到 [] 是向左结合的,并且具有比 * 高的优先级,我们可以把 [] 翻译成向右结合的形式方便更直观的理解,上面的示例 int (*p)[4] 可以转化为 int[4]*p(将 [] 放到紧贴着前面的括号的前面) 。这样可以很直观的看到:p 首先是一个指针,指向一个包含 4 个元素的数组,数组包含的元素是 int 。
flowchart RL
1(void * p) --> 2("[4]") --> 3(int)
后面再来举一些更复杂的例子吧。
const int p[4]
const int | [4] p
flowchart RL
1("p[4]") --> 2(const int)
const int * const (* const p[4])[5][6]
const int | * const | [6] | [5] | * const | [4] p
flowchart RL
1("p[4]") --> 2(void * const) --> 3("[5]") --> 4("[6]") --> 5(void * const) --> 6(const int)
const char *(*p)[10][3]
const char * | [3] | [10] | * p
flowchart RL
1(void * p) --> 2("[10]") --> 3("[3]") --> 4(const char *)
多级指针的应用
最后还是想简单介绍一下多级指针的应用,在 C 中没有封装的概念,因此许多用法必须要多级指针的支持;在 C++ 中,即使支持多级的封装,但是许多提供的接口同时支持 C/C++,因此依然是 C 的。
例如,我想要存储一张 的表格,表格存储字符串,并且现在要求表格是静态的、字符串长度是动态的。那么就需要类似上一个示例 char *p[10][3] ;此时我又要求传入的指针在接口内可以任意修改,那么就必须申明为 char *(*p)[10][3] ;如果要求表格的行数得是动态的而列数是静态的,那么就必须申明为 char *(**p)[3] ;现在要求全部是动态的,char ****p,最后这种形式其实是十分常用的形式。
