为什么 C 不允许将 int ** 隐式转换为 const int **?
结论
C 不允许将 int ** 隐式转换为 const int **。如果允许这种转换,一个声明为 int * 的指针可能指向真正的 const int 对象,调用方随后就可能通过它尝试修改只读对象。
你的补充理解抓住了关键,但需要区分修改发生的位置:ptptc 中的 q 是局部参数,给 *q 赋值才会修改调用方的指针变量 pa。pa 的声明类型始终是 int *,不会因为函数调用变成 const int *。
原代码中,ptcp(pa) 还有一个独立的指针层级不匹配问题,应改为 ptcp(&pa)。
为什么单层指针可以添加 const
下面的转换是合法的:
int a = 0;
int *p = &a;
const int *view = p;
通过 view 不能直接修改目标整数:
// *view = 5; // 不允许
*p = 5; // 允许:a 本身不是 const 对象
这个转换只创建了另一个指针值,既没有改变 p 保存的地址,也没有让 p 获得指向只读对象的机会。
C 的隐式转换规则允许给指针直接指向的类型添加限定符。int * 到 const int * 符合这一规则,但不能将它直接推广到任意深度的目标类型。
为什么二级指针会造成漏洞
假设允许下面这个调用:
static const int fixed = 42;
void attach(const int **q) {
*q = &fixed;
}
int main(void) {
int a = 0;
int *p = &a;
attach(&p); // 不合法:int ** 不能隐式转换为 const int **
*p = 7; // 如果上一步被放行,这里就会尝试修改 fixed
return 0;
}
单独看,attach 内的 *q = &fixed 是合法的:*q 的类型是 const int *,可以保存 &fixed。
如果允许 attach(&p),函数就能把 &fixed 写进调用方的 p。在调用方看来,p 仍然是 int *,因此 *p = 7 在类型检查层面看起来可以执行,但它实际尝试修改的是 const 对象,行为未定义。
C 在传参时拒绝这种隐式转换,从而阻止这种情况。示例中的非法调用需要编译器给出诊断;具体是警告还是错误,取决于编译器和编译选项。
分清 q、*q 和 **q
几个参数声明的区别如下:
| 参数声明 | 能否给 q 重新赋值 | 能否给 *q 重新赋值 | 能否给 **q 重新赋值 |
|---|---|---|---|
int * const *q | 可以 | 不可以 | 可以 |
int ** const q | 不可以 | 可以 | 可以 |
const int **q | 可以 | 可以 | 不可以 |
表中讨论的是类型是否允许赋值。实际解引用时,指针还必须有效,目标对象也必须允许相应访问。
对于原来的 ptptc:
void ptptc(const int **q) {
*q = &op;
q = 0;
}
*q = &op修改的是q指向的指针对象。如果调用方传入的是一个类型正确的指针变量的地址,这个操作会改变该指针的值。q = 0只修改函数内部的参数副本,不会改变调用方的指针变量。
*q = &op 本身合法,因为 &op 的类型是 int *,可以转换为 const int *。禁止 int ** 到 const int ** 的规则针对整个类型关系,不会因为某个函数当前只写入了可修改对象的地址而放宽。
原示例应该怎样修改
先修正 ptcp 的调用:
ptcp(&pa);
pa 的类型是 int *,而 ptcp 需要的是 int * const *。传入 &pa 后,实参类型是 int **,可以为它直接指向的指针类型添加 const。
这里的 const 保护的是指针对象:函数不能通过 *q 改写 pa,但仍能通过 **q 修改 a。
如果确实需要调用 ptptc,应创建类型匹配的指针变量:
int a = 0;
int *pa = &a;
const int *read_ptr = pa;
ptptc(&read_ptr);
read_ptr 的类型是 const int *,所以 &read_ptr 的类型正是 const int **。函数可以改变 read_ptr 保存的地址,但不会因此改变 pa。
如果函数只是读取整数,通常直接接收 const int * 就足够了:
int read_value(const int *p) {
return *p;
}
调用时传入 pa 即可,前提是它指向有效的整数对象。
注意事项
定义 SUCCEED 只会移除 #ifndef SUCCEED 包围的赋值语句,不会修复 main 中的参数类型问题。
也不要用 (const int **)&pa 强制转换来消除诊断。强制转换不会建立这两种类型之间所缺少的安全保证,函数仍可能把只读对象的地址写入调用方的可写指针。
备注:内容仅供参考。
