如何正确分配新的字符串值?

65

我正在尝试理解如何以最干净/最安全的方式解决这个C语言中的琐碎问题。以下是我的示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    typedef struct
    {
        char name[20];
        char surname[20];
        int unsigned age;
    } person;

    // Here I can pass strings as values...how does it work?
    person p = {"John", "Doe", 30};

    printf("Name: %s; Age: %d\n", p.name, p.age);

    // This works as expected...
    p.age = 25;

    //...but the same approach doesn't work with a string
    p.name = "Jane";

    printf("Name: %s; Age: %d\n", p.name, p.age);

    return 1;
}

编译器报错如下:

main.c: In function ‘main’: main.c:18: error: incompatible types when assigning to type ‘char[20]’ from type ‘char *’

我理解C语言(不是C++)没有String类型,而是使用字符数组,因此另一种方法是修改示例结构体以保存字符指针。
#include <stdio.h>

int main(int argc, char *argv[])
{
    typedef struct
    {
        char *name;
        char *surname;
        int unsigned age;
    } person;

    person p = {"John", "Doe", 30};

    printf("Name: %s; Age: %d\n", p.name, p.age);

    p.age = 25;

    p.name = "Jane";

    printf("Name: %s; Age: %d\n", p.name, p.age);

    return 1;
}

这个方法运行正常,但我想知道是否有更好的方法来实现这一点。
6个回答

65

第一个示例无法运行,因为您无法将值分配给数组 - 在这方面,数组的工作原理有点像常量指针。但您可以将一个新值复制到数组中:

strcpy(p.name, "Jane");

如果你事先知道字符串的最大大小,那么使用字符数组是可以的。例如在第一个例子中,你可以100%确定名字将适合19个字符(不是20个,因为一个字符总是需要用于存储终止零值)。

相反,如果您不知道字符串可能的最大大小,并且/或者您想要优化内存使用,指针更好。比如,避免为名为"John"的字符串保留512个字符。但是,使用指针需要动态分配它们所指向的缓冲区,并在不再需要时释放它,以避免内存泄漏。

更新: 动态分配缓冲区的示例(使用您第二个例子中的结构定义):

char* firstName = "Johnnie";
char* surname = "B. Goode";
person p;

p.name = malloc(strlen(firstName) + 1);
p.surname = malloc(strlen(surname) + 1);

p.age = 25;
strcpy(p.name, firstName);
strcpy(p.surname, surname);

printf("Name: %s; Age: %d\n",p.name,p.age);

free(p.surname);
free(p.name);

11

将字符串视为抽象对象,而将字符数组视为容器。字符串可以是任何大小,但容器至少要比字符串长度多1(以容纳null终止符)。

C语言对字符串的语法支持非常有限。没有字符串运算符(只有字符数组和字符指针运算符)。你不能直接给字符串赋值。

但你可以调用函数来帮助实现你想要的功能。

strncpy()函数可以在这里使用。为了最大限度地提高安全性,建议遵循以下模式:

strncpy(p.name, "Jane", 19);
p.name[19] = '\0'; //add null terminator just in case

同时也要查看 strncat()memcpy() 函数。


1
目前来看,最佳答案是Péter的一条(展示如何使用指针),所以我会再等一段时间,看看是否还有更多人可以在这个主题上添加更多的技巧/建议。 - Gianluca Bargelli
几年前,如果我们像这样做char *s; s = "foobar";,与数组不同,它实际上是有效的。我的意思是我不能这样做char s[9]; s = "foobar"。在这两种情况下,我们都试图将字符串值分配给s,不是吗? - mayankkaizen

7
这两个结构体是不同的。当你初始化第一个结构体时,会分配大约40字节的内存。而初始化第二个结构体时,会分配大约10字节的内存。(实际分配量与架构有关)
你可以使用字符串字面值 (字符串常量) 来初始化字符数组。这就是为什么在第一个例子中 person p = {"John", "Doe",30};
可以工作。
在C语言中,你不能以传统意义上的方式赋值字符串。
你所拥有的字符串字面值 ("John") 在代码执行时会被加载到内存中。当你用其中一个字面值来初始化一个数组时,那么该字符串就会被复制到一个新的内存位置。在你的第二个例子中,你只是复制了指向(位置)字符串字面值的指针。像这样做:
char* string = "Hello";
*string = 'C'

