C语言:字符指针和字符数组的区别

158
考虑:
char amessage[] = "now is the time";
char *pmessage = "now is the time";

我从C程序设计语言,第二版中读到,上述两个语句的作用不同。
我一直认为数组是一个方便的方式来操作指针以存储一些数据,但这显然不是这种情况...在C语言中,数组和指针之间有哪些“非平凡”的区别?

3
我可能记错了,但我想指出可以在指针上使用[]符号,在数组上使用*符号。从代码的角度看,唯一的重要区别是amessage的值不能改变,因此amessage ++会失败(但我相信 *(amessage+1)会成功)。我相信内部还有其他差异,但它们几乎从不真正影响到代码。 - Bill K
2
哦,一般来说(不包括你提到的情况),数组会自动分配内存,而指针则需要你自己分配内存。你的两个变量都应该只是指向在程序加载时分配的内存块。 - Bill K
1
除了K&R(顺便说一句,这是一本很棒的书),我建议你在此期间阅读http://pw2.netcom.com/~tjensen/ptr/cpoint.htm。 - amaterasu
请查看https://dev59.com/WGkw5IYBdhLWcg3wBmCA#10186799。 - Pacerier
由于我们已经有两个关于同一个问题的“规范”FAQ主题,因此将其标记为重复。 - Lundin
可能相关:https://dev59.com/0YXca4cB1Zd3GeqPEixi - Mohit Jain
14个回答

173
这是一个假设性的内存映射,展示了这两个声明的结果。
                0x00  0x01  0x02  0x03  0x04  0x05  0x06  0x07
    0x00008000:  'n'   'o'   'w'   ' '   'i'   's'   ' '   't'
    0x00008008:  'h'   'e'   ' '   't'   'i'   'm'   'e'  '\0'
        ...
amessage:
    0x00500000:  'n'   'o'   'w'   ' '   'i'   's'   ' '   't'
    0x00500008:  'h'   'e'   ' '   't'   'i'   'm'   'e'  '\0'
pmessage:
    0x00500010:  0x00  0x00  0x80  0x00

字符串字面量 "now is the time" 存储在内存地址 0x00008000 处的一个包含16个字符的数组中。这块内存可能是不可写的,最好假设它是不可写的。您永远不应该尝试修改字符串字面量的内容。
声明:
char amessage[] = "now is the time";

在内存地址0x00500000处分配一个16个元素的char数组,并将字符串字面值的内容复制到其中。这块内存是可写的;你可以随心所欲地更改amessage的内容。
strcpy(amessage, "the time is now");

The declaration

char *pmessage = "now is the time";

在内存地址0x00500010上分配一个指向char的单个指针,并将字符串字面量的地址复制到其中。

由于pmessage指向字符串字面量,因此不应将其用作需要修改字符串内容的函数的参数:

strcpy(amessage, pmessage); /* OKAY */
strcpy(pmessage, amessage); /* NOT OKAY */
strtok(amessage, " ");      /* OKAY */
strtok(pmessage, " ");      /* NOT OKAY */
scanf("%15s", amessage);    /* OKAY */
scanf("%15s", pmessage);    /* NOT OKAY */

如果你将 pmessage 改为指向 amessage,等等。
pmessage = amessage;

然后它可以在任何可以使用amessage的地方使用。

12
@John Bode,非常棒的回答 :)。 - Mahesh
2
最后一行并不完全正确:如果作为 sizeof message&pmessage 使用,其行为是不同的。 - M.M
1
@zen:将pmessage设置为指向另一个字符串将绝对不会影响存储在0x0000800处的字符串字面量的内容。该字符串字面量的存储空间直到程序退出才会被释放。 - John Bode
2
@Zen:你的朋友们是错误的;像 amessage 这样的数组 不是 指针。数组 对象 不会在任何地方存储地址(从我的答案中的内存映射应该很清楚)。相反,除非它们是一元运算符 &sizeof 的操作数,否则数组 表达式 将“衰减”为指针类型(因此 sizeof 的行为差异)。 - John Bode
1
@Zen:查看此答案以获取更多详细信息。 - John Bode
显示剩余20条评论

117
没错,但这是一个微妙的区别。本质上,前者:
char amessage[] = "now is the time";

定义一个数组,其成员存在于当前作用域的堆栈空间中,而:
char *pmessage = "now is the time";

定义了一个指针,它存在于当前作用域的堆栈空间中,但引用了其他地方的内存(在这里,“现在是时候”存储在其他地方的内存中,通常是一个字符串表)。
此外,需要注意的是,由于第二个定义(显式指针)所属的数据不存储在当前作用域的堆栈空间中,因此它的存储位置是未指定的,不应进行修改。
正如Mark、GMan和Pavel指出的那样,当对这些变量中的任何一个使用取地址运算符时,也会有所不同。例如,&pmessage返回类型为char**的指针,或者指向指针的指针,而&amessage返回类型为char(*)[16]的指针,或者指向16个字符数组的指针(与char**类似,需要进行两次解引用,正如litb所指出的)。

