C字符指针问题

4
如果我们声明char * p="hello";,那么由于它写在数据段中,我们不能修改p指向的内容,但是我们可以修改指针本身。但我在《C陷阱与缺陷》一书中发现了这个例子。 作者:Andrew Koenig AT&T贝尔实验室 新泽西州默里山市07974
char *p, *q;
p = "xyz";
q = p;
q[1] = ’Y’;

q将指向包含字符串xYz的内存。p也是如此,因为p和q指向相同的内存。

如果我提到的第一个语句是真的,那么它是怎么成立的呢?类似地,我运行了以下代码:

main()
{
char *p="hai friends",*p1;
p1=p;
while(*p!='\0') ++*p++;
printf("%s %s",p,p1);
}

并且得到了输出结果:ibj!gsjfoet

请解释一下在这两种情况下我们是如何修改内容的? 提前致谢。


修改字符串字面量的内容会导致未定义行为,这意味着任何事情都有可能发生。 - Prasoon Saurav
我们需要多少个字符串字面量问题? - James Morris
11个回答

5

在我的系统上,您的相同示例会导致分段错误。

您在此处遇到了未定义的行为。请注意,.data(请注意,字符串文字也可能在.text中)不一定是不可变的 - 这取决于操作系统和编译器,机器不一定会通过页表写保护该内存。


你使用了什么操作系统/编译器? - drfrogsplat
我也遇到了一个段错误;在WinXP上使用g++。 - Aman Aggarwal

4
只有您的操作系统可以保证数据段中的内容是只读的,即使这样也涉及设置段限制和访问标志以及使用远指针等操作,因此并不总是这样做。C本身没有这种限制;在平面内存模型(几乎所有32位操作系统现在都使用),您地址空间中的任何字节都可能是可写的,甚至包括代码段中的内容。如果您有一个指向main()的指针,一些机器语言知识,以及一个已经正确设置的操作系统(或者说,未能防止它),则有可能将其重写为返回0。请注意,这种黑魔法很少有人故意这样做,但这是C成为系统编程强大语言的一部分。

3

1
main()
{
int i = 0;
char *p= "hai friends", *p1;
p1 = p;
while(*(p + i) != '\0')
    {
        *(p + i);
        i++;
    }
printf("%s %s", p, p1);
return 0;
}

这段代码将会输出:hai friends hai friends。

在 while 循环中,*(p + i) 是不必要的。 - Marko Mandic

1

这将取决于编译器是否支持。

x86是冯·诺伊曼结构(与哈佛结构相对),因此在基本层面上“数据”和“程序”内存之间没有明显的区别(即编译器不会强制将程序内存和数据内存分开,因此不一定会将任何变量限制为其中之一)。

因此,一个编译器可能允许修改字符串,而另一个则不允许。

我猜想,一个更加宽松的编译器(例如MS Visual Studio C++编译器中的cl)将允许这样做,而一个更加严格的编译器(例如gcc)则不会。如果您的编译器允许这样做,那么很有可能它实际上正在将您的代码更改为类似以下的内容:

...
char p[] = "hai friends";
char *p1 = p;
...
// (some disassembly required to really see what it's done though)

也许是出于“好意”,让新的C/C++编码者在编码时减少限制和混淆的错误。(这是否是一件“好事”还有很多争议,我将在此帖子中大多数时间保持我的观点 :P)
顺便问一下,你用的是哪个编译器?

x86确实有将内存页面标记为只读的能力,因此在.data和.text之间存在区别,即对于应用程序,.text几乎永远不会开启写入权限。 - nategoose

1
在早期,当K&R在他们的书“C编程语言”中描述的C语言是“标准”时,您所描述的内容是完全可以接受的。事实上,一些编译器为了使字符串字面量可写而跳过了许多障碍。它们会在初始化时将字符串从文本段复制到数据段。
即使现在,gcc也有一个标志来恢复这种行为:-fwritable-strings

0

你的程序在我的系统(Windows + Cygwin)上也可以运行。然而,标准规定不应该这样做,尽管后果未定义。

以下摘自《C语言参考手册》第5版第33页:

您永远不应尝试修改保存字符串常量字符的内存,因为它们可能是只读的。

char p1[] = "Always writable";
char *p2 = "Possibly not writable";
const char p3[] = "Never writable";

p1行总是有效的;p2行可能有效,也可能导致运行时错误;p3行总是导致编译时错误。


0

p 实际上指向只读内存。将值分配给 p 所指向的数组的结果可能是未定义的行为。仅仅因为编译器让你这样做并不意味着它是可以的。

看一下 C-FAQ 中的这个问题:comp.lang.c FAQ list · Question 1.32

问:这些初始化之间有什么区别?

char a[] = "string literal";
char *p  = "string literal";

如果我尝试给p[i]赋一个新值,我的程序会崩溃。
A: 字符串字面值(C源代码中双引号字符串的正式术语)可以以两种略微不同的方式使用:
1.作为char数组的初始化器,在char a[]的声明中,它指定了该数组中字符的初始值(如果需要,还有其大小)。
2.在任何其他地方,它会变成一个未命名的静态字符数组,并且这个未命名的数组可能存储在只读内存中,因此不能保证可以修改。在表达式上下文中,该数组立即被转换为指针,与通常情况一样(参见第6节),因此第二个声明将p初始化为指向未命名数组的第一个元素。
一些编译器具有控制字符串字面值是否可写的开关(用于编译旧代码),并且一些编译器可能具有选项,使字符串字面值被正式视为const char数组(以获得更好的错误捕获)。

0

修改字符串字面值是一个不好的想法,但这并不意味着它不能工作。

有一个非常好的理由不要这样做:编译器允许多个实例指向同一个内存块。因此,如果在代码的其他地方定义了"xyz",你可能会无意中破坏其他代码,而其他代码则期望它是常量。


0

虽然在您的系统上修改字符串文字可能是可能的,但这是您平台的怪癖,而不是语言的保证。实际的C语言对.data部分或.text部分一无所知。那都是实现细节。

在某些嵌入式系统上,您甚至没有文件系统来包含带有.text部分的文件。在某些这样的系统上,您的字符串文字将存储在ROM中,尝试写入ROM只会导致设备崩溃。

如果您编写依赖于未定义行为并且仅适用于您的平台的代码,则可以保证,早晚会有人认为将其移植到某个不按您预期方式工作的新设备是个好主意。当发生这种情况时,一群愤怒的嵌入式开发人员将追捕并刺伤您。


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