← Назад к вопросам

Что такое C-строка?

1.6 Junior🔥 111 комментариев
#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое C-строка?

C-строка (также называется null-terminated string) — это массив символов, завершающийся нулевым байтом ('\0'). Это один из фундаментальных способов работы со строками в C и C++, и понимание её механики критично для написания безопасного кода.

Определение и структура

C-строка — это:

  • Массив элементов типа char или unsigned char
  • Заканчивается символом с кодом 0 (null terminator '\0')
  • Строка "Hello" занимает 6 байт в памяти (5 символов + null terminator)
// В памяти это выглядит так:
char str[] = "Hello";
// str[0] = 'H'  (0x48)
// str[1] = 'e'  (0x65)
// str[2] = 'l'  (0x6C)
// str[3] = 'l'  (0x6C)
// str[4] = 'o'  (0x6F)
// str[5] = '\0' (0x00)  <- NULL TERMINATOR
// Всего 6 байт

const char* ptr = "Hello";  // Указатель на строковый литерал (read-only)
int length = strlen(ptr);    // 5 (null terminator не считается)

Создание и инициализация

#include <cstring>
#include <iostream>

int main() {
    // Способ 1: Строковый литерал (read-only)
    const char* literal = "Hello";
    
    // Способ 2: Массив на стеке (изменяемый)
    char array[] = "Hello";  // Копируется в array
    array[0] = 'h';           // OK: можем менять
    
    // Способ 3: Указатель на стеке (небезопасно!)
    char* ptr = new char[6];
    strcpy(ptr, "Hello");     // ОПАСНО: нет проверки размера!
    delete[] ptr;
    
    // Способ 4: Безопасное копирование
    char buffer[10];
    strncpy(buffer, "Hello", sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // Гарантируем null terminator
    
    return 0;
}

Работа с C-строками

Определение длины:

#include <cstring>

const char* str = "Hello";
size_t len = strlen(str);  // 5

// strlen() подсчитывает символы до null terminator
// Внутренняя реализация:
size_t my_strlen(const char* str) {
    size_t len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

Копирование (ОПАСНАЯ функция!):

char source[] = "Hello";
char dest[10];

// ❌ ОПАСНО: нет проверки размера
strcpy(dest, source);  // Buffer overflow if source is too long!

// ✅ БЕЗОПАСНО: указываем максимум
strncpy(dest, source, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // Гарантируем null terminator

Конкатенация (объединение):

char buffer[20];
strcpy(buffer, "Hello");
strcat(buffer, " ");      // ❌ Опасно!
strcat(buffer, "World");  // ❌ Опасно!

// ✅ Безопасно:
char buffer[20];
strcpy(buffer, "Hello");
strncat(buffer, " ", sizeof(buffer) - strlen(buffer) - 1);
strncat(buffer, "World", sizeof(buffer) - strlen(buffer) - 1);

Сравнение:

const char* str1 = "Hello";
const char* str2 = "Hello";

// ❌ НЕПРАВИЛЬНО (сравнивает указатели, не содержимое)
if (str1 == str2) { }  // Может быть false даже если содержимое одинаковое

// ✅ ПРАВИЛЬНО (сравнивает содержимое)
if (strcmp(str1, str2) == 0) { }  // strcmp возвращает 0 если равны

// ✅ Безопасное сравнение (с лимитом)
if (strncmp(str1, str2, 10) == 0) { }

Проблемы и опасности

1. Buffer Overflow — классическая уязвимость:

char buffer[10];
const char* input = "This is a very long string that will overflow the buffer";

strcpy(buffer, input);  // BUFFER OVERFLOW!
// Переписываем память за пределами buffer
// Может привести к краху или security exploit

2. Отсутствие null terminator:

char str[5] = {'H', 'e', 'l', 'l', 'o'};  // Нет null terminator!
stdout::cout << strlen(str) << std::endl;  // Undefined behavior!
// strlen() будет читать память за пределами массива

3. Use-after-free:

char* get_string() {
    char local[] = "Hello";
    return local;  // ОПАСНО! Возвращаем указатель на локальную переменную
}

int main() {
    const char* str = get_string();
    std::cout << str << std::endl;  // Undefined behavior!
    // local уже удалена, str указывает на невалидную память
}

4. Неинициализированная память:

char* ptr = new char[10];
std::cout << strlen(ptr) << std::endl;  // Undefined behavior!
// ptr указывает на неинициализированную память
// strlen() будет читать до первого нулевого байта (неизвестное количество)

Правильные практики

1. Используйте std::string вместо C-строк:

#include <string>

std::string str = "Hello";  // Автоматическое управление памятью
str += " World";             // Безопасная конкатенация
size_t len = str.length();   // Быстрое получение длины

if (str == "Hello World") {  // Безопасное сравнение
    std::cout << "Match!" << std::endl;
}

2. Если всё же используете C-строки, будьте осторожны:

// ❌ Опасные функции
strcpy()      // Без проверки размера
strcat()      // Без проверки размера
gets()        // Всегда unsafe
scanf("%s")   // Без проверки размера

// ✅ Безопасные альтернативы
strncpy()          // С максимальным размером
strncat()          // С максимальным размером
fgets()            // С проверкой размера
scanf("%10s")      // С ограничением размера

3. Всегда проверяйте границы:

void safe_copy(char* dest, size_t dest_size, const char* src) {
    if (src == nullptr || dest == nullptr || dest_size == 0) {
        return;  // Invalid input
    }
    
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';  // Гарантируем null terminator
}

C-строки vs std::string

ХарактеристикаC-строкаstd::string
Управление памятьюManualAutomatic (RAII)
РазмерUnknown (вычисляется)Known (length())
Конкатенацияstrcat (опасно)+ (безопасно)
Сравнениеstrcmp==
ПроизводительностьЧуть быстрееNegligible overhead
БезопасностьНизкая (требует дисциплины)Высокая
ГибкостьНизкаяВысокая (substr, find, etc.)

Внутреннее устройство

// Как компилятор обрабатывает строковый литерал
const char* str = "Hello\0World";  // В памяти:
// Address  Content
// 0x1000:  'H' (0x48)
// 0x1001:  'e' (0x65)
// 0x1002:  'l' (0x6C)
// 0x1003:  'l' (0x6C)
// 0x1004:  'o' (0x6F)
// 0x1005:  '\0' (0x00)  <- STOP здесь для strlen
// 0x1006:  'W' (0x57)
// 0x1007:  'o' (0x6F)
// 0x1008:  'r' (0x72)
// 0x1009:  'l' (0x6C)
// 0x100A:  'd' (0x64)
// 0x100B:  '\0' (0x00)

stdout::cout << strlen(str) << std::endl;  // 5, не 11!
// strlen() останавливается на первом null terminator

Оптимизация

// ❌ Неоптимально: вычисляем длину много раз
for (int i = 0; i < strlen(str); i++) {
    // O(n^2) сложность!
}

// ✅ Оптимально: вычисляем один раз
size_t len = strlen(str);
for (int i = 0; i < len; i++) {
    // O(n) сложность
}

// ✅ Лучше всего: используем std::string
std::string s = str;
for (int i = 0; i < s.length(); i++) {
    // length() = O(1), потому что хранится явно
}

Заключение

C-строка — это мощный инструмент, но требует дисциплины и осторожности:

  1. Всегда проверяйте размер при копировании
  2. Гарантируйте null terminator (null-terminated string)
  3. Используйте стандартные функции (strlen, strncpy, strncmp)
  4. Предпочитайте std::string когда возможно
  5. Проверяйте возвращаемые значения функций
  6. Используйте инструменты (valgrind, asan) для обнаружения ошибок

В современном C++ рекомендуется минимизировать использование C-строк и переходить на std::string, но понимание их механики остаётся критичным для работы с legacy кодом и системным программированием.

Что такое C-строка? | PrepBro