如果 char*s 是只读的,为什么我可以覆盖它们?

If char*s are read only, why can I overwrite them?

我的课程告诉我,char*s 只是 static/read,所以我认为这意味着您不能在定义它们之后对其进行编辑。但是当我 运行:

char* fruit = "banana";
printf("fruit is %s\n", fruit);
fruit = "apple";
printf("fruit is %s\n", fruit);

然后它编译正常并给我:

fruit is banana
fruit is apple

为什么?我是否误解了只读的含义?很抱歉,如果这很明显,但我是编码新手,我无法在线找到答案。

提供的代码片段不会更改字符串文字本身。它仅更改存储在指针 fruit.

中的值

你可以想象这些线条

char* fruit = "banana";
fruit = "apple";

以下方式

char unnamed_static_array_banana[] = { 'b', 'a', 'n', 'a', 'n', 'a', '[=11=]' };
char *fruit = &unnamed_static_array_banana[0];
char unnamed_static_array_apple[]  = { 'a', 'p', 'p', 'l', 'e', '[=11=]' };
fruit = &unnamed_static_array_apple[0];

这些语句不会更改对应于字符串文字的数组。

另一方面,如果你会尝试写

char* fruit = "banana";
printf("fruit is %s\n", fruit);
fruit[0] = 'h';
^^^^^^^^^^^^^^
printf("fruit is %s\n", fruit);

也就是说,如果您尝试使用指向字符串文字(指向字符串文字的第一个字符)的指针更改字符串文字,则程序将具有未定义的行为。

来自 C 标准(6.4.5 字符串文字)

7 It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined.

你的课程教给你的是正确的!

当您首先定义 char* fruit = "banana" 时,您基本上将 fruit 作为指向 常量字符 的指针。字符串的 7 个字节(包括空终止符)位于目标文件的 .ro 部分(部分名称显然会因平台而异)。

当您将 char 指针 fruit 重置为 "apple" 时,它只是指向只读部分中包含 "apple"

的另一个内存位置

本质上,当你说 fruit 是一个 常量 时,它指的是 fruit 是指向 const 内存的指针。如果您将其定义为 const pointer to a const string :-
char* const fruit = "banana";
编译器会阻止您将其重置为 "apple"

基本上,当你执行

char* fruit = "banana";

您设置了一个指针 fruit 指向 "banana" 的第一个字母。打印出来时,C 基本上从 'b' 开始并一直打印字母,直到它在末尾遇到一个 [=13=] 空字符。

到时候说

fruit = "apple";

您已将指针 fruit 更改为现在指向 "apple"

的第一个字母

首先,char* 不是只读的。 char * const 是。而且它们不同于char const *。文字字符串(例如 "banana")应该是,但不一定是。

char * const  cpfruit = "banana";
cpfruit = "apple";        // error

char const * cpfruit = "banana";
cpfruit[0] = 'x';        // error

char * ncfruit = "banana";
ncfruit[0] = 'x';        // compile will allow, but may cause run-time error.

您正在将变量 fruit 指向不同的字符串。您只是在覆盖地址(位置)。编译器将看到您的常量字符串 "banana" 和 "apple" 并将它们分别存储在程序内存中。假设字符串 "banana" 进入位于地址 1 的内存单元,并且 "apple" 存储到内存地址 2。现在当你这样做时:

fruit = "banana";

编译器只会将 1 分配给变量 fruit,这意味着它指向包含字符串 banana 的地址 1。当你这样做时:

fruit = "apple";

编译器将分配2变量fruit,这意味着它指向存储字符串apple的地址2

在你的程序中,表达式"banana"表示程序图像中的一个字符串文字对象,一个字符数组。表达式的值是 char * 或 "pointer to character" 类型。指针指向该数组的第一个字节,字符 'b'.

你的 char *fruit 变量也有类型 "pointer to character" 并从这个表达式中获取它的初始值:它被初始化为指向数据的指针的副本,而不是数据本身;它仅仅指向 b.

当您将 "apple" 分配给 fruit 时,您只是将其指针值替换为另一个指针值,因此它现在指向不同的文字数组。

要修改数据本身,您需要一个表达式,例如:

char *fruit = "banana";
fruit[0] = 'z';  /* try to turn "banana" into "zanana" */

根据 ISO C 标准,未定义此行为。 可能 "banana" 数组是只读的,但这不是必需的。

C 实现可以使字符串文字可写,或使其成为一个选项。

