PAYATHON 2026

为什么 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 强制转换来消除诊断。强制转换不会建立这两种类型之间所缺少的安全保证,函数仍可能把只读对象的地址写入调用方的可写指针。

备注:内容仅供参考。