严格符合标准的 main 定义能否用 const 限定 argc 和 argv?
结论
argc 和 argv 参数本身可以加顶层 const,但题中的完整定义不具备严格可移植性:
int main(const int argc, const char * const * const argv)
{
/* ... */
}
这里 argv 的内层 const 改变了所指类型,导致函数类型与标准形式 int main(int, char **) 不兼容。实现可以将它作为其他实现定义的形式支持,但依赖这种支持的程序不属于严格符合标准的程序。标准规定的启动形式见 C99 技术勘误合并草案 N1256,§5.1.2.2.1。
顶层 const 与内层 const 的区别
只给参数本身加顶层 const,可以这样写:
int main(const int argc, char ** const argv)
{
return 0;
}
C99 §6.7.5.3 ¶15 规定,判断函数类型兼容性时,忽略参数类型的顶层限定符。因此,const int 按 int 比较,char ** const 按 char ** 比较。这条规则只忽略参数类型最外层的限定符,不会递归移除所指类型中的 const。N1256,§6.7.5.3
第二个参数的几种声明有如下区别:
| 参数声明 | 与 char ** 的参数类型兼容 |
|---|---|
char **argv | 是 |
char ** const argv | 是 |
char * const *argv | 否 |
const char **argv | 否 |
const char * const * const argv | 否 |
顶层 const 在函数体内仍然有效。使用上面的定义后,不能执行 argc-- 或 argv++。通过强制转换修改这些定义为 const 的参数对象,也会产生未定义行为。C11 草案 N1570,§6.7.3 ¶6
“应当允许程序修改”保证了什么
这项规定保证,在宿主环境启动程序时,程序可以修改命令行参数及相关字符串,不必担心实现将启动字符串放在不可修改的存储中。程序可以选择不修改它们,也可以用顶层 const 限制自己对参数的操作。
标准没有要求程序必须修改这些对象,main 的函数类型要求也仍然适用。N1256,§5.1.2.2.1
采用标准形式时,下面的操作有定义:
int main(int argc, char *argv[])
{
if (argc > 0 && argv[0][0] != '\0') {
argv[0][0] = 'P';
}
return 0;
}
代码在修改前检查了字符串是否非空。修改已有字符时,仍须遵守原字符串所占存储的边界,不能越界写入。
题中的模拟调用需要分开判断
假设调用的是标准形式的 main:
const int argsno = 1;
char *args[] = { "progname", (char *)0 };
main((int)argsno, args);
C 按值传参,argc 是独立的参数对象,因此 argsno 的 const 不会让它变成只读对象。这里的 (int) 转换也没有必要。N1256,§6.9.1 ¶9 至 ¶10
args[0] 指向的则是字符串字面量。虽然字符串字面量在 C 中的类型是字符数组,修改它仍会产生未定义行为。程序自行调用 main 时,这个字面量不会获得启动参数字符串的可修改保证。如果调用过程中只读取它,传入字符串字面量本身不会导致这种未定义行为。N1570,§6.4.5 ¶7
如果需要修改字符串,应创建字符数组:
const int argsno = 1;
char progname[] = "progname";
char *args[] = { progname, (char *)0 };
main(argsno, args);
如果调用的仍是题中带内层 const 的定义,还存在类型问题:args 转换后的类型为 char **,与第二个参数的类型不满足 C 的隐式转换要求。
需要只读访问时怎么写
可以保留与标准兼容的 main 入口,在读取单个字符串时使用带 const 的指针:
int main(const int argc, char ** const argv)
{
for (int i = 0; i < argc; ++i) {
const char * const arg = argv[i];
/* 通过 arg 读取参数 */
(void)arg;
}
return 0;
}
这样既不能修改参数变量本身,也不能通过 arg 修改字符;底层启动字符串仍具有标准保证的可修改性。
备注:内容仅供参考。