(如果您能够修改字符串文字,那并不意味着一切都很好。首先,根据 ISO C,您的程序仍然没有很好地定义:它不可移植。其次,C 编译器允许将具有共同内容的文字合并到同一存储中。这意味着 "banana" 在程序中的两次出现实际上可能是完全相同的数组。此外,字符串文字 "nana" 出现在某处该程序可能是出现在其他地方的数组 "banana" 的后缀;换句话说,共享相同的存储空间。修改文字可能会产生令人惊讶的效果;修改可以出现在其他文字中。)

另外 "static" 和 "read-only" 不是同义词。 C 中的大多数静态存储实际上是可修改的。我们可以创建一个可修改的静态字符数组,其中包含这样一个字符串:

/* at file scope, i.e. outside of any function */
char fruit[] = "banana";

或:

{
  /* in a function */
  static fruit[] = "banana";

如果我们省略数组大小,它会根据初始化字符串文字自动调整大小,并包括 space 作为空终止字节。在函数中,我们需要static将数组放入静态存储,否则我们得到一个局部变量。

这些数组可以修改; fruit[0] = 'z' 是定义明确的行为。

此外,在这些情况下,"banana" 不表示字符数组。数组就是变量fruit"banana" 表达式只是指示数组初始值的一段语法:

char *fruit = "banana";  // "banana" is an object in program image
                         // initial value is a pointer to that object

char fruit_array[] = "apple"; // "apple" is syntax giving initial value

fruit 对象是可写的 - 它可以设置为指向不同的字符串文字。

字符串文字 "banana""apple" 不可写。您可以修改 fruit 以指向字符串文字,但如果您这样做,则不应尝试修改 fruit 指向 的内容:

char *fruit = "banana"; // fruit points to first character of string literal
fruit = "apple";        // okay, fruit points to first character of different string literal
*fruit = 'A';           // not okay, attempting to modify contents of string literal
fruit[1] = 'P';         // not okay, attempting to modify contents of string literal

尝试修改字符串文字的内容会导致未定义的行为 - 您的代码可能会按预期运行,或者您可能会遇到运行时错误,或者可能会发生完全意外的事情。为了安全起见,如果您要定义一个指向字符串文字的变量,您应该声明它 const:

const char *fruit = "banana";  // can also be written char const *

您仍然可以分配 fruit 以指向不同的字符串:

fruit = "apple";

但是如果你试图修改 fruit 指向的内容,编译器就会对你大喊大叫。

如果你想定义一个只能指向一个特定字符串文字的指针,那么你也可以const限定指针:

const char * const fruit = "banana"; // can also be written char const * const

这样,如果您尝试写入 fruit 指向的内容,或尝试将 fruit 设置为指向不同的对象,编译器将对您大喊大叫。

当您使用 char *p="banana"; 时,字符串 banana 存储在只读内存位置。随后,当您输入 p="apple"; 时,字符串 apple 存储在其他内存位置,指针现在指向新的内存位置。

您可以在每次分配后打印 p 来确认这一点。

#include<stdio.h>
int main(void)
{
    char *p = "Banana";
    printf("p contains address of string constant 'Banana' at 0x%p\n", p);

    p="Apple";
    printf("p contains address of string constant 'Apple' at 0x%p\n", p);

}

定义 C 字符串时,又名 char 数组,用双引号 "...",格式如下:

char * <varName> = "<someString>"

只有数组的元素是不可变的(它们的内容不能改变)。换句话说,<varName> 具有 const char * 类型(指向只读存储器的可变指针)。每次用双引号 <varName> = "<otherString>" 调用赋值运算符时,它都会自动更改指针值。下面的例子应该给出不同可能性的完整概述:

#include <stdio.h>

int main()
{
    char * var_1 = "Lorem";
    printf("1. %s , %p\n", var_1, var_1); // --> 1. Lorem , 0x400640

    var_1 = "ipsu";
    printf("2. %s , %p\n", var_1, var_1); // --> 2. ipsu , 0x400652

    // var_1[0] = 'x'; // --> Segmentation fault

    var_1++;
    printf("3. %s , %p\n", var_1, var_1); // --> 3. psu , 0x400653

    char var_2[] = {'L', 'o', 'r', 'e', 'm', '[=11=]'};
    printf("4. %s , %p\n", var_2, var_2); // --> 4. Lorem , 0x7ffed0fc5381

    var_2[0] = 'x';
    printf("5. %s , %p\n", var_2, var_2); // --> 5. xorem , 0x7ffed0fc5381

    // var_2++; //error: lvalue required as increment operand

    char var_3[] = "Lorem";
    printf("6. %s , %p\n", var_3, var_3); // --> 6. Lorem , 0x7ffe36a42d5c

    // var_3 = "ipsu"; // --> error: assignment to expression with array type

    var_3[0] = 'x';
    printf("7. %s , %p\n", var_3, var_3); // --> 7. xorem , 0x7ffe36a42d5c

    char * const var_4 = "Lorem";

    // var_4 = "ipsu"; // --> error: assignment of read-only variable

    // var_4[0] = 'x'; // --> Segmentation fault

    char const * var_5 = "Lorem";
    printf("8. %s , %p\n", var_5, var_5); // --> Lorem , 0x400720

    var_5 = "ipsu";
    printf("9. %s , %p\n", var_5, var_5); // --> ipsu , 0x400732

    // var_5[0] = 'x'; // --> error: assignment of read-only location

    const char * var_6 = "Lorem";
    printf("10. %s , %p\n", var_6, var_6); // --> 10. Lorem , 0x400760

    var_6 = "ipsu";
    printf("11. %s , %p\n", var_6, var_6); // --> 11. ipsu , 0x400772

    // var_6[0] = 'x'; // --> error: assignment of read-only location

    const char const * var_7 = "Lorem"; // clang only --> warning: duplicate 'const' declaration specifier [-Wduplicate-decl-specifier]
    printf("12. %s , %p\n", var_7, var_7); // --> 12. Lorem , 0x400760

    var_7 = "ipsu";
    printf("13. %s , %p\n", var_7, var_7); // --> 13. ipsu , 0x400772

    // var_7[0] = 'x'; // --> error: assignment of read-only location

    char const const * var_8 = "Lorem"; // clang only --> warning: duplicate 'const' declaration specifier [-Wduplicate-decl-specifier]
    printf("14. %s , %p\n", var_8, var_8); // --> 14. Lorem , 0x400790

    var_8 = "ipsu";
    printf("15. %s , %p\n", var_8, var_8); // --> 15. ipsu , 0x4007a2

    // var_8[0] = 'x'; // --> error: assignment of read-only location

    char const * const var_9 = "Lorem";

    // var_9 = "ipsu"; // --> error: assignment of read-only variable

    // var_9[0] = 'x'; // --> error: assignment of read-only location

    const char var_10[] = {'L', 'o', 'r', 'e', 'm', '[=11=]'};

    // var_10[0] = 'x'; // --> error: assignment of read-only location

    // var_10++; // --> error: lvalue required as increment operand

    char const var_11[] = {'L', 'o', 'r', 'e', 'm', '[=11=]'};

    // var_11[0] = 'x'; // --> error: assignment of read-only location 

    // var_11++; // --> error: lvalue required as increment operand

    const char var_12[] = "Lorem";

    // var_12[0] = 'x'; // --> error: assignment of read-only location

    // var_12++; // --> error: lvalue required as increment operand

    char const var_13[] = "Lorem";

    // var_13[0] = 'x'; // --> error: assignment of read-only location

    // var_13++; // --> error: lvalue required as increment operand


    return 0;
}

此代码已在 GCC、Clang 和 Visual Studio 上测试。

基本上有3种可能:

  • 不可变指针,可变内容

    • char <varName>[] = {'L', 'o', 'r', 'e', 'm', '[=17=]'};
    • char <varName>[] = "Lorem";
  • 指针可变,内容不可变

    • char * <varName> = "Lorem";
    • char const * <varName> = "Lorem";
    • const char * <varName> = "Lorem";
    • const char const * <varName> = "Lorem";
    • char const const * <varName> = "Lorem";
  • 不可变指针,不可变内容

    • char * const <varName> = "Lorem";
    • char const * const <varName> = "Lorem";
    • const char * const <varName> = "Lorem";
    • const char <varName>[] = {'L', 'o', 'r', 'e', 'm', '[=27=]'};
    • char const <varName>[] = {'L', 'o', 'r', 'e', 'm', '[=28=]'};
    • const char <varName>[] = "Lorem";
    • char const <varName>[] = "Lorem";

结论:

  • <typing> <varName>[] = <string> 总是 returns 不可变指针,内容的可变性独立于 <array> 格式("Lorem"{'L', 'o', 'r', 'e', 'm', '[=34=]'}
  • <typing> * <varName> = "someString"始终returns不可变内容
  • <typing> * const <varName> = "someString"总是returns不可变内容和指针
  • char const <other>char const <other>const char const <other>char const const <other> 始终创建不可变内容。

我试图详细总结 C 的数组行为 here