Работа со строками в C: Полное руководство
-
В C строки — это просто массивы символов (char), заканчивающиеся нулевым байтом (
\0
char str[] = "Hello"; // В памяти: H e l l o \0 // Индексы: 0 1 2 3 4 5Нулевой байт (
\0) — это терминатор, он сигнализирует окончание строки. Без него функции вродеprintf()не будут знать, где остановиться, и напечатают мусор из памяти.Это ключевое отличие от других языков, где строки — это отдельный тип данных. В C это просто договоренность: строка = массив char + нулевой терминатор.
Объявление строк
Способ 1: Массив с инициализацией
char str[] = "Hello"; // Автоматический размер (6 байт: H e l l o \0)Компилятор сам считает, сколько нужно места, и выделит 6 байт (5 символов + терминатор).
Способ 2: Массив с явным размером
char str[50] = "Hello"; // Массив размером 50 байтПервые 6 байт:
H e l l o \0, остальные 44 — неинициализированы (мусор).Способ 3: Указатель на строковый литерал
char *str = "Hello"; // Указатель на неизменяемую строкуЭта строка хранится в read-only памяти (в сегменте кода). Вы можете читать её, но НЕ можете менять:
char *str = "Hello"; str[0] = 'J'; // КРАХ! Segmentation fault — попытка написать в read-only памятьСпособ 4: Массив для изменяемой строки
char str[] = "Hello"; // Копия в стеке, можно менять str[0] = 'J'; // OK! Теперь str = "Jello"Таблица различий:
Объявление Размер Изменяемо? Хранилище char str[] = "..."Автоматический Да Стек char str[50] = "..."Фиксированный Да Стек char *str = "..."N/A Нет Readonly память char *str = malloc(...)Динамический Да Heap
Основные функции для строк
Все они находятся в
<string.h>:#include <string.h>strlen() — длина строки
Возвращает количество символов БЕЗ терминатора:
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello"; size_t len = strlen(str); printf("Длина: %zu\n", len); // 5 return 0; }Важно:
strlen()идёт по памяти, пока не найдёт\0. Если его нет, программа зависнет или упадёт.char str[5]; // Ошибка: нет терминатора! strlen(str); // Неопределённое поведениеstrcpy() — копирование строки
char src[] = "Hello"; char dest[50]; strcpy(dest, src); printf("%s\n", dest); // HelloОПАСНОСТЬ:
strcpy()не проверяет размер буфера — это классическая уязвимость:char dest[5]; // 5 байт strcpy(dest, "Hello, World!"); // 13 символов! // БУФЕР ПЕРЕПОЛНЕН! Пишем за границы массиваИсправление: используйте
strncpy():char dest[50]; strncpy(dest, src, 49); // Максимум 49 символов dest[49] = '\0'; // Гарантируем терминаторИли ещё лучше — используйте
snprintf():char dest[50]; snprintf(dest, sizeof(dest), "%s", src); // Безопасно и удобноstrcat() — конкатенация (склеивание)
char str1[50] = "Hello"; char str2[] = " World"; strcat(str1, str2); printf("%s\n", str1); // Hello WorldТребование:
str1должен быть выделен достаточно большой, чтобы вместить результат.ОПАСНОСТЬ: как и
strcpy(),strcat()не проверяет границы:char str1[6] = "Hello"; // Только 6 байт strcat(str1, " World"); // ПЕРЕПОЛНЕНИЕИсправление:
strncat():char str1[50] = "Hello"; char str2[] = " World"; strncat(str1, str2, 49 - strlen(str1) - 1); // Добавить не более N символовstrcmp() — сравнение строк
#include <string.h> int main() { char str1[] = "Hello"; char str2[] = "Hello"; char str3[] = "World"; printf("%d\n", strcmp(str1, str2)); // 0 (одинаковые) printf("%d\n", strcmp(str1, str3)); // < 0 (str1 < str3 в ASCII) printf("%d\n", strcmp(str3, str1)); // > 0 (str3 > str1 в ASCII) return 0; }Возвращаемые значения:
0— строки одинаковые< 0— первая строка лексикографически меньше> 0— первая строка лексикографически больше
Почему не использовать
==?char *str1 = "Hello"; char *str2 = "Hello"; if (str1 == str2) { } // НЕПРАВИЛЬНО! Сравнивает адреса, не значения if (strcmp(str1, str2) == 0) { } // ПРАВИЛЬНОstrchr() — поиск символа
char str[] = "Hello World"; char *pos = strchr(str, 'o'); if (pos != NULL) { printf("Найден 'o' на позиции %ld\n", pos - str); // 4 }strchr()возвращает указатель на первое вхождение символа илиNULL, если не найден.Вычисляем позицию как
pos - str(арифметика указателей).strstr() — поиск подстроки
char str[] = "Hello World"; char *pos = strstr(str, "World"); if (pos != NULL) { printf("Найдена подстрока на позиции %ld\n", pos - str); // 6 }Аналогично
strchr(), но ищет не один символ, а всю подстроку.strdup() — дублирование строки
char str[] = "Hello"; char *copy = strdup(str); // Динамическое выделение + копирование printf("%s\n", copy); free(copy); // Не забыть!strdup()эквивалентен:char *copy = malloc(strlen(str) + 1); strcpy(copy, str);Важно:
strdup()выделяет память, которую вы должны освободить.strtok() — разбор строки на токены
Разделяет строку по разделителям:
char str[] = "apple,banana,orange"; char *token = strtok(str, ","); while (token != NULL) { printf("%s\n", token); token = strtok(NULL, ","); // NULL = продолжить с предыдущей строки }Вывод:
apple banana orangeВнимание:
strtok()модифицирует исходную строку (вставляет\0на месте разделителей). Если вам нужна исходная строка, сделайте копию:char str[] = "apple,banana,orange"; char *copy = strdup(str); char *token = strtok(copy, ","); // ... использование ... free(copy);
Работа с динамическими строками
Чтение строки от пользователя
НЕПРАВИЛЬНО (уязвиво):
char name[10]; scanf("%s", name); // Если пользователь введёт "Alexander", ПЕРЕПОЛНЕНИЕ!ПРАВИЛЬНО:
char name[50]; fgets(name, sizeof(name), stdin); // Максимум 49 символов // Удалить символ новой строки if (name[strlen(name) - 1] == '\n') { name[strlen(name) - 1] = '\0'; }ЛУЧШЕ (динамическое):
#include <stdio.h> #include <stdlib.h> #include <string.h> char *read_string(void) { char buffer[256]; if (fgets(buffer, sizeof(buffer), stdin) == NULL) { return NULL; } // Удалить \n buffer[strcspn(buffer, "\n")] = 0; // Скопировать в динамическую память return strdup(buffer); } int main() { printf("Введите имя: "); char *name = read_string(); if (name != NULL) { printf("Привет, %s!\n", name); free(name); } return 0; }Конкатенация без переполнения
#include <stdio.h> #include <string.h> int main() { char result[100] = ""; snprintf(result, sizeof(result), "%s %s %d", "Hello", "World", 2025); printf("%s\n", result); // Hello World 2025 return 0; }snprintf()— это самый безопасный способ форматирования и конкатенации строк.
Строки и указатели
Массив строк
char *names[] = { "Alice", "Bob", "Charlie" }; for (int i = 0; i < 3; i++) { printf("%s\n", names[i]); }Это массив указателей, каждый указывает на строковый литерал.
Нельзя менять эти строки, так как они в read-only памяти:
names[0][0] = 'X'; // КРАХЕсли нужно менять: используйте массив массивов:
char names[][20] = { "Alice", "Bob", "Charlie" }; names[0][0] = 'X'; // OK, теперь names[0] = "Xlice"Обход строки по указателю
char str[] = "Hello"; char *ptr = str; while (*ptr != '\0') { printf("%c ", *ptr); ptr++; } // Вывод: H e l l oЭто эквивалентно:
for (char *ptr = str; *ptr; ptr++) { printf("%c ", *ptr); }
Преобразования типов
atoi() — строка в целое число
char str[] = "123"; int num = atoi(str); printf("%d\n", num); // 123atof() — строка в float
char str[] = "3.14"; double num = atof(str); printf("%.2f\n", num); // 3.14strtol() и strtof() — с проверкой ошибок
#include <stdlib.h> char str[] = "123abc"; char *endptr; long num = strtol(str, &endptr, 10); printf("Число: %ld\n", num); // 123 printf("Остаток: %s\n", endptr); // abcstrtol()правильнее, так как возвращает информацию об ошибке.
Практический пример: парсер CSV
#include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX_LINE 1000 #define MAX_FIELDS 10 int parse_csv_line(char *line, char *fields[], int max_fields) { int count = 0; char *copy = strdup(line); char *token = strtok(copy, ","); while (token != NULL && count < max_fields) { // Удалить пробелы while (*token == ' ') token++; fields[count++] = strdup(token); token = strtok(NULL, ","); } free(copy); return count; } int main() { char line[] = "Alice, 25, New York, Engineer"; char *fields[MAX_FIELDS]; int count = parse_csv_line(line, fields, MAX_FIELDS); for (int i = 0; i < count; i++) { printf("Field %d: [%s]\n", i, fields[i]); free(fields[i]); } return 0; }Вывод:
Field 0: [Alice] Field 1: [25] Field 2: [New York] Field 3: [Engineer]
Частые ошибки
Ошибка 1: забыть место для терминатора
char str[5] = "Hello"; // Нужно 6 байт (5 символов + \0), выделили 5 // БУФЕРНОЕ ПЕРЕПОЛНЕНИЕИсправление:
char str[6] = "Hello"; // ПравильноОшибка 2: использовать strcpy без проверки размера
char dest[10]; char src[] = "This is a very long string"; strcpy(dest, src); // ПЕРЕПОЛНЕНИЕИсправление:
strncpy(dest, src, sizeof(dest) - 1); dest[sizeof(dest) - 1] = '\0';Ошибка 3: сравнивать строки с ==
char *str1 = "Hello"; char *str2 = "Hello"; if (str1 == str2) { } // Может быть неправильным! if (strcmp(str1, str2) == 0) { } // ПравильноОшибка 4: забыть free для strdup
char *str = strdup("Hello"); // Использование free(str); // ОБЯЗАТЕЛЬНО!Ошибка 5: стоковый буфер для возврата
char *bad_function() { char str[] = "Hello"; // Локальная переменная return str; // Возвращаем адрес стека — НЕПРАВИЛЬНО } int main() { char *str = bad_function(); printf("%s\n", str); // Мусор или крах }Исправление:
char *good_function() { char *str = malloc(50); strcpy(str, "Hello"); return str; // Вызывающий должен free }
Лучшие практики
-
Всегда проверяйте размер буфера:
strncpy(dest, src, sizeof(dest) - 1); dest[sizeof(dest) - 1] = '\0'; -
Используйте
snprintf()вместоsprintf():snprintf(buffer, sizeof(buffer), "%s: %d", name, age); -
Используйте
fgets()вместоgets()илиscanf("%s"):fgets(buffer, sizeof(buffer), stdin); -
Проверяйте возвращаемые значения:
char *token = strtok(str, " "); if (token != NULL) { } -
Освобождайте динамические строки:
char *str = strdup("Hello"); free(str); str = NULL; -
Документируйте правила памяти:
// Возвращает выделенную динамически строку (вызывающий должен free) char *create_greeting(const char *name) { } -
Для сложных манипуляций используйте вспомогательные функции:
// Вместо прямого strtok, инкапсулируйте логику char **split_string(const char *str, const char *delim, int *count) { }
Заключение
Работа со строками в C требует дисциплины и внимания к деталям. Главное правило: всегда знайте размер вашего буфера и проверяйте границы.
Основные инструменты:
strlen()— длинаstrcpy()/strncpy()— копирование (используйтеstrncpy())strcat()/strncat()— конкатенация (используйтеstrncat())strcmp()— сравнениеstrchr()/strstr()— поискstrtok()— разбор на токеныsnprintf()— безопасное форматирование
Овладев этими функциями и избегая ошибок, вы сможете писать надёжный код без утечек памяти и переполнений буферов.
© 2022 - 2025 InvestSteel, Inc. Все права защищены.