В 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); // 123
atof() — строка в float
char str[] = "3.14";
double num = atof(str);
printf("%.2f\n", num); // 3.14
strtol() и strtof() — с проверкой ошибок
#include <stdlib.h>
char str[] = "123abc";
char *endptr;
long num = strtol(str, &endptr, 10);
printf("Число: %ld\n", num); // 123
printf("Остаток: %s\n", endptr); // abc
strtol() правильнее, так как возвращает информацию об ошибке.
Практический пример: парсер 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() — безопасное форматирование
Овладев этими функциями и избегая ошибок, вы сможете писать надёжный код без утечек памяти и переполнений буферов.