Синтаксис си/с++

Строки в C: char*, массивы символов, отличие от std::string

1 просмотров

Введение

Завершаем первый блок цикла темой строк — в чистом C (и в C++ коде, взаимодействующем с C-API или унаследованным кодом) строки представлены не отдельным типом данных, а особым соглашением о работе с массивами символов, принципиально отличающимся от удобного, безопасного std::string современного C++.

Концепция

C-строка — это массив символов (char[]) или указатель на такой массив (char*), завершающийся специальным нулевым байтом ('', «null terminator»), который сигнализирует о конце строки — длина C-строки не хранится явно где-либо, а определяется именно поиском этого завершающего нулевого байта (что и обуславливает линейную, O(n) сложность функции strlen(), алгоритмического цикла). Стандартная библиотека C (<cstring>) предоставляет набор функций для работы с такими строками (strlen, strcpy, strcat, strcmp), требующих внимательного, ручного управления размером буфера, поскольку эти функции в большинстве своём не выполняют автоматическую проверку границ. std::string современного C++ — принципиально иной, гораздо более безопасный и удобный подход, инкапсулирующий буфер символов, его длину, и автоматическое управление памятью (включая динамическое расширение при необходимости) внутри одного объекта, устраняя большинство подводных камней «сырых» C-строк.

Пример кода

// C-строка как массив символов — явный, завершающий нулевой байт
#include <stdio.h>
#include <string.h>

int main()
{
    char greeting[20] = "Привет"; // буфер размером 20 байт, реально занято меньше — остальное не определено
    printf("Длина строки: %zun", strlen(greeting)); // strlen ИЩЕТ нулевой байт, не хранит длину явно

    strcat(greeting, ", мир!"); // дописывание в конец — ОПАСНО при недостаточном размере буфера!
    printf("%sn", greeting);

    char buffer[10];
    strcpy(buffer, "Слишком длинная строка для этого буфера"); // ПЕРЕПОЛНЕНИЕ БУФЕРА — undefined behavior!

    return 0;
}
// std::string — современная, безопасная альтернатива в C++
#include <string>
#include <iostream>

int main()
{
    std::string greeting = "Привет"; // автоматическое управление памятью, без явного буфера фиксированного размера
    std::cout << "Длина: " << greeting.length() << std::endl; // O(1) — длина хранится явно, не вычисляется поиском

    greeting += ", мир!"; // безопасное расширение — std::string САМ управляет необходимой памятью
    std::cout << greeting << std::endl;

    std::string anotherString = "Любая длина, не ограниченная заранее заданным размером буфера";
    // НЕ требует ручного расчёта размера буфера заранее — std::string динамически адаптируется

    return 0;
}
// Преобразование между std::string и C-строкой при необходимости взаимодействия с C-API
#include <string>
#include <cstring>

void legacyCFunction(const char *cString) // функция, ожидающая именно C-строку (например, из старого API)
{
    std::cout << "Получена C-строка длиной: " << strlen(cString) << std::endl;
}

int main()
{
    std::string modernString = "Современная строка";
    legacyCFunction(modernString.c_str()); // c_str() — получение C-совместимого указателя для взаимодействия
    return 0;
}

Пояснения к коду

Пример на чистом C показывает классические, потенциально опасные операции — strcat, дописывающий содержимое в конец существующего буфера, не проверяет, достаточно ли в этом буфере свободного места, и strcpy в буфер buffer[10] строки значительно большей длины демонстрирует прямое переполнение буфера — одну из самых распространённых и опасных категорий ошибок в истории программирования на C, неоднократно приводившую к серьёзным уязвимостям безопасности в реальном программном обеспечении. Сравнение с std::string показывает принципиальное архитектурное отличие — операция greeting += ", мир!" автоматически обрабатывает необходимое расширение внутреннего буфера, если текущей выделенной памяти недостаточно для нового, более длинного содержимого, полностью избегая риска переполнения буфера, присущего ручному управлению C-строками. Метод .c_str() показывает практичный мост между двумя мирами — современный код, использующий std::string, может при необходимости взаимодействовать с устаревшими C-API или библиотеками, ожидающими именно C-совместимый указатель на завершённую нулевым байтом строку, получая такой указатель через этот метод без необходимости полностью переписывать взаимодействие с таким API.

Подводные камни

  • Переполнение буфера при использовании strcpy/strcat/аналогичных функций без проверки реального размера копируемых данных относительно размера буфера-получателя — это, как показано в примере, не просто теоретическая, а исторически одна из самых распространённых, эксплуатируемых категорий уязвимостей безопасности реального программного обеспечения, и современная практика рекомендует либо использовать более безопасные альтернативы с явным указанием максимального размера (strncpy, strncat, хотя и они имеют собственные, менее очевидные подводные камни), либо, что значительно предпочтительнее для нового кода, полностью переходить на std::string, устраняющий эту категорию ошибок архитектурно.
  • Забытый нулевой завершающий байт при ручном создании или модификации C-строки (например, при заполнении буфера символов вручную, посимвольно, без явной установки '' в конце) — без этого завершающего байта функции, работающие с C-строками (strlen, printf с %s), не имеют способа узнать, где заканчивается реальное содержимое строки, и будут продолжать читать память далее, за пределами реально предназначенных данных, пока случайно не встретят байт со значением 0 где-то в памяти далее (что является неопределённым поведением и потенциальной уязвимостью чтения за границами буфера).
  • Сравнение C-строк через оператор == вместо strcmp() — поскольку C-строка с точки зрения типа — это указатель (char*), оператор == сравнивает сами адреса указателей, а не содержимое строк, на которые они указывают, и две C-строки с одинаковым текстовым содержимым, но хранящиеся в разных областях памяти (что типично для большинства практических случаев), дадут результат false при сравнении через ==, хотя интуитивно (особенно для разработчиков, привыкших к std::string или строкам других языков, где == сравнивает содержимое) ожидался бы true — для сравнения реального текстового содержимого C-строк нужно использовать strcmp(), возвращающий 0 при равенстве содержимого.
  • Предположение о неизменном, фиксированном размере выделенного буфера для строки, длина которой на практике может варьироваться (пользовательский ввод, данные из внешнего источника) без выделения буфера достаточного, заведомо избыточного размера или динамического управления памятью — в отличие от std::string, автоматически адаптирующегося к любой длине содержимого, статический буфер фиксированного размера (char buffer[N]) требует от разработчика заранее, вручную предусмотреть достаточный размер для ЛЮБОГО реально возможного содержимого, что для данных переменной, заранее не ограниченной длины (как пользовательский ввод) либо невозможно сделать надёжно без риска переполнения, либо требует значительно более сложного, явного динамического управления памятью, которое std::string предоставляет автоматически и значительно безопаснее.