10.6 – Строки в стиле C
В уроке «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.