C语言中的字符串指针和字符数组

10

我刚开始学习C语言,对于字符串指针和字符数组这些概念感到有些混淆。有没有人能帮我稍微澄清一下呢?

// source code
char name[10] = "Apple";
char *name2 = "Orange";

printf("the address of name: %p\n", name);
printf("the address of the first letter of name: %p\n", &(*name));
printf("first letter of name using pointer way: %c\n", *name);
printf("first letter of name using array way: %c\n", name[0]);
printf("---------------------------------------------\n");
printf("the address of name2: %p\n", name2);
printf("the address of the first letter of name2: %p\n", &(*name2));
printf("first letter of name2 using pointer way: %c\n", *name2);
printf("first letter of name2 using array way: %c\n", name2[0]);

// output
the address of name: 0x7fff1ee0ad50
the address of the first letter of name: 0x7fff1ee0ad50
first letter of name using pointer way: A
first letter of name using array way: A
---------------------------------------------
the address of name2: 0x4007b8
the address of the first letter of name2: 0x4007b8
first letter of name2 using pointer way: O
first letter of name2 using array way: O

我认为name和name2都指向它们自己的第一个字母的地址。那么我的困惑是(请参见下面的代码)

//code
char *name3; // initialize a char pointer
name3 = "Apple"; // point to the first letter of "Apple", no compile error

char name4[10]; // reserve 10 space in the memory
name4 = "Apple"; // compile errorrrr!!!!!!!!!!

我创建了一个名为name2的字符指针,并将name2指向"Apple"的第一个字母,这很好。然后我创建了另一个字符数组并在内存中分配了10个空间。接着我尝试使用name4,它是一个指向"Apple"第一个字母的地址。结果,编译错误。

我对这种编程语言感到非常沮丧。有时它们的工作方式相同,但有时它们却不同。有人能解释一下原因吗?如果我真的想在不同行上创建一个字符串或字符数组,我该怎么做呢?

非常感谢...


你有没有考虑学习Python、Java或C#呢?对于初学者来说,它们是更好的编程语言。 - Patashu
14
@Patashu的评论非常糟糕。这个评论的水平比任何一种语言都低。 - Jonathon Reinhart
1
@eded 数组和指针之间的区别是一些汇编程序员容易被绊倒的东西(因为它们都只是地址!),但我会让其中一个 C 语言专家回答(他们可能会为您引用规范)。 - Jonathon Reinhart
我非常感激并感到惊喜,许多朋友都在这里尝试帮助。非常感谢大家 :) - eded
@AndreyT 很棒的链接,谢谢。是的,这个主题很复杂。 - Steven Lu
显示剩余5条评论
5个回答

10
您可以在声明数组时初始化它,就像这样:

int n[5] = { 0, 1, 2, 3, 4 };
char c[5] = { 'd', 'a', 't', 'a', '\0' };

但是由于我们通常将char数组看作字符串,C语言允许一种特殊情况:

char c[5] = "data";  // Terminating null character is added.

然而,一旦您声明了一个数组,就无法重新分配它。 为什么?考虑以下赋值:

char *my_str = "foo";  // Declare and initialize a char pointer.
my_str = "bar";        // Change its value.

第一行声明了一个char指针,并将其“指向”foo中的第一个字母。由于foo是一个字符串常量,它与其他所有常量一样存储在内存中的某个位置。当您重新分配指针时,您正在为其分配一个新值:即bar的地址。但原始字符串foo保持不变。您已经移动了指针,但没有改变数据。

然而,当您声明一个数组时,您根本没有声明一个指针。您只是预留了一定数量的内存并给它命名。因此,下面这行代码:

char c[5] = "data";

以字符串常量data开头,然后分配5个新字节,称之为c,并将字符串复制到其中。您可以像声明指针一样精确地访问数组的元素;在大多数情况下,数组和指针是可互换的。

但是,由于数组不是指针,因此您无法重新分配它们。

您无法使c“指向”其他位置,因为它不是指针;它是内存区域的名称。

您可以逐个字符地更改字符串的值,或通过调用像strcpy()这样的函数来更改字符串的值:

c[3] = 'e';       // Now c = "date", or 'd', 'a', 't', 'e', '\0'
strcpy(c, "hi");  // Now c = 'h', 'i', '\0', 'e', '\0'
strcpy(c, "too long!") // Error: overflows into memory we don't own.

效率提示

注意,初始化数组通常会复制数据:原始字符串从常量区域复制到数据区域,在那里您的程序可以更改它。当然,这意味着您将使用比预期多两倍的内存。您可以通过声明指针来避免复制并通常节省内存。这将使字符串保留在常量区域,并仅为指针分配足够的内存,而不管字符串的长度。


1
是的,我认为它会复制字符串而不是指向字符串常量,这就是为什么我们在那里使用 strcpy 的原因。 - eded

2
当你说
char *name3 = "Apple";

你正在声明name3指向静态字符串"Apple"。如果你熟悉高级语言,你可以把它想象成不可变的(我会在这个上下文中解释,因为听起来你之前已经编程过;有关技术原理,请查看C标准)。
当你说:
char name4[10];
name4 = "Apple";

你会得到一个错误,因为你首先声明了一个包含10个字符的数组(换句话说,你正在“指向”可变内存的10字节部分的开头),然后尝试将不可变值“Apple”赋给该数组。在后一种情况下,实际数据分配发生在某个只读内存段中。
这意味着类型不匹配:
error: incompatible types when assigning to type 'char[10]' from type 'char *'

