因为常年从事 iOS 开发,很多 c 语言的相关知识并没有机会深入了解。最近的加班比较少,特地整理了一篇关于 const 的文字以飨读者。
本文将会简单地通过示例代码 猜测 llvm 的编译处理过程。
被 const 修饰的局部变量
让我们通过一份实例代码来快速验证一下,const 修饰的变量真的是常量吗?
void test0() {
const int const_int_0 = 9; // 声明并定义常量
const int *j = &const_int_0; // 声明并定义指向常量的指针
int *k = (int *)j; // 强制转换指针类型
*k = 200; // 修改指向区域的值
printf("%d\n", const_int_0); // 输出 9
printf("%d\n", *j); // 输出 200
}
仔细读一遍上面的代码,很容易产生两个疑惑。
- 被
const
修饰的值是否能够被修改?
- 如果被修改了,直接打印时,为什么仍然输出初始值?
关于第一个疑惑,实际上,被 const
修饰的值,只会在编译期保证其值不被修改。程序运行时,通过特定方法是可能修改值的(修改常量的值十分危险,下面会举例说明)。
上面的代码通过强制转换的方式修改了常量的值。
下面,开始解答第二个问题。
在编译时,编译器发现 const_int_0
是真·常量。所以,会把后面使用到该常量的地方直接进行替换。printf("%d\n", const_int_0);
→ printf("%d\n", 9);
也就是起到了类似于 define
的作用。
下面是该函数在 -OS
下的汇编代码。(发布应用时,默认的优化选项为-OS
,含义为:在尽可能的不加大代码体积的情况下,尽可能的优化代码)
// 调用 _printf 前需要准备一些参数。
// 寄存器 r0 保存第一个参数="%d\n",寄存器 r1 保存第二个参数=需要打印的值
.private_extern _test0
.globl _test0
.align 1
.code 16
.thumb_func _test0
_test0:
Lfunc_begin0:
.loc 1 13 0
.cfi_startproc
push {r4, r7, lr} //先保存后面用到的寄存器的值,该函数调用结束后,会恢复这些值。
add r7, sp, #4 r7 = sp +4
Ltmp0:
.loc 1 20 3 prologue_end
movw r4, :lower16:(L_.str-(LPC0_0+4))
movs r1, #9 // 寄存器r1 的值为9,第一次打印的值,编译后为常量
movt r4, :upper16:(L_.str-(LPC0_0+4))
LPC0_0:
add r4, pc // r4 = pc+r4;
mov r0, r4 // r0 = r4
blx _printf //打印
.loc 1 20 3
mov r0, r4 //r0 = r4
movs r1, #200 // 寄存器r1 的值为200,第二次打印的值,编译器分析代码后,发现该值也为常量
blx _printf //打印
.loc 1 21 1
pop {r4, r7, pc} //出栈,函数结束
Ltmp1:
Lfunc_end0:
.cfi_endproc
既然提到了真·常量,自然也有假·常量。下面的代码,会展示假·常量的两种情况。希望读者能自行体会之间的区别。
void test0_0() {
const int const_int_0 = arc4random_uniform(100); // 声明并通过随机数的方式初始化常量
printf("%d\n", const_int_0); // 输出72,该值为随机数
const int *j = &const_int_0; // 声明并定义指向常量的指针
int *k = (int *)j; // 强制转换指针类型
*k = 200; // 修改指向区域的值
printf("%d\n", const_int_0); // 输出 200
printf("%d\n", *j); // 输出 200
}
void test0_1(const int const_int_0) {
printf("%d\n", const_int_0); // 输出11,该值为函数的入参
const int *j = &const_int_0; // 声明并定义指向常量的指针
int *k = (int *)j; // 强制转换指针类型
*k = 200; // 修改指向区域的值
printf("%d\n", const_int_0); // 输出 200
printf("%d\n", *j); // 输出 200
}
被 const 修饰的全局变量
上面曾提及修改常量的值十分危险,下面这份代码展示了一种危险的操作情况。
const int const_int_1 = 9;// 声明并定义常量
void test1() {
const int *j = &const_int_1; // 声明并定义指向常量的指针
int *k = (int *)j; // 强制转换指针类型
*k = 200;// 修改指向区域的值
// 该行代码会产生 `EXC_BAD_ACCESS` 错误。错误代码: `KERN_PROTECTION_FAILURE` (禁止写操作)
printf("%d\n", const_int_1);
printf("%d\n", *j);
}
实际上,编译器检测到全局的真·常量后,会把它放到只读区,当尝试修改内容时,系统会抛出异常。
.private_extern _const_int_1 @ @const_int_1
.section __TEXT,__const
.globl _const_int_1
.align 2
_const_int_1:
.long 9 @ 0x9
那么,该如何避免它被放到只读区域呢?
很简单,通过 __attribute__((section(...)))
显式地指定编译后所在的区域即可。
const int const_int_0 __attribute__((section("_DATA,myConst"))) = 1119; // 声明并定义常量