7
虽然是真的,但这并不是最大的区别。例如,&amessage和&pmessage之间有什么区别? - Mark Ransom
2
"&pmessage" 将会是 "pmessage" 的地址,位于堆栈中某个地方。同样地,"&amessage" 也将是数组在堆栈中的地址,与 "amessage" 相同。然而,"&amessage" 的类型与 "amessage" 不同。 - GManNickG
5
不,它不是未定义的。区别在于&pmessage的类型是char ** - 指向指针的指针,而&amessage的类型是char (*) [16] - 指向16个字符数组的指针。这两种类型不兼容(特别是第二种类型仅仅是字符串中第一个字符的地址,而第一种类型是存储第一个字符地址的变量的地址)。 - Pavel Minaev
1
@Bill:不,因为数组版本实际上只是数组实例化的一种快捷方式。因此,数组在堆栈中分配,然后加载字符串的数据。 - Walt W
“堆栈空间”与此问题无关。有些系统甚至没有堆栈;即使在有堆栈的系统上,如果这些声明在文件范围内,也不会使用堆栈空间。 - M.M
显示剩余13条评论

13

一个数组包含元素。指针指向这些元素。

前者是说的一种简略形式。

char amessage[16];
amessage[0] = 'n';
amessage[1] = 'o';
...
amessage[15] = '\0';

也就是说,它是一个包含所有字符的数组。特殊的初始化会为你初始化它,并自动确定其大小。数组元素是可修改的-您可以覆盖其中的字符。

第二种形式是指针,只是指向这些字符。它不直接存储字符。由于数组是字符串字面值,因此您不能获取指针并写入指针所指向的位置。

char *pmessage = "now is the time";
*pmessage = 'p'; /* undefined behavior! */

这段代码可能会在你的电脑上崩溃。但它可以做任何它想做的事情,因为它的行为是未定义的。


先生,我想问一件事,我能在printf中使用%d打印字符吗?我曾认为可以,因为它们默认会提升为整数,但是这个答案说不行,我很困惑,如果您能澄清这个问题,那将是极大的帮助。http://stackoverflow.com/a/41349954/5473170 - Suraj Jain

7

我对其他答案没有有益的补充,但要说明的是,在《深入C语言秘籍》一书中,Peter van der Linden详细介绍了这个例子。如果你正在问这些问题,我认为你会喜欢这本书。


附注:你可以给pmessage赋新值。你不能给amessage赋新值;它是不可变的


@Norman,肯定有这本书的免费版本吧? - Pacerier

7
如果数组在定义时已经确定了其大小,sizeof(p)/sizeof(type-of-array)将返回数组中元素的数量。该公式常用于计算数组的长度。

1
所有其他答案都关注“指向字符串字面地址与将字符串字符复制到数组中”,这是有效的,但具体适用于OP的示例代码。它们都没有提及这个(sizeof()的不同结果),这在我看来是数组和指针之间非常重要的区别。 - Nicolas Miari

4
区别char指针和数组 C99 N1256草案 字符字符串字面值有两种不同的用法:
  1. 初始化 char[]:

    char c[] = "abc";      
    

    这是“更多的魔法”,并在6.7.8/14“初始化”中描述:

    字符类型的数组可以通过字符字符串文字进行初始化,可选地包含在大括号中。 字符串文字的连续字符(包括终止空字符,如果有空间或数组大小未知)初始化数组的元素。

    所以这只是一个快捷方式:

    char c[] = {'a', 'b', 'c', '\0'};
    

    像任何其他常规数组一样,c可以被修改。

  2. 在其他地方:它生成一个:

    因此,当您编写:

    char *c = "abc";
    

    这类似于:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;
    

    请注意从char []char *的隐式转换,这始终是合法的。

    然后,如果您修改c [0],则还会修改__unnamed,这是UB。

    这在6.4.5“字符串文字”中有记录:

    5 在第7个翻译阶段,将值为零的字节或代码附加到由字符串文字或文字产生的每个多字节字符序列。 然后使用多字节字符序列初始化具有静态存储期和长度的数组,该数组刚好足以包含该序列。 对于字符字符串文字,数组元素具有char类型,并使用多字节字符序列的各个字节进行初始化[...]

    6 未指定这些数组是否不同,只要它们的元素具有适当的值即可。 如果程序尝试修改此类数组,则行为是未定义的。

6.7.8/32 "Initialization" 给出了一个直接的例子:

EXAMPLE 8: The declaration

char s[] = "abc", t[3] = "abc";

defines "plain" char array objects s and t whose elements are initialized with character string literals.

This declaration is identical to

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

The contents of the arrays are modifiable. On the other hand, the declaration

char *p = "abc";

defines p with type "pointer to char" and initializes it to point to an object with type "array of char" with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

GCC 4.8 x86-64 ELF实现

程序:

#include <stdio.h>