如果你想让name4的值为"Apple",使用strcpy函数:

strcpy(name4, "Apple");

如果您想让name4的初始值为"Apple",也可以这样做:
char name4[10] = "Apple"; // shorthand for {'A', 'p', 'p', 'l', 'e', '\0'}

这个例子能够正常工作,而你之前的例子不能,是因为"Apple"char[]的有效数组初始化器。换句话说,你正在创建一个10字节的char数组,并将其初始值设置为"Apple"(其余位置为0)。
如果你考虑一个int数组,这可能更容易理解:
int array[3] = {1, 2, 3}; // initialise array

我能想到的最简单的口语解释是,数组是一组用于存放东西的桶,而静态字符串"Apple"则是一个单独的东西。

strcpy(name4, "Apple")之所以有效,是因为它将"Apple"中的每个东西(字符)逐个复制到name4中。

然而,说“这组桶等同于那个东西”是没有意义的。只有通过“填充”值来使桶有意义。


谢谢,一开始我明白了,然后有点困惑。为什么我不能将不可变的值分配给此数组,因为 char name4 [10] =“苹果” 有效,而为什么要使用 strcpy 使其有效?任何进一步的解释都将非常有帮助。 - eded
@eded - 我添加了一些额外的澄清。希望能有所帮助。 - sapi

2

你不能直接将值重新分配给数组类型(例如,包含10个char的数组name4)。对于编译器来说,name4是一个“数组”,你不能使用赋值= 运算符用字符串字面量写入数组。

为了将字符串“Apple”的内容实际移动到你为其分配的十个字节的数组(name4)中,你必须使用strcpy()或类似的函数。

而你对name3所做的事情则大不相同。它被创建为char *并初始化为垃圾值或零(此时你不确定)。然后,你将指向静态字符串“Apple”的地址赋值给它。这是一个存在于只读内存中的字符串,试图写入name3指针所指向的内存永远不可能成功。

基于此,你可以推断出最后一条语句试图将静态字符串的内存位置赋值给其他某个代表10个char的地方。该语言不提供预先确定的方法执行此任务。

这就是它的强大之处。


谢谢您的回复。正如您所说,如果我不能为数组类型分配一个值,那么为什么我可以这样做 -> char name[10] = "Apple";? - eded
1
你可以用这种方式将数组初始化为字符串,但之后不能再使用它进行赋值。这是一个非常好的问题,很容易让人感到困惑。实际上,= 运算符在第一次对变量使用时与第二次使用时并不完全相同。这显然会让初学者感到困惑,但答案涉及对汇编语言的理解。 - Steven Lu

2
尽管指针和数组看起来很相似,但它们是不同的。 char *name3 只是一个指向 char 的指针,它所占用的内存不比一个指针多。它只是存储了一个地址,因此您可以将一个字符串分配给它,然后存储在 name3 中的地址将更改为 "Apple" 的地址。
但是,name4 是一个 char[10] 数组,它保存了 10 个字符的内存。如果要对其进行赋值,需要使用 strcpy 或其他方法来写入其内存,而不是像 "Apple" 一样分配一个地址。

1
我希望看到的是字符数组和字符指针之间的区别。 - Jonathon Reinhart

0

我认为这也会有所帮助:

int main() {
    char* ptr   = "Hello";
    char  arr[] = "Goodbye";

    // These both work, as expected:
    printf("%s\n", ptr);
    printf("%s\n", arr);
    printf("%s\n", &arr);   // also works!

    printf("ptr  = %p\n", ptr);
    printf("&ptr = %p\n", &ptr);
    printf("arr  = %p\n", arr);
    printf("&arr = %p\n", &arr);

    return 0;
 }

输出:

Hello
Goodbye
Goodbye
ptr  = 0021578C         
&ptr = 0042FE2C         
arr  = 0042FE1C         \__ Same!
&arr = 0042FE1C         /

我们可以看到arr == &arr。由于它是一个数组,编译器知道您总是想要第一个字节的地址,无论如何使用。

arr是一个7+1字节的数组,位于main()堆栈上。编译器生成指令来保留这些字节,然后用“Goodbye”填充它。没有指针!

另一方面,ptr是一个指针,一个4字节的整数,也在堆栈上。这就是为什么&ptr非常接近&arr的原因。但它所指向的是一个静态字符串(“Hello”),该字符串位于可执行文件的只读部分(这就是为什么ptr的值是一个非常不同的数字)。


你正在依赖未定义的行为。printf("%s\n", &arr) 对你来说可能有效,但标准并不保证它能正常工作。&arr 的类型是 char(*)[8],即“指向包含 8 个字符的数组的指针”。不能保证它会按照你期望的方式正确转换为 char* - Benjamin Lindley
真的,和yech。数组的名称评估为其第一个元素的地址。这就是为什么*(arr+1)有效的原因。在名称之前使用额外的&是多余的、非标准的和令人困惑的。 - Adam Liss
@BenjaminLindley 谢谢大家 - 说得对。Visual Studio 没有报错,但我没有在 gcc -Wall 中尝试过。我认为我的错误和你们的评论一起非常有帮助,可以更好地理解这个问题,所以我决定不改动它。 - Jonathon Reinhart

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