可能会导致编译或运行时错误(我不确定)。这是一个坏主意,因为您正在修改字面字符串“Hello”,例如在微控制器上,它可能位于只读内存中。

你是正确的,你写的那个赋值语句导致了段错误,因为你试图改变一个指针的值。但是参考我的例子,char *string = "Hello"; string = "C";(注意最后一条语句没有指针赋值),这样做可以按预期工作。 - Gianluca Bargelli
2
我想提到的是,“改变指针的值”不一定会导致段错误。我的原始帖子中的代码片段引起段错误的原因是,您试图修改位于受限地址空间中的内存。 - Gus

0
在这两种情况下,你都在写作:
p.age = 25;
p.name = "Jane";
  • 在第一种情况下,p.name 是一个数组,在 C 语言中无法进行数组赋值。
  • 在第二种情况下,p.name 是一个 char*,可以将其赋值给字符串字面量,因为字符串字面量是 char 数组(数组可以转换为指针)。

您可以使用诸如 strcpymemcpy 等函数,如其他回答所示,但也可以通过整个 struct 的赋值来绕过此问题。

// compound literal, C99 feature
p = (person) {.age = 25, .name = "Jane", .surname = p.surname};

在实践中,将字符串作为一个char*size_t捆绑在一个struct中是很有用的,因此通常也可以对单个字符串进行这样的操作。

0
第一个结构体是字符数组[],第二个结构体是指向字符字符串的指针*(对于64位机器而言,大小为8字节)。根据Stephen Kochan的书《C语言编程》所述,C语言允许你分配常量字符串的唯一时刻是在定义和初始化char数组时。
char name[20] = { "John Doe" };

甚至不用

char name[20];
name = { "John Doe" };

在 char *name; 的情况下,name 是一个字符指针,而不是一个数组。当你执行了该操作时,
p.name = "Jane";

它指向另一个字符串对象。

person p = { .surname = "Doe", .name = "Johnny", .age = 30 };
printf("Ptr. value:\tp.name: 0x%p;\tp.surname: 0x%p\n", p.name, p.surname);
p.name = "Spy, watch out!";
printf("Ptr. value:\tp.name: 0x%p;\tp.surname: 0x%p\n", p.name, p.surname);

输出:

Ptr. value:     p.name: 0x00007FF726F7B16C;     p.surname: 0x00007FF726F7B174
Ptr. value:     p.name: 0x00007FF726F7ACE8;     p.surname: 0x00007FF726F7B174

然而,在字符数组[]的情况下,当你执行完之后
strcpy(p.name, "Jane");

要更改其内容,缓冲区 p.name[] 的地址永远不会改变。

C语言和Python之间的一个有趣的相似之处是Python的字符串是不可变的,类似于C的字符串指针,其中字符串文字是只读的。 Python的列表是可变的,类似于C的字符数组。

>>> name = "John"
>>> print(hex(id(name)))
0x261654235f0
>>> name = "Jane"
>>> print(hex(id(name)))
0x261654237b0
>>> type(name)
<class 'str'>
>>> name[1] = 'o'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> name = list(name)
>>> type(name)
<class 'list'>
>>> name
['J', 'a', 'n', 'e']
>>> name[1] = 'o'
>>> name
['J', 'o', 'n', 'e']
>>> name = ''.join(name)
>>> name
'Jone'
>>> type(name)
<class 'str'>
>>>

0
这是一个实现安全字符串赋值的示例。如果一个字符串比目标数组长,断言将失败并且程序退出。
#include <assert.h>
#include <stdio.h>
#include <string.h>

#define LEN(arr) (sizeof (arr) / sizeof (arr)[0])
#define APAR(arr) (arr), LEN(arr)

void Assign(char target[], int targetLen, const char source[], int sourceLen)
{   
    size_t srcStrLen;
    
    srcStrLen = strnlen(source, sourceLen);
    assert(targetLen > srcStrLen);
    memcpy(target, source, srcStrLen);
    target[srcStrLen] = '\0';
}

int main(void)
{
    typedef struct {
        char name[20];
        char surname[20];
        int unsigned age;
    } person;
    
    person p;
    
    Assign(APAR(p.name), APAR("Jane"));
    Assign(APAR(p.surname), APAR("Anderson"));
    p.age = 25;
    printf("Name: %s %s; Age: %d\n", p.name, p.surname, p.age);
    return 0;
}

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