9.6 – Строки в стиле C

Добавлено 5 июня 2021 в 19:56
Глава 9 – Массивы, строки, указатели и ссылки  (содержание)

В уроке «4.12 – Знакомство с std::string» мы определили строку как набор последовательных символов, таких как "Hello, world!". Строки – это основной способ работы с текстом в C++, а std::string упрощает работу с ними.

Современный C++ поддерживает два разных типа строк: std::string (как часть стандартной библиотеки) и строки в стиле C (изначально унаследованные от языка C). Оказывается, std::string реализована с использованием строк в стиле C. В этом уроке мы подробнее рассмотрим строки в стиле C.

Строки в стиле C

Строка в стиле C – это просто массив символов, в котором используется нулевой терминатор. Нулевой терминатор – это специальный символ ('\0', ASCII код 0), используемый для обозначения конца строки. В более общем смысле, строка в стиле C называется строкой с завершающим нулем.

Чтобы определить строку в стиле C, просто объявите массив char и инициализируйте его строковым литералом:

char myString[]{ "string" };

Хотя "string" состоит только из 6 букв, C++ автоматически добавляет нулевой терминатор в конец строки за нас (нам не нужно включать его самим). Следовательно, myString на самом деле представляет собой массив длиной 7!

Доказательство этого можно увидеть в следующей программе, которая выводит длину строки, а затем значения ASCII всех символов:

#include <iostream>
#include <iterator> // для std::size
 
int main()
{
    char myString[]{ "string" };
    const int length{ static_cast<int>(std::size(myString)) };
//  const int length{ sizeof(myString) / sizeof(myString[0]) }; // если C++17 не поддерживается
    std::cout << myString << " has " << length << " characters.\n";
 
    for (int index{ 0 }; index < length; ++index)
        std::cout << static_cast<int>(myString[index]) << ' ';
 
    std::cout << '\n';
 
    return 0;
}

Эта программа дает следующий результат:

string has 7 characters.
115 116 114 105 110 103 0

Этот 0 – это код ASCII нулевого терминатора, добавленного в конец строки.

При объявлении строк таким образом рекомендуется использовать [] и позволить компилятору самому вычислить длину массива. Таким образом, если позже вы измените строку, вам не придется изменять длину массива вручную.

Следует отметить один важный момент: строки в стиле C подчиняются всем тем же правилам, что и массивы. Это означает, что вы можете инициализировать строку при создании, но после этого вы не можете присваивать ей значения с помощью оператора присваивания!

char myString[]{ "string" }; // хорошо
myString = "rope";           // не нормально!

Поскольку строки в стиле C представляют собой массивы, вы можете использовать оператор [] для изменения отдельных символов в строке:

#include <iostream>
 
int main()
{
    char myString[]{ "string" };
    myString[1] = 'p';
    std::cout << myString << '\n';
 
    return 0;
}

Эта программа напечатает:

spring

При печати строки в стиле C std::cout печатает символы до тех пор, пока не встретит нулевой терминатор. Если вы случайно перезапишете нулевой терминатор строки (например, присвоив что-то элементу myString[6]), вы получите не только все символы в строке, но std::cout просто продолжит печатать всё из соседних слотов памяти до тех пор, пока не наткнется на 0!

Обратите внимание, что это нормально, если массив по размеру больше, чем содержащаяся в нем строка:

#include <iostream>
 
int main()
{
    char name[20]{ "Alex" }; // используется только 5 символов (4 буквы + нулевой терминатор)
    std::cout << "My name is: " << name << '\n';
 
    return 0;
}

В этом случае будет напечатана строка "Alex", и std::cout остановится на нулевом терминаторе. Остальные символы в массиве игнорируются.

Строки в стиле C и std::cin

Во многих случаях мы не знаем заранее, какой длины будет наша строка. Например, рассмотрим задачу написания программы, в которой нам нужно попросить пользователя ввести свое имя. Насколько длинное у него имя? Мы не узнаем, пока он не введет его!

В этом случае мы можем объявить массив больше, чем нам нужно:

#include <iostream>
 
int main()
{
    char name[255]; // объявляем массив достаточно большим, чтобы вместить 255 символов
    std::cout << "Enter your name: ";
    std::cin >> name;
    std::cout << "You entered: " << name << '\n';
 
    return 0;
}

В приведенной выше программе мы выделили для имени массив из 255 символов, предполагая, что пользователь не будет вводить столько много символов. Хотя это обычно встречается в программировании на C/C++, это плохая практика, поскольку ничто не мешает пользователю ввести более 255 символов (непреднамеренно или с умыслом).

Чтение строк в стиле C с помощью std::cin рекомендуется выполнять следующим способом:

#include <iostream>
#include <iterator> // для std::size
 
int main()
{
    char name[255]; // объявляем массив достаточно большим, чтобы вместить 255 символов
    std::cout << "Enter your name: ";
    std::cin.getline(name, std::size(name));
    std::cout << "You entered: " << name << '\n';
 
    return 0;
}

Вызов cin.getline() считывает имя длиной до 254 символов (оставляя место для нулевого терминатора!). Любые лишние символы будут отброшены. Таким образом, мы гарантируем, что не переполним массив!

Управление строками в стиле C