int main(void) {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

编译和反编译:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

输出包含:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

结论:GCC 会将 char* 存储在 .rodata 节段中,而不是 .text
如果我们对 char[] 做同样的操作:
 char s[] = "abc";

我们得到:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

因此,它被存储在堆栈中(相对于%rbp)。

但请注意,默认的链接器脚本将.rodata.text放在同一段中,该段具有执行但没有写入权限。可以通过以下方式观察到这一点:

readelf -l a.out

其中包含:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata

4

指针只是一个保存内存地址的变量。请注意,您在操作“字符串字面值”,这是另一个问题。差异在下面进行了解释:基本上:

#include <stdio.h>

int main ()
{

char amessage[] = "now is the time"; /* Attention you have created a "string literal" */

char *pmessage = "now is the time";  /* You are REUSING the string literal */


/* About arrays and pointers */

pmessage = NULL; /* All right */
amessage = NULL; /* Compilation ERROR!! */

printf ("%d\n", sizeof (amessage)); /* Size of the string literal*/
printf ("%d\n", sizeof (pmessage)); /* Size of pmessage is platform dependent - size of memory bus (1,2,4,8 bytes)*/

printf ("%p, %p\n", pmessage, &pmessage);  /* These values are different !! */
printf ("%p, %p\n", amessage, &amessage);  /* These values are THE SAME!!. There is no sense in retrieving "&amessage" */


/* About string literals */

if (pmessage == amessage)
{
   printf ("A string literal is defined only once. You are sharing space");

   /* Demostration */
   "now is the time"[0] = 'W';
   printf ("You have modified both!! %s == %s \n", amessage, pmessage);
}


/* Hope it was useful*/
return 0;
}

1
根据您的编译器,字符串字面值可能会有不同的行为。 - Sergio

4

除了字符串“现在是时候”的内存被分配到两个不同的位置之外,您还应该记住数组名称作为指针值而不是指针变量。主要区别在于指针变量可以修改为指向其他地方,而数组则不能。

char arr[] = "now is the time";
char *pchar = "later is the time";

char arr2[] = "Another String";

pchar = arr2; //Ok, pchar now points at "Another String"

arr = arr2; //Compiler Error! The array name can be used as a pointer VALUE
            //not a pointer VARIABLE

4

第一种形式(amessage)定义了一个变量(数组),其中包含字符串"now is the time"的副本。

第二种形式(pmessage)定义了一个变量(指针),它位于与字符串"now is the time"的任何副本不同的位置。

尝试运行这个程序:

#include <inttypes.h>
#include <stdio.h>

int main (int argc, char *argv [])
{
     char  amessage [] = "now is the time";
     char *pmessage    = "now is the time";

     printf("&amessage   : %#016"PRIxPTR"\n", (uintptr_t)&amessage);
     printf("&amessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&amessage[0]);
     printf("&pmessage   : %#016"PRIxPTR"\n", (uintptr_t)&pmessage);
     printf("&pmessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&pmessage[0]);

     printf("&\"now is the time\": %#016"PRIxPTR"\n",
            (uintptr_t)&"now is the time");

     return 0;
}

您会发现,虽然&amessage等于&amessage[0],但对于&pmessage&pmessage[0]却不是这样。事实上,您会发现存储在amessage中的字符串存在于堆栈中,而由pmessage指向的字符串存在于其他地方。
最后一个printf显示了字符串字面量的地址。如果您的编译器执行"字符串池"操作,则字符串"now is the time"只有一份副本--并且您会看到它的地址与amessage的地址不同。这是因为amessage在初始化时获得字符串的副本
最终,重点是amessage将字符串存储在自己的内存中(在此示例中为堆栈),而pmessage指向存储在其他位置的字符串。

1
那是错误的。数组保存了字符串字面量的副本 - 它不是同一个数组。 - Johannes Schaub - litb
也许我表述有点含糊。让我澄清一下:有一个名为amessage的变量。有一个字符串,其内容为“现在是时候”。 amessage的地址与该字符串中“n”的地址相同。这就是我所说的关系。诚然,在程序的地址空间中可能会有其他副本的“现在是时候”,但我所说的是存储在数组中的副本。 - Dan Moulding
现在我完全明白了。感谢您进一步的解释! - Johannes Schaub - litb
@DanMoulding 我已经编辑过了,将编辑后的文本与原始文本合并。就其原貌而言,未经编辑的开头段落是具有误导性的。希望这样可以吧! - M.M
@M.M,&amessage&amessage[0] 有什么区别? - Suraj Jain
@DanMoulding amessage 不是字符数组的第一个元素的指针吗?这意味着 amessage 指向 amessage[0],所以 &amessage[0] = amessage,但是 &amessageamessage 的地址)怎么会和 amessage[0] 相同呢? - Suraj Jain

3
第二种方式将字符串分配在ELF文件的某个只读区域。 请尝试以下操作:
#include <stdio.h>

int main(char argc, char** argv) {
    char amessage[] = "now is the time";
    char *pmessage = "now is the time";

    amessage[3] = 'S';
    printf("%s\n",amessage);

    pmessage[3] = 'S';
    printf("%s\n",pmessage);
}

如果在第二个赋值语句中将pmessage[3]设为'S',则会导致段错误。


1
这是一个非常以实现为中心的解释。如果它是不针对 ELF(例如 VC++)的流行编译器呢? - Pavel Minaev
可能会得到一个段错误。这是未定义的。 - Tadeusz A. Kadłubowski

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