Как правильно назначить новое строковое значение?
Я пытаюсь понять, как решить эту тривиальную проблему в 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 works?
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: В функции 'main': main.c:18: ошибка: несовместимые типы при назначении типу 'char[20]' из типа '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 ответов
Первый пример не работает, потому что вы не можете присвоить значения массивам - массивы работают (вроде) как константные указатели в этом отношении. Однако вы можете скопировать новое значение в массив:
strcpy(p.name, "Jane");
Массивы символов можно использовать, если вы заранее знаете максимальный размер строки, например, в первом примере вы на 100% уверены, что имя будет соответствовать 19 символам (а не 20, поскольку для хранения конечного нуля всегда требуется один символ) значение).
И наоборот, указатели лучше, если вы не знаете возможный максимальный размер вашей строки и / или хотите оптимизировать использование памяти, например, избегайте резервирования 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);
Думайте о строках как об абстрактных объектах, а о массивах как о контейнерах. Строка может быть любого размера, но контейнер должен быть как минимум на 1 больше длины строки (чтобы содержать нулевой терминатор).
C имеет очень мало синтаксической поддержки строк. Строковых операторов нет (только операторы char-array и char-pointer). Вы не можете назначать строки.
Но вы можете вызывать функции, чтобы помочь достичь того, что вы хотите.
strncpy()
функция может быть использована здесь. Для максимальной безопасности я предлагаю следующий шаблон:
strncpy(p.name, "Jane", 19);
p.name[19] = '\0'; //add null terminator just in case
Также посмотрите на strncat()
а также memcpy()
функции.
Две структуры разные. Когда вы инициализируете первую структуру, выделяется около 40 байт памяти. Когда вы инициализируете вторую структуру, выделяется около 10 байт памяти. (Фактическая сумма зависит от архитектуры)
Вы можете использовать строковые литералы (строковые константы) для инициализации символьных массивов. Вот почему
person p = {"John", "Doe",30};
работает в первом примере.
Вы не можете назначить (в обычном смысле) строку в C.
У вас есть строковые литералы ("Джон"), которые загружаются в память при выполнении вашего кода. Когда вы инициализируете массив одним из этих литералов, строка копируется в новую ячейку памяти. Во втором примере вы просто копируете указатель на (местоположение) строкового литерала. Делать что-то вроде:
char* string = "Hello";
*string = 'C'
может вызвать ошибки компиляции или времени выполнения (я не уверен.) Это плохая идея, потому что вы модифицируете буквенную строку "Hello", которая, например, на микроконтроллере, может быть расположена в постоянной памяти.
Первая структура представляет собой массив символов [], а вторая структура представляет собой указатель * на строку символов (размер 8 байт для 64-разрядной машины). Согласно книге Стивена Кочана «Программирование на C», единственный раз, когда C позволяет вам назначать постоянную строку, — это определение и инициализация массива символов, как в
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 String является неизменяемым и похож на указатель строки 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'>
>>>
Вот пример того, как реализовать безопасное присвоение строк. Если строка длиннее целевого массива, утверждение не выполняется и программа завершается.
#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;
}
В обоих случаях вы пишете:
p.age = 25;
p.name = "Jane";
- в первом случае это массив, и в C невозможно присваивать массивы
- во втором случае,
p.name
является a, и их можно присвоить строковым литералам, поскольку строковые литералы представляют собой массивыchar
(массивы конвертируются в указатели)
Вы можете использовать такие функции, какstrcpy
,memcpy
и другие. как показано в других ответах, но вы также можете обойти эту проблему, назначив весь файл .
// compound literal, C99 feature
p = (person) {.age = 25, .name = "Jane", .surname = p.surname};
На практике полезно объединять строки в видеchar*
иsize_t
в однойstruct
, поэтому это часто можно сделать и для отдельных строк.