Строки в C: null-терминированные массивы символов, функции библиотеки string.h и лучшие практики
Что такое строка в C?
В C нет встроенного строкового типа. Строка — это массив char, заканчивающийся нуль-терминатором '\0' (код 0). Длина строки = количество символов до терминатора.
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // явная инициализация
char str2[] = "Hello"; // неявный \0, размер 6
char *str3 = "Hello"; // указатель на строковый литерал (read-only!)
Память:
char s[] = "abc";— массив в стеке (или .data если глобальный), изменяемый, размер 4.char *s = "abc";— указатель на литерал в сегменте .rodata, нельзя менять (UB).
Стандартная библиотека <string.h>
| Функция | Описание | Возврат |
|---|---|---|
strlen(s) | Длина строки (без \0) | size_t |
strcpy(dest, src) | Копирует src в dest (с \0). Не проверяет границы! | dest |
strncpy(dest, src, n) | Копирует до n символов. Не дописывает \0, если src длиннее! | dest |
strcat(dest, src) | Дописывает src в конец dest. Не проверяет границы! | dest |
strncat(dest, src, n) | Дописывает до n символов, гарантирует \0. | dest |
strcmp(s1, s2) | Лексикографическое сравнение | <0, 0, >0 |
strncmp(s1, s2, n) | Сравнение первых n символов | <0, 0, >0 |
strchr(s, c) | Поиск первого вхождения символа c | Указатель или NULL |
strrchr(s, c) | Поиск последнего вхождения | Указатель или NULL |
strstr(haystack, needle) | Поиск подстроки | Указатель или NULL |
strtok(str, delim) | Разбиение на токены (модифицирует строку, не потокобезопасна) | Указатель на токен |
memcpy(dest, src, n) | Копирует n байт (может перекрываться? нет, используйте memmove) | dest |
memmove(dest, src, n) | Копирует n байт, корректно при перекрытии | dest |
memset(ptr, val, n) | Заполняет n байт значением val | ptr |
memcmp(s1, s2, n) | Сравнивает n байт | <0, 0, >0 |
Безопасные аналоги (C11 Annex K — опционально)
strcpy_s(dest, destsz, src);
strcat_s(dest, destsz, src);
strncpy_s(dest, destsz, src, n);
Проверяют границы, возвращают код ошибки. Не все компиляторы поддерживают (MSVC да, GCC/Clang — через __STDC_LIB_EXT1__).
Примеры использования
Конкатенация с проверкой размера
char buf[100] = "Hello";
const char *add = ", World!";
if (strlen(buf) + strlen(add) + 1 <= sizeof(buf)) {
strcat(buf, add);
} else {
// ошибка: не хватает места
}
Безопасное копирование
// strncpy не гарантирует \0!
char dest[10];
strncpy(dest, source, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // принудительный терминатор
Поиск и парсинг
char *line = "user:admin:12345";
char *colon = strchr(line, ':');
if (colon) {
*colon = '\0'; // разделяем: line="user", colon+1="admin:12345"
printf("User: %s\n", line);
// дальше парсим colon+1...
}
strtok — осторожно!
char str[] = "apple,banana,cherry"; // должен быть изменяемым массивом!
char *token = strtok(str, ",");
while (token) {
printf("Token: %s\n", token);
token = strtok(NULL, ","); // NULL = продолжить ту же строку
}
// strtok НЕ потокобезопасна (хранит состояние в static переменной).
// Альтернатива: strtok_r (POSIX) или strsep (BSD).
Динамические строки
Для строк неизвестной/меняющейся длины используйте структуру с буфером:
typedef struct {
char *data;
size_t len;
size_t capacity;
} String;
void str_init(String *s) {
s->data = malloc(16);
s->data[0] = '\0';
s->len = 0;
s->capacity = 16;
}
void str_append(String *s, const char *src) {
size_t need = s->len + strlen(src) + 1;
if (need > s->capacity) {
s->capacity = need * 2;
s->data = realloc(s->data, s->capacity);
}
strcpy(s->data + s->len, src);
s->len = strlen(s->data);
}
void str_free(String *s) {
free(s->data);
s->data = NULL;
s->len = s->capacity = 0;
}
UTF-8 и Unicode
- Обычные
char*строки в UTF-8 — это байтовые последовательности.strlenвозвращает число байт, не кодовых точек. - Для работы с Unicode используйте
<wchar.h>(wchar_t,wcslen,wcscpy...) или библиотеки: ICU, libutf8proc. - C11:
char16_t,char32_t, префиксыu"...",U"...",<uchar.h>.
Форматированный ввод-вывод строк
// Вывод
printf("Name: %s, Age: %d\n", name, age);
snprintf(buf, sizeof(buf), "Value: %.2f", val); // безопасный sprintf
// Ввод
char name[100];
scanf("%99s", name); // %99s — максимум 99 символов + \0 (защита от переполнения)
// fgets(name, sizeof(name), stdin); // читает строку с пробелами, включает \n
Частые ошибки и UB
- Недостаточный буфер для
strcpy/strcat/sprintf→ переполнение буфера (уязвимость). - Модификация строкового литерала:
char *s = "abc"; s[0] = 'x';→ UB. - Использование неинициализированного/нетерминированного массива как строки.
strncpyбез ручного добавления\0.strtokв многопоточном коде или на строковых литералах.- Сравнение строк через
==(сравнивает адреса!). Всегдаstrcmp.
Лучшие практики
- Всегда проверяйте границы буфера. Используйте
snprintf,strncat, ручное управление размером. - Для динамических строк — структура с
len/capacity+realloc. - Используйте
const char *для параметров-функций, не меняющих строку. - Избегайте
gets()(удалена в C11) — используйтеfgets(). - Для сложной работы со строками — подключайте библиотеку (например, sds — simple dynamic strings от Redis, или bstring).
Язык C
- Строки в C#: основы, операции и продвинутые техники
- Освоение строк в Java: создание, методы и лучшие практики
- Строковые представления в Java Enums: настройка toString() и лучшие практики
- Создание списков строк в VHDL: лучшие практики и пример
- Функции строк в C++: strcpy, strcat, strlen, strcmp — практические примеры
- Строки в C просто: объявление, ввод, вывод и библиотечные функции
- Понимание String.charAt() в Java: синтаксис, возвращаемый тип, исключения и пример
- Освоение String.endsWith() в Java: проверка суффиксов строки с примерами
- Освоение строковых операций в Python: replace, join, split, reverse, преобразование регистра
- Мастерство метода str.count() в Python: как подсчитывать символы и подстроки с примерами