C语言中,char数组和char指针有什么区别?

279
我正在尝试理解C语言中指针的概念,但目前我对以下内容感到困惑:

  • char *p = "hello"
    

    这是一个指向以h开头的字符数组的字符指针。

  • char p[] = "hello"
    

    这是一个存储hello的数组。

当我将这两个变量都传入此函数时,有何区别?

void printSomething(char *p)
{
    printf("p: %s",p);
}

7
这样是不合法的:char p[3] = "hello";初始化字符串对于你声明的数组大小来说太长了。打错字了吗? - Cody Gray
22
或者只需要 char p[]="hello"; 就足够了! - deepdive
1
可能是在C语言中,char s[]和char *s有什么区别?的重复问题。确实,这也特别询问了函数参数,但这并不是char特定的。 - Ciro Santilli OurBigBook.com
2
你需要理解它们在本质上是不同的。唯一的共同点是数组p[]的基础是一个const指针,这使得可以通过指针访问数组p[]。 p[]本身保存了一个字符串的内存,而*p只是指向已分配字符串的第一个字符的地址。 为了更好地说明这一点,请考虑以下内容: char *cPtr = {'h','e','l','l','o', '\0'}; ==> 这是错误的,因为cPtr只是一个字符的指针 char cBuff[] = {'h', 'e','l','l','o','\0'}; ==> 这是正确的,因为cBuff本身就是一个字符数组 - Ilavarasan
这个回答解决了你的问题吗?char s[]和char *s有什么区别? - TylerH
显示剩余2条评论
8个回答

279

char*char[]是不同类型,但在某些情况下并不立即明显。这是因为数组会退化为指针,也就是说,如果提供了一个char[]类型的表达式,而期望的是char*类型,则编译器会自动将数组转换为指向其第一个元素的指针。

你的示例函数printSomething期望传入一个指针,所以如果你像这样尝试将一个数组传递给它:

char s[10] = "hello";
printSomething(s);
编译器会假装你写了这个代码:
char s[10] = "hello";
printSomething(&s[0]);

1
从2012年到现在有什么变化吗?对于一个字符数组"s",打印整个数组...即:"hello"。 - Bhanu Tez
4
@BhanuTez 不,数据的存储方式和对数据的处理是两个不同的问题。这个例子会打印整个字符串,因为 printf 函数会按照 %s 格式化字符串的方式进行处理:从提供的地址开始并一直打印直到遇到 null 终止符。如果想要只打印一个字符,可以使用 %c 格式化字符串。 - jacobq
1
想问一下 char *p = "abc"; 这个语句中是否会自动添加空字符 \0,就像 char [] 数组一样? - ajaysinghnegi
为什么我可以设置 char *name; name="123";,但是不能使用 int 类型做同样的事情?并且在使用 %c 打印 name 后,输出是无法读取的字符串: - TomSawyer

104

让我们来看看:

#include <stdio.h>
#include <string.h>

int main()
{
    char *p = "hello";
    char q[] = "hello"; // no need to count this

    printf("%zu\n", sizeof(p)); // => size of pointer to char -- 4 on x86, 8 on x86-64
    printf("%zu\n", sizeof(q)); // => size of char array in memory -- 6 on both

    // size_t strlen(const char *s) and we don't get any warnings here:
    printf("%zu\n", strlen(p)); // => 5
    printf("%zu\n", strlen(q)); // => 5

    return 0;
}

foo*和foo[]是不同的类型,编译器会以不同的方式处理它们(指针=地址+指针类型的表示,数组=指针+数组长度(如果已知),例如,如果数组是静态分配的),详细信息可以在标准中找到。在运行时级别上,它们没有区别(在汇编语言中,几乎没有区别,见下文)。

此外,在C FAQ中有一个相关的问题

Q: What is the difference between these initializations?

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

My program crashes if I try to assign a new value to p[i].

A: A string literal (the formal term for a double-quoted string in C source) can be used in two slightly different ways:

  1. As the initializer for an array of char, as in the declaration of char a[] , it specifies the initial values of the characters in that array (and, if necessary, its size).
  2. Anywhere else, it turns into an unnamed, static array of characters, and this unnamed array may be stored in read-only memory, and which therefore cannot necessarily be modified. In an expression context, the array is converted at once to a pointer, as usual (see section 6), so the second declaration initializes p to point to the unnamed array's first element.

Some compilers have a switch controlling whether string literals are writable or not (for compiling old code), and some may have options to cause string literals to be formally treated as arrays of const char (for better error catching).

See also questions 1.31, 6.1, 6.2, 6.8, and 11.8b.

References: K&R2 Sec. 5.5 p. 104

ISO Sec. 6.1.4, Sec. 6.5.7

Rationale Sec. 3.1.4

H&S Sec. 2.7.4 pp. 31-2


在sizeof(q)中,为什么q不会像@Jon在他的回答中提到的那样衰变为指针? - garyp
@garyp q不会衰变为指针,因为sizeof是一个运算符,而不是一个函数(即使sizeof是一个函数,只有当函数期望一个char指针时,q才会衰变)。 - GiriB
谢谢,但是我认为应该使用 printf("%u\n" 而不是 printf("%zu\n",所以你应该去掉 z。 - Zakaria

59
在C语言中,char数组和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 "初始化" 给出了一个直接的例子:

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

3
“@leszek.hanusz Undefined Behaviour https://dev59.com/63E85IYBdhLWcg3wdDOz Google "C language UB" ;-)” 的意思是:“@leszek.hanusz 未定义行为 https://dev59.com/63E85IYBdhLWcg3wdDOz 搜一下“ C语言UB”;-)” - Ciro Santilli OurBigBook.com

11

您不允许更改字符串常量的内容,这就是第一个p所指向的内容。第二个p是用字符串常量初始化的数组,您可以更改其内容。


9
针对这种情况,效果是相同的:你最终会传递一个字符串中第一个字符的地址。
但声明显然不同。
下面的代码为字符串和字符指针分配了内存,然后将指针初始化为指向字符串中的第一个字符。
char *p = "hello";

以下代码为字符串保留了一定的内存空间,因此实际上可以使用更少的内存。
char p[10] = "hello";

然而,对于数组来说,初始化变量会带来巨大的性能和空间惩罚。 - leef
@leef:我认为这取决于变量的位置。如果它在静态内存中,那么数组和数据可能会存储在EXE映像中,而不需要任何初始化。否则,如果数据必须被分配,然后复制静态数据,则肯定会更慢。 - Jonathan Wood

6

来自APUE,第5.14节:

char    good_template[] = "/tmp/dirXXXXXX"; /* right way */
char    *bad_template = "/tmp/dirXXXXXX";   /* wrong way*/

对于第一个模板,名称被分配在堆栈上,因为我们使用一个数组变量。对于第二个名称,我们使用指针。在这种情况下,仅指针本身的内存驻留在堆栈上;编译器安排字符串存储在可执行文件的只读段中。当mkstemp函数尝试修改字符串时,会发生段错误。

3
据我所记,数组实际上是指针的一组集合。例如:
p[1]== *(&p+1)

这是一个正确的声明。


2
我会将数组描述为指向一块内存地址的指针。这就是为什么 *(arr + 1) 可以让你访问到 arr 的第二个成员。如果 *(arr) 指向一个32位的内存地址,例如 bfbcdf5e,那么 *(arr + 1) 就指向 bfbcdf60(第二个字节)。这也是为什么如果操作系统没有发生段错误,超出数组范围会导致奇怪的结果。如果 int a = 24; 存储在地址 bfbcdf62,那么访问 arr[2] 可能会返回 24,前提是没有发生段错误。 - Braden Best

2

char p[3] = "hello"?应该是char p[6] = "hello",请记住在C语言中,“字符串”末尾有一个'\0'字符。

无论如何,在C语言中,数组只是指向内存中相邻对象的第一个对象的指针。唯一的区别在于语义。虽然您可以更改指针的值以指向内存中的不同位置,但创建后,数组将始终指向同一位置。
此外,在使用数组时,“new”和“delete”会自动完成。


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