前两天在调一块 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 手册里关于类型限定符的说明,感谢这些无私奉献的作者们。有错误欢迎留言交流,咱们一起把这个关键字彻底搞明白。