我试图理解 C 中的指针,但我目前对以下内容感到困惑:
char *p = "hello"
这是一个指向字符数组的 char 指针,从h开始。
char p[] = "hello"
这是一个存储hello的数组。
当我将这两个变量都传递给这个函数时有什么区别?
void printSomething(char *p)
{
printf("p: %s",p);
}
char*
并且char[]
是不同的类型,但并非在所有情况下都立即显现。这是因为数组衰减为指针,这意味着如果在需要类型char[]
之一的地方提供类型表达式char*
,编译器会自动将数组转换为指向其第一个元素的指针。
您的示例函数printSomething
需要一个指针,因此如果您尝试像这样将数组传递给它:
char s[10] = "hello";
printSomething(s);
编译器假装你写了这个:
char s[10] = "hello";
printSomething(&s[0]);
让我们来看看:
#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[] 是不同的类型,编译器对它们的处理方式不同(指针 = 地址 + 指针类型的表示,数组 = 指针 + 数组的可选长度,如果已知,例如,如果数组是静态分配的),详细信息可以在标准中找到。并且在运行时它们之间没有区别(在汇编程序中,差不多,见下文)。
问:这些初始化有什么区别?
char a[] = "string literal"; char *p = "string literal";
如果我尝试为 p[i] 分配一个新值,我的程序就会崩溃。
答:字符串文字(C 源代码中双引号字符串的正式术语)可以以两种略有不同的方式使用:
- 作为 char 数组的初始值设定项,就像在 char a[] 的声明中一样,它指定了该数组中字符的初始值(如果需要,还指定其大小)。
- 在其他任何地方,它都会变成一个未命名的静态字符数组,并且这个未命名的数组可能存储在只读存储器中,因此不一定可以修改。在表达式上下文中,数组像往常一样立即转换为指针(参见第 6 节),因此第二个声明初始化 p 以指向未命名数组的第一个元素。
一些编译器有一个开关来控制字符串文字是否可写(用于编译旧代码),有些编译器可能有选项可以使字符串文字被正式视为 const char 数组(以便更好地捕获错误)。
另见问题 1.31、6.1、6.2、6.8 和 11.8b。
参考文献:K&R2 Sec。5.5 页。104
ISO 秒。6.1.4,秒。6.5.7
理由二。3.1.4
H&S秒。2.7.4 第 31-2 页
C中的char数组与char指针有什么区别?
C99 N1256 草案
字符串字面量有两种不同的用法:
初始化char[]
:
char c[] = "abc";
这是“更神奇”,并在 6.7.8/14“初始化”中描述:
字符类型的数组可以由字符串字面量初始化,可选地用大括号括起来。字符串文字的连续字符(如果有空间或数组大小未知,则包括终止的空字符)初始化数组的元素。
所以这只是一个捷径:
char c[] = {'a', 'b', 'c', '\0'};
像任何其他常规数组一样,c
可以修改。
在其他任何地方:它会生成:
所以当你写:
char *c = "abc";
这类似于:
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
char[]
请注意从to的隐式转换char *
,这始终是合法的。
那么你修改了c[0]
,你也修改__unnamed
了,也就是UB。
这记录在 6.4.5“字符串文字”中:
5 在翻译阶段 7 中,将一个字节或零值代码附加到每个由一个或多个字符串文字产生的多字节字符序列。然后使用多字节字符序列来初始化一个静态存储持续时间和长度刚好足以包含该序列的数组。对于字符串文字,数组元素的类型为 char,并使用多字节字符序列的各个字节初始化 [...]
6 如果这些数组的元素具有适当的值,则未指定这些数组是否不同。如果程序尝试修改这样的数组,则行为未定义。
6.7.8/32“初始化”给出了一个直接的例子:
例 8:声明
char s[] = "abc", t[3] = "abc";
定义“普通”字符数组对象
s
,t
其元素用字符串字面量初始化。此声明与
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
数组的内容是可修改的。另一方面,声明
char *p = "abc";
定义
p
类型为“pointer to char”并将其初始化为指向长度为 4 的类型为“char 数组”的对象,其元素使用字符串字面量进行初始化。如果尝试使用p
来修改数组的内容,则行为未定义。
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
section 中,而不是.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
您不能更改字符串常量的内容,这是第一个p
指向的内容。第二个p
是一个用字符串常量初始化的数组,你可以改变它的内容。
对于这样的情况,效果是一样的:您最终传递了字符串中第一个字符的地址。
声明显然不一样。
下面为字符串和字符指针留出内存,然后将指针初始化为指向字符串中的第一个字符。
char *p = "hello";
而以下仅为字符串留出内存。所以它实际上可以使用更少的内存。
char p[10] = "hello";
来自APUE,第 5.14 节:
char good_template[] = "/tmp/dirXXXXXX"; /* right way */
char *bad_template = "/tmp/dirXXXXXX"; /* wrong way*/
...对于第一个模板,名称是在堆栈上分配的,因为我们使用了一个数组变量。但是,对于第二个名称,我们使用指针。在这种情况下,只有指针本身的内存驻留在堆栈上;编译器安排将字符串存储在可执行文件的只读段中。当
mkstemp
函数试图修改字符串时,会发生分段错误。
引用的文字与@Ciro Santilli 的解释相匹配。
据我所知,数组实际上是一组指针。例如
p[1]== *(&p+1)
是一个真实的陈述
char p[3] = "hello"
? 应该char p[6] = "hello"
记住 C 中“字符串”的末尾有一个 '\0' 字符。
无论如何,C 中的数组只是指向内存中调整对象的第一个对象的指针。唯一不同的 s 在语义上。虽然您可以更改指针的值以指向内存中的不同位置,但数组在创建后将始终指向同一位置。
同样在使用数组时,会自动为您完成“新建”和“删除”。