有一种情况,当你通过指针传递参数时,添加或删除const
限定符会导致严重的错误。这种情况需要特别注意。
以下是一个简单的例子,展示了可能出现的问题。这段代码在C中存在问题:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#undef strncpy
char* (*const fp1)(char*, const char*, size_t) = strncpy;
char* (*const fp2)(char*, char*, size_t) = strncpy;
char* (*const fp3)(const char*, const char*, size_t) = strncpy;
const char* const unmodifiable = "hello, world!";
int main(void)
{
fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );
fputs( unmodifiable, stdout );
return EXIT_SUCCESS;
}
问题在于
fp3
。这是一个指向接受两个
const char*
参数的函数的指针。然而,它指向了标准库调用
strncpy()
¹,其第一个参数是一个它修改的缓冲区。也就是说,
fp3(dest, src, length)
具有承诺不修改
dest
指向的数据的类型,但是它将参数传递给
strncpy()
,从而修改了该数据!这只有因为我们改变了函数的类型签名才有可能发生。
尝试修改字符串常量是未定义行为-我们有效地告诉程序调用
strncpy("hello, world!", "Whoops!", sizeof("hello, world!"))
-并且在我测试过的几个不同编译器上,它将在运行时默默失败。
任何现代 C 编译器都应该允许将值赋给
fp1
,但会警告你使用
fp2
或
fp3
时会自食其果。在 C++ 中,如果没有使用
reinterpret_cast
,
fp2
和
fp3
行根本无法编译。添加显式转换使编译器假定你知道自己在做什么并消除了警告,但由于其未定义的行为,程序仍然会失败。
const auto fp2 =
reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
const auto fp3 =
reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);
这不会出现在传递值的参数中,因为编译器会复制它们。将传递值的参数标记为
const
只意味着函数不希望修改其临时副本。例如,如果标准库内部声明了
char* strncpy(char* const dest, const char* const src, const size_t n)
,则无法使用K&R习语
*dest++ = *src++;
。这会修改函数的参数临时副本,而我们已经声明为
const
。由于这不会影响程序的其余部分,所以C不介意您在函数原型或函数指针中添加或删除
const
限定符。通常,您不会在头文件中将它们作为公共接口的一部分,因为它们是实现细节。
¹虽然我以strncpy()
作为具有正确签名的众所周知的函数的例子,但它已被弃用。