const的四种位置,含义完全不同

前两天在调一块 imx6ull 的板子,写一个 I2C 从设备的驱动,编译的时候 gcc 报了一堆 warning,全是 assignment of read-only location 之类的。我心想不对啊,这个变量我明明是想让它能在初始化的时候赋值、后面运行的时候只读,怎么一赋值就报错。

别急,咱们今天就把 const 这个关键字的位置问题掰扯清楚。说实话,我当年学 C 的时候,const 就是我这方面太弱势的地方,抱大腿学了点皮毛,一直模模糊糊的,直到踩了几次坑才搞明白。

一、背景:我在 imx6ull 上踩的坑

事情是这样的。我在写一个传感器驱动的配置结构体,大概是这么个意思:

c 复制代码
const char *name = "tmp102";

然后代码里有个地方要改这个指针,让它指向另一个名字:

c 复制代码
name = "tmp103";   // 这里我一开始以为不行

结果编译没报错,运行也正常。但是我另一个地方写的是:

c 复制代码
char * const name = "tmp102";
name = "tmp103";   // 这个报错了

我当时就懵了------这俩看着差不多啊,怎么一个行一个不行?网上一搜,出来一堆呀,太多了,各种"const 在星号左边还是右边"的口诀,看得我头大。

后来折腾了大半天,我才真正把四种位置理清楚。这里要注意,const 的位置不同,修饰的东西完全不一样,这四种最常见:

c 复制代码
const char *p;          // 1. const 在类型前
char const *p;          // 2. const 在类型后(和 1 等价)
char * const p;         // 3. const 在星号后
const char * const p;   // 4. 两个 const

二、相关知识点:从右往左读

讲原理之前,先给个我觉得最好用的办法:从变量名开始,从右往左读。这个技巧是当年一个老同事教我的,比什么口诀都靠谱。

咱们一个一个来。

第 1 种:const char *p

从 p 往左读:p 是一个指针(*),指向 char,这个 char 是 const 的。说白了,指针本身可以改,但指向的内容不能改。

c 复制代码
const char *p = "hello";
p = "world";      // OK,指针可以改指向
// p[0] = 'H';    // 错误,内容不能改

第 2 种:char const *p

这个和第 1 种完全等价 ,只是写法不同。const 放在类型后面,修饰的还是 char。从右往左读还是一样:p 是指针,指向 const char。

面试里经常有人拿这个来绕人,其实就是同一个东西。

第 3 种:char * const p

从 p 往左读:p 是一个 const 的指针(* const),指向 char。也就是说,指针本身不能改,但指向的内容可以改。

c 复制代码
char buf[] = "hello";
char * const p = buf;
p[0] = 'H';       // OK,内容可以改
// p = buf + 1;   // 错误,指针不能改

这就是我上面踩坑的那个------char * const name 的 name 不能再指向别的地方了。

第 4 种:const char * const p

两个 const,从右往左读:p 是 const 指针,指向 const char。指针不能改,内容也不能改,彻底锁死。

c 复制代码
const char * const p = "hello";
// p = "world";   // 错误
// p[0] = 'H';    // 错误

这种最常见的地方就是字符串常量表、寄存器地址表这类东西。

顺便提一句,还有一种容易混的:

c 复制代码
const char * const *pp;   // 指向"const 指针"的指针

从右往左读:pp 是指针,指向一个 const 指针,这个指针指向 const char。这种在驱动里传二维配置表的时候会用到,我第一次见的时候盯着看了五分钟。

三、解决过程

好,原理讲完,回到我那块 imx6ull 的板子上,说说我怎么一步步把问题解决的。

1、先搞清楚我要的语义

我那个配置结构体,本意是:名字在初始化时定下来,运行中不许改。那就应该是 char * const,而不是 const char *。我一开始写反了,写成了 const char *name,结果指针能改、内容不能改,跟我想要的正好相反。

2、改完发现还是编译不过

改成 char * const name = "tmp102"; 之后,我以为万事大吉,结果 gcc 又报了:

python 复制代码
warning: initialization discards 'const' qualifier from pointer target type

实际测试还是不行呀,折腾了大半天才发现是字符串字面量的问题。"tmp102" 在 C 里是 const char[] 类型的,你拿一个 char * 去接它,编译器就警告了。正确写法应该是:

c 复制代码
const char * const name = "tmp102";

既保证内容只读(字符串字面量本来就不该改),又保证指针只读。

3、顺手把整个结构体也 const 掉

后来我干脆把整个结构体变量声明成 const:

c 复制代码
static const struct tmp102_config cfg = {
    .name = "tmp102",
    .addr = 0x48,
};

这样编译期就能把它放到 .rodata 段,跑起来更安全。验证一下,用 objdump 看看段:

bash 复制代码
arm-linux-gnueabihf-objdump -h vmlinux | grep rodata

输出里能看到 .rodata 段确实有内容,说明常量被正确放到了只读段。要是你发现它跑到 .data 段去了,那多半是某处 const 漏了。

4、编译警告全清零

最后重新 make,之前那堆 assignment of read-only location 和 discards 'const' qualifier 的 warning 全没了。这里要注意,warning 不能当没看见,很多时候它就是在提醒你语义写反了。

四、结果和一点遗留

现在驱动跑起来正常,I2C 读写都通,配置结构体也稳稳地待在只读段里。

遗留问题嘛,我目前也就学了 7788,也不敢乱说。比如 const 加在函数返回值上、加在函数参数上,还有 volatile const 这种组合,我还没完全吃透。特别是 volatile const,在寄存器操作里偶尔会见到,那个语义挺微妙的,等哪天再踩坑了咱们单独聊。

参考资料方面,主要是翻了《C 和指针》里讲 const 的那一章,还有 gcc 手册里关于类型限定符的说明,感谢这些无私奉献的作者们。有错误欢迎留言交流,咱们一起把这个关键字彻底搞明白。

相关推荐
Zevalin爱灰灰1 天前
Linux入门第二部分(ARM裸机篇) 第二章 Cortex-A7 MPCore架构与ARM汇编基础
linux·汇编·arm开发·嵌入式
Zwawa1 天前
串口命令如何从字符串长成协议:帧头、长度、校验和与状态机
c语言·单片机·嵌入式
小李不想当小白1 天前
BKP备份寄存器与RTC实时时钟(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
La Pulga1 天前
【FreeRTOS源码学习笔记】链表的实现
数据结构·笔记·stm32·mcu·链表·嵌入式·freertos
凉、介1 天前
深入理解 ARMv7-A | Generic Timer
笔记·学习·嵌入式·arm
La Pulga1 天前
【FreeRTOS源码学习笔记】任务创建
笔记·stm32·mcu·学习·嵌入式·freertos·实时操作系统
Zevalin爱灰灰2 天前
Linux入门第二部分(ARM裸机篇) 第一章 开发环境搭建与了解
linux·arm开发·嵌入式
菜鸟小芯2 天前
【Quectel Pi M2 实战】Day3 USB 摄像头图像采集:拍照、录像与踩坑记录(V4L2 + FFmpeg)
ffmpeg·嵌入式
金銀銅鐵3 天前
[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化
java·源码·ai编程