字符串字面值是常量吗?

30

无论是使用GCC还是Clang,即使在使用许多严格的选项(例如-Wall -W -pedantic -std=c99)时,我将字符串文字赋给一个char*也不会出现警告:

char *foo = "bar";

当我将 const char* 赋值给 char* 时,他们(当然)会抱怨。

这是否意味着字符串字面值被认为是 char* 类型?它们不应该是 const char* 吗?如果它们被修改了,那就不是定义行为!

还有一个无关的问题,命令行参数(例如: argv)又是怎样的呢?它被认为是字符串字面值数组吗?


5
如果你希望它发出警告,请使用“-Wwrite-strings”。在g++中,相当于设置这个选项。 - moinudin
8个回答

26

它们的类型是 char[N],其中 N 是字符数(包括终止符 \0)。因此,您可以将它们分配给 char*,但仍然不能写入它们(效果将是未定义的)。

关于 argv:它指向字符串指针数组。这些字符串是明确可修改的。您可以更改它们,它们需要保持最后存储的值。


8
+1 -- 注意这是它们的类型,但你必须将它们视为“const”(否则未定义的行为/访问违规妖精会来找你麻烦)。 - Billy ONeal
3
“@Billy:你不觉得这有些不一致吗?否则,原帖的作者就不需要问这个问题了。” - Vlad
3
@Vlad:我没有写标准 :P - Billy ONeal
1
@Vlad,C++委员会已经禁止将字符串字面值转换为char*。在C++0x中这样做将成为不合法的操作。 - Johannes Schaub - litb
6
C语言自C89就开始有const关键字了,它在C99中并没有新增。 - Johannes Schaub - litb
显示剩余5条评论

10
为了完整起见,C99草案标准(C89和C11的措辞类似)在第6.4.5节“字符串字面量第5段”中说:
[...] 从字符串文字或文本得到的每个多字节字符序列都附加了一个值为零的字节或代码。然后使用多字节字符序列来初始化具有静态存储期和足以包含该序列的长度的数组。对于字符字符串字面量,数组元素的类型为char,并用多字节字符序列的各个字节进行初始化; [...]
因此,这表明一个“字符串字面量”具有静态存储期(即程序的生命周期),其类型为char [](而不是char *),其长度为附加零的字符串字面量的大小。*第6段说:
如果程序尝试修改这样的数组,则行为是未定义的。
尝试修改字符串常量是未定义行为,无论它们是否为const
关于第5.1.2.2.1节中的argv程序启动段落2说:
如果声明了它们,则main函数的参数应遵守以下约束条件: [...] -程序可以修改参数argc和argv以及argv数组指向的字符串,并在程序启动和程序终止之间保留它们的最后存储值。
因此,argv不被视为字符串常量数组,可以修改argv的内容。

6
使用-Wwrite-strings选项,您将获得以下结果:
warning: initialization discards qualifiers from pointer target type

不论哪种选项,GCC都会将文字常量放入只读内存段,除非使用-fwritable-strings命令显式指定(然而这个选项在最新的GCC版本中已经被删除)。
命令行参数不是const类型,它们通常存在于栈上。

5

很抱歉,我刚注意到这个问题标记为c而不是c++。也许我的答案并不那么相关!

字符串字面值不完全是constnot-const,对于字面值有一个特殊的奇怪规则。

(总结: 字面值可以通过引用数组作为foo(const char(&)[N])来获取,并且不能作为非const数组获取。它们更倾向于衰减为const char *。到目前为止,它们似乎是const。但是,有一个特殊的遗留规则允许文字转化为char *。请参见下面的实验。)

(以下实验使用-std = gnu ++ 0x的clang3.3进行。也许这是一个C++11问题?还是特定于clang?无论如何,有些奇怪的事情正在发生。)

首先,字面值似乎是const

void foo( const char  * ) { std::cout << "const char *" << std::endl; }
void foo(       char  * ) { std::cout << "      char *" << std::endl; }

int main() {
        const char arr_cc[3] = "hi";
        char arr_c[3] = "hi";

        foo(arr_cc); // const char *
        foo(arr_c);  //       char *
        foo("hi");   // const char *
}

这两个数组的表现符合预期,说明foo能够告诉我们指针是否为const。然后"hi"选择了fooconst版本。所以似乎就这样解决了:字面常量是const的……是吧?
但是,如果你删除void foo( const char * ),情况就会变得奇怪。首先,调用foo(arr_c)在编译时会出错,这是意料之中的。但是,通过非const调用方式,字面常量调用(foo("hi"))却能正常工作。
因此,与arr_c相比,字面常量“更加const”(因为它们倾向于衰减为const char *,与arr_c不同)。但是,字面常量相对于arr_cc来说“不那么const”,因为如果需要的话,它们愿意衰减为char *。(当它衰减为char *时,Clang会发出警告。)
但是,关于衰减呢?为了简单起见,我们可以避免它。让我们将这两个数组作为引用传递到foo中。这会给我们带来更“直观”的结果:
void foo( const char  (&)[3] ) { std::cout << "const char (&)[3]" << std::endl; }
void foo(       char  (&)[3] ) { std::cout << "      char (&)[3]" << std::endl; }

与之前相同,字面量和常量数组(arr_cc)使用 const 版本,非 const 版本由 arr_c 使用。如果我们删除 foo(const char (&)[3]),那么 foo(arr_cc)foo("hi") 都会出现错误。简而言之,如果我们避免指针衰减并改用引用数组,则字面量的行为就像它们是 const
模板中,系统将推导出 const char * 而不是 char *,您需要接受这一点。
template<typename T>
void bar(T *t) { // will deduce   const char   when a literal is supplied
    foo(t);
}

基本上来说,字面量始终表现得像 const,除非您直接使用字面量初始化char *

2
在C89和C99中,字符串字面量的类型为char *(出于历史原因,据我了解)。您是正确的,尝试修改其中一个会导致未定义的行为。GCC具有特定的警告标志-Wwrite-strings(不包括-Wall),如果您尝试这样做,它将向您发出警告。
至于argv,参数被复制到您的程序地址空间中,并且可以在main()函数中安全地进行修改。 编辑:哎呀,不小心复制了-Wno-write-strings。更新为正确的(正面)警告标志。

3
字符串字面量是数组类型(char [N]),而不是指针类型(char *)。请参考 n1256 中的 6.4.5 字符串字面量 - John Bode

2

Johannes的回答涵盖了类型和内容方面的正确性。但除此之外,修改字符串字面量的内容是未定义的行为。

关于你对argv的问题:

程序可以修改参数argcargv,以及argv数组指向的字符串,并在程序启动和终止之间保留它们的最后存储值。


2

字符串文字具有形式类型char [],但语义类型为const char []。纯粹主义者讨厌它,但这通常是有用且无害的,除了会带来很多新手到SO并提出“为什么我的程序崩溃?!?”的问题。


1
让我很好奇,你有这方面的实用例子吗?我的意思是除了这个向后兼容的想法之外,这个想法似乎越来越不相关了? - Jens Gustedt
2
有时候,你想要一个 char * 而不是一个 const char * 来迭代一个字符串,特别是如果你将会把它的指针传递给像 strtol 这样的函数。这不仅仅是为了向后兼容,更多的是因为 const 有时候很麻烦。当然,在像这样的情况下,大部分时间我也需要处理非字面字符串,并且只是强制转换掉 const... - R.. GitHub STOP HELPING ICE
@R.. 我认为使用const char *进行迭代甚至将其传递给strtol()并没有问题。毕竟它不是一个const char const * - Morty
@morty:strtolendptr 参数采用char **而不是const char **。相关的函数也一样。如果你有一个const char *p,你不能传递&pstrtol;相反,你必须将该值复制到char *tmp中(使用强制类型转换来删除指向类型中的const),传递&tmp,然后再将结果复制回去。 - R.. GitHub STOP HELPING ICE
特别要注意,这里不能传递(char **)&p; 这样做会导致未定义的行为(别名违规)。 - R.. GitHub STOP HELPING ICE
显示剩余2条评论

1

它们是const char*,但对于在const出现之前存在的遗留代码将它们分配给char*有特定的排除规则。而命令行参数绝对不是字面量,它们是在运行时创建的。


不确定为什么你被踩了。我做了一些实验来确认它们并不完全是const或non-const。我会发布我的答案。 - Aaron McDaid
这是不正确的。在C中它们不是const,但在C++中是。正如其他人所指出的那样,它们通常是不可写的,但它们并不像常量一样行为。在C中有一些奇怪的事情是你不能做的,比如'case "abc"[2]:',但在C++中你可以,因为字符串字面值是常量。这可能会对某些宏产生很大的影响。 - Preston Crow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接