C++ предоставляет множество функций для управления строками в стиле C как часть заголовка <cstring>. Вот несколько наиболее полезных:

strcpy() позволяет копировать строку в другую строку. Чаще всего это используется для присвоения значения строке:

#include <cstring>
 
int main()
{
    char source[]{ "Copy this!" };
    char dest[50];
    std::strcpy(dest, source);
    std::cout << dest << '\n'; // печатает "Copy this!"
 
    return 0;
}

Однако strcpy() может легко вызвать переполнение массива, если вы не будете осторожны! В следующей программе dest недостаточно велик, чтобы вместить всю строку, и поэтому возникает переполнение массива.

#include <cstring>
 
int main()
{
    char source[]{ "Copy this!" };
    char dest[5]; // обратите внимание, что длина dest всего 5 символов!
    std::strcpy(dest, source); // переполнение!
    std::cout << dest << '\n';
 
    return 0;
}

Многие программисты рекомендуют вместо этого использовать strncpy(), которая позволяет указать размер буфера и гарантирует, что переполнение не произойдет. К сожалению, strncpy() не гарантирует, что строки завершаются нулем, что по-прежнему оставляет много места для переполнения массива.

В C++11 предпочтительнее использование strcpy_s(), которая добавляет новый параметр для определения размера места назначения. Однако эту функцию поддерживают не все компиляторы, и для ее использования необходимо определить __STDC_WANT_LIB_EXT1__ целочисленным значением 1.

#define __STDC_WANT_LIB_EXT1__ 1

#include <cstring> // для strcpy_s

int main()
{
    char source[]{ "Copy this!" };
    char dest[5]; // обратите внимание, что длина dest всего 5 символов!
    strcpy_s(dest, 5, source); // в режиме отладки произойдет ошибка времени выполнения
    std::cout << dest << '\n';
 
    return 0;
}

Поскольку strcpy_s() поддерживают не все компиляторы, популярной альтернативой является strlcpy(), даже несмотря на то, что она нестандартна и поэтому не включена во многие компиляторы. У нее также есть свой набор проблем. Короче говоря, если вам нужно скопировать строку в стиле C, универсального решения нет.

Еще одна полезная функция – функция strlen(), которая возвращает длину строки в стиле C (без нулевого терминатора).

#include <iostream>
#include <cstring>
#include <iterator> // для std::size
 
int main()
{
    char name[20]{ "Alex" }; // используется только 5 символов (4 буквы + нулевой терминатор)
    std::cout << "My name is: " << name << '\n';
    std::cout << name << " has " << std::strlen(name) << " letters.\n";
    // если C++17 не поддерживается, используйте sizeof(name) / sizeof(name [0])
    std::cout << name << " has " << std::size(name) << " characters in the array.\n";
 
    return 0;
}

В приведенном выше примере напечатается:

My name is: Alex
Alex has 4 letters.
Alex has 20 characters in the array.

Обратите внимание на разницу между strlen() и std::size(). strlen() печатает количество символов перед нулевым терминатором, тогда как std::size (или трюк с sizeof()) возвращает размер всего массива, независимо от того, что в нем находится.

Другие полезные функции:

  • strcat() – добавляет одну строку к другой (опасно);
  • strncat() – добавляет одну строку к другой (с проверкой длины буфера);
  • strcmp() – сравнивает две строки (возвращает 0, если они равны);
  • strncmp() – сравнивает две строки до определенного количества символов (возвращает 0, если равны).

Вот пример программы, использующей некоторые концепции из этого урока:

#include <cstring>
#include <iostream>
#include <iterator> // для std::size
 
int main()
{
    // Просим пользователя ввести строку
    char buffer[255];
    std::cout << "Enter a string: ";
    std::cin.getline(buffer, std::size(buffer));
 
    int spacesFound{ 0 };
    int bufferLength{ static_cast<int>(std::strlen(buffer)) };
    // Перебираем все символы, введенные пользователем
    for (int index{ 0 }; index < bufferLength; ++index)
    {
        // Если текущий символ - это пробел, сосчитать его
        if (buffer[index] == ' ')
            ++spacesFound;
    }
 
    std::cout << "You typed " << spacesFound << " spaces!\n";
 
    return 0;
}

Обратите внимание, что мы помещаем strlen(buffer) за пределы цикла, чтобы длина строки вычислялась только один раз, а не каждый раз, когда проверяется условие цикла.

Не используйте строки в стиле C

О строках в стиле C знать важно, потому что они используются во многих программах. Однако теперь, когда мы объяснили, как они работают, мы порекомендуем вам вообще избегать их, когда это возможно! Если у вас нет конкретной веской причины использовать строки в стиле C, используйте вместо них std::string (определенную в заголовке <string>). std::string проще, безопаснее и гибче. В редком случае, когда вам действительно нужно работать с фиксированными размерами буфера и строками в стиле C (например, для устройств с ограничением памяти), мы рекомендуем использовать хорошо протестированную стороннюю строковую библиотеку, разработанную для этой цели, или std::string_view, который рассматривается в следующем уроке.

Правило


Используйте std::string или std::string_view (следующий урок) вместо строк в стиле C.

Теги

C++ / CppLearnCppstd::stringДля начинающихМассивОбучениеПрограммированиеСтрокаСтрока в стиле C

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.