§25 Символьный тип. С-cтрока и контейнер string. Объект класса string как динамический массив

Символьный тип данных

В C++ символьный тип данных char относится к интегральным или целым типам (Integer types), так как в переменных этого типа скрыт код символа в кодовой таблице. В C++ таких типов несколько:

  • char – базовый символьный тип, размер – 1 байт
  • unsigned char – символьный тип без знака, 1 байт
  • signed char – символьный тип со знаком, 1 байт
  • wchar_t – строки широких символов (размер зависит от архитектуры)
  • char8_t – тип для представления восьмибитных символов Unicode (начиная с C++20)
  • char16_t – тип для представления 16-битных символов Unicode (UTF-16)
  • char32_t – тип для представления 32-битных символов Unicode (UTF-32)
Символьные литералы (один или два символа) должны заключаться в апострофы (одинарные кавычки):

char ch_1 = 'F';
char ch_2 = '\n';

Использование двойных кавычек приведет к ошибке.
Если используются настройки локали по умолчанию, то корректно обрабатываться будут только символы ASCII (“аски”).

Получить кодовую таблицу ASCII можно в программе Терминал путем ввода команды: man ascii

Символы Unicode будут обработаны как восьмибитные, что вернет предупреждение о переполнении при конвертации: “overflow in conversion from ‘int’ to ‘char’ changes value from ‘53393’ to ‘'\37777777621'’ [-Woverflow]“.

Программа cpp-25.1
#include <iostream>
using namespace std;

int main()
{
    char ascii = 'W';
    char non_ascii_1 = 'Б'; // Предупреждение!
    cout << sizeof(ascii) << '\n'  // 1 байт
         << sizeof(non_ascii_1)    // 1 байт
         << endl;
    wchar_t non_ascii_2 = L'Б';
    cout << sizeof(non_ascii_2)    // 4 байта
         << endl; 
    return 0;
}

Поскольку это не ошибка, а предупреждение, то программу запустить можно, но символ 'Б' не будет обработан корректно. Для работы с символами Unicode следует использовать специальный тип широких символов wchar_t, а инициализацию сопровождать суффиксом L (обозначающий использование широкого символа). О суффиксах символьных литералов мы говорили в §3.
Получить символы можно и другими способами. Так в классе istream определен соответствующий метод get():

cin.get();
cin.get(ch);

Если get() используется без параметров, то из потока ввода будет извлечен один символ. Если извлекаемое значение ничему не присваивается, то символ будет отброшен. Вторая форма имеет в качестве аргумента символьную переменную в которую сохраняется значение, находящееся на очереди в потоке. Можно, например, организовать посимвольное чтение данных в потоке следующим образом:

Программа cpp-25.2
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
	char ch;
	int i = 0;
	while ((ch = cin.get()) != '.') {
		i++;
		cout << setw(i)
			 << ch
			 << endl;
	}
	return 0;
}
Примечание. Чтение данных из потока завершится, если символом окажется точка.

Поскольку, как мы уже сказали, тип char является целочисленным, то нередко можно встретить такой трюк для преобразования символ => число:

char cr = '7';
int s = '7' - '0'; // неявное приведение типа
cout << cr << " = " << s << endl;

Объясняется это просто. Код символа '0' равен 48, а код '7' равен 55. Разница кодов дает цифру 7, которая неявно преобразуется к типу int.
Вывод символов по их кодам можно осуществить следующим образом:

Программа cpp-25.3
#include <iostream>
using namespace std;

int main() {
    for (int i = 33; i < 127; i++)
            cout << i 
                 << " => " 
                 << static_cast<char>(i) 
                 << endl;
    return 0;
}

В этой программе используется явное приведение типов.
Специальными символьными литералами являются управляющие последовательности. Они используются для описания определённых специальных символов внутри строковых литералов.
Некоторые управляющие последовательности:

\'	одинарная кавычка
\"	двойная кавычка
\?	вопросительный знак
\\	обратный слеш
\0	нулевой символ
\n	перевод строки - новая строка
\t	горизонтальная табуляция
\v	вертикальная табуляция
Мы уже применяли символ '\n' для перевода на новую строку при выводе матриц. Манипулятор endl, также осуществляет перевод на новую строку, но, в тоже время, производит сброс буфера вывода (а следовательно и всех форматов при использовании манипуляторов). Поэтому, там где нужно производить переход на новую строку и при этом сохранять форматы, нужно использовать '\n'.

C-строка как символьный массив

C-строка в С++ – это массив символов, в котором последний элемент имеет значение '\0' (нулевой символ).

Примечание. Не путать с символом '0'. В кодовой таблице ASCII нулевой символ (иначе NULL) находится на самой первой позиции, а именно нулевой, откуда и название.

Это значение является признаком конца строки. Символьный массив не является строкой, если он не заканчивается нулевым символом.
В C++ С-строка имеет тип const char* (или const char16_t* и прочие типы). Это означает, что C-строка является массивом фиксированного размера. Для работы с C-строкой STD предоставляет большой набор функций (в данном курсе не рассматриваются, но вы можете познакомиться с ними в этом разделе). Тем не менее, работа с C-строкой является небезопасной и неудобной.
Способ инициализации C-строки, как массива не является удобным:

char str1[] {'T', 'h', 'i', 's', ' ', 's', 't', 'r', 'i', 'n', 'g', '!', '\0'};

Инициализировать C-строку можно более простым способом, заключив её в двойные кавычки:

char str2[] = "This string!";

В этом случае добавлять нулевой символ не нужно. Он будет добавлен неявно автоматически. При вводе символьного массива с клавиатуры нулевой символ также будет добавлен автоматически.
Для получения С-строки можно использовать поток cin, например, так:

const int buf = 80;
char myStr[buf];
cout << "Введите строку: ";
cin >> myStr;

В этом фрагменте константой buf определяется размер массива (с учетом нулевого символа). Существует возможность ограничить количество вводимых символов с помощью манипулятора setw():

cin >> setw(buf) >> myStr;

В поток будут поступать символы, пока не будет встречен пробельный символ (к ним относятся следующие: собственно пробел, \t, \v, \r, \n и \f). В программе пробельный символ будет расцениваться как нулевой символ (т. е. конец строки) и в символьный массив будет записана только часть строки, до первого пробела, а все остальное будет оставлено на очереди в потоке.

Программа cpp-16.4
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
	const int buf = 80;
	char myStr[buf];
	cout << "Введите строку: ";
	cin >> setw(buf) >> myStr;
	cout << "Вы ввели строку:\n"
	     << myStr
	     << endl;
	return 0;
}

Вывод

Введите строку: Это моя первая строка
Вы ввели строку: 
Это

Ввод и вывод C-строки

cin.get()

Но что, если необходимо взять не первое слово, а всю строку целиком? Для чтения строки, имеющей символы пробела, необходимо использовать функцию get(). Эта функция является методом класса istream, поэтому вызывается как функция-член этого класса: cin.get(). Исправим программу cpp-25.4:

const int buf = 80;
char myStr[buf];
cout << "Введите строку: ";
cin.get(str, buf);
cout << "Вы ввели строку:\n"
     << myStr
     << endl;
При использовании операции вставки "<<" и имени массива, С-строка выводится так, как если бы мы использовали переменную строки, т. е. при выводе строки не нужно прибегать к посимвольному выводу в цикле, как это делается для C-массивов.

Функция get() читает строку целиком, включая завершающий символ, но оставляет его в потоке. Завершающий символ (перевода на новую строку) заменяется нулевым символом. Функция get() имеет два аргумента: имя символьного массива и его размер. Если функция используется без аргументов, то произойдет считывание символа и удаление его из потока.

Обычно функция get() используется без аргументов, если необходимо удалить в очереди потока завершающий символ.
cin.getline()

Альтернативой функции get() является функция getline(), которая проще в использовании:

cin.getline(str, buf, char_delim);

Функция имеет три аргумента. Два обязательных: имя массива и размер передаваемого массива с учетом символа конца строки. Третий аргумент (не обязательно) используется для указания завершающего символа. Если третьего аргумента нет, то предполагается, что завершающий символ — '\0'. Функция считывает все символы пока не встретит завершающий символ (или '\n', если завершающий символ не указан в качестве третьего аргумента) или buf символов (с учетом добавления завершающего символа), если количество вводимых символов строки больше, чем указано в buf. Далее функция считывает завершающий символ, но в потоке ввода его не оставляет (также будут удалены и все символы превышающие лимит buf). Если быть точнее, то считывается символ перевода на новую строку, который заменяется на завершающий символ '\0' или char_delim.
Недостатки присущие C-массивам в равной степени относятся и к С-строкам, как к символьным массивам. В продолжении нашего курса, для работы со строкой, будет использоваться исключительно класс string о котором и пойдет речь в следующем разделе.

Создание объекта класса string и его инициализация

Стандартная библиотека языка С++ включает класс последовательного контейнера string, предназначенного для работы с массивами символов. Этот класс более безопасен в использовании, чем С-строка. Инструментарий класса string более чем достаточен, чтобы не прибегать к функциям, работающим со строкой в стиле Си. Вместе с тем, функции класса могут работать не только с экземплярами класса, но и с C-строками. C-строки автоматически преобразуются в строки типа string. (Обратное преобразование не производится, но есть возможность копировать символы в символьные C-массивы). В отличие от C-строки, контейнер string представляет строку как динамический массив. В строках типа string завершающий символ не добавляется в массив, но может содержаться в массиве, как и любой другой символ. Для работы с контейнером string стандартная библиотека предоставляет полный набор функций, общий для всех контейнеров. Для начала работы с классом string необходимо подключить заголовочный файл директивой:

#include <string>

Объявление переменных типа string (на языке ООП - создание экземпляра класса) не отличается от объявления переменных других типов:

string myStr;

Здесь создается пустая строка myStr.
Инициализировать объекты класса string можно различными способами.

//Инициализация во время объявления
string myStr("Это первая строка.");

//Объявление с присваиванием
string myStr = "Это вторая строка.";

//Копирование другой строки (копирующий конструктор);
//othe_Str может быть C-строкой
string myStr(other_Str);

//Определённой длины count C-строки
string myStr(chart_Str, count);

//Заполнить символами
string myStr(20, 'a');

//Копировать из диапазона [pos, pos+count)
string myStr(other_Str, pos, count);

//Создание строки с содержимым диапазона [first, last),
//где first и last - InputIterator
string myStr(first, last);

Ниже демонстрируется использование некоторых способов инициализации.

Программа cpp-25.5
#include <iostream>
#include <string>
using namespace std;

int main() {
	char S1[] = "Aliena vitia in oculis habemus, a tergo nostra sunt";
	//Получаем объект string
	string S2(S1, 13, 9);
	cout << "S2 -> " << S2 << endl;
	string S3(S1);
	// Получаем итераторы для S3;
	auto first = S3.begin();
	auto last = S3.end();
	string S4(first + 13, last - 4);
	cout << "S4 -> " << S4 << endl;
	return 0;
}

Вывод

S2 -> in oculis
S4 -> in oculis habemus, a tergo nostra 
Отдельным символом строку инициализировать нельзя!

Ввод и вывод объектов класса string

getline()

Ввод и вывод строки типа string с помощью потоковых объектов cin и cout будет производиться абсолютно аналогично вводу/выводу C-строки.
Если будет установлен флаг skipws начальные пробелы будут проигнорированы.
Для ввода строки типа string полностью, включая пробельные символы, необходимо использовать функцию getline. Это не тот метод класса istream, который мы применяли для C-строки, а глобальная функция класса string, с помощью которой можно получить объект этого класса. Синтаксис этой функции таков:

getline (istream& is, string& myStr, char_delim);

Аргументы функции:

  • is – потоковый объект ввода;
  • myStr – объект класса string, куда будет помещена строка;
  • char_delim – третий аргумент (необязательный) используется для указания завершающего символа. Если третьего аргумента нет, то предполагается, что завершающий символ — '\n'.
Функция считывает строку полностью с завершающим символом. Сам символ новой строки отбрасывается и в строковой переменной не сохраняется. Также не добавляется нулевой символ. Символ новой строки завершает выполнение функции, где бы он не находился.
Если нулевой символ находится в начале строки, то функция вернет пустую строку. Рассмотрим этот случай:

Программа cpp-25.6
#include <iostream>
#include <string>
using namespace std;

int main() {
	string S1;
	string S2;
	cout << "Введите строку S1\n";
	getline(cin, S1);
	cout << "S1 => " << S1 << endl;
	int i;
	cout << "i = "; cin >> i;
	cout << "Введите строку S2\n";
	//cin.ignore()
	getline(cin, S2);
	cout << i << "\n"
         << "S2 => " << S2 << endl;
	return 0;
}

Вывод

Введите строку S1
This is my string
S1 => This is my string
i = 5
Введите строку S2
5
S2 => 

После выполнения инструкции в стр. 9: getline(cin, S2); и вывода числа программа выводит пустую строку и завершает свою работу. Причина того, что getline не приняла новую строку в том, что поток ввода оказался не пуст! После ввода значения переменной i в потоке остается завершающий символ, этот символ и перехватила функция getline. Для очистки потока можно применить функцию cin.get() или cin.ignore() (убрать в программе комментарий).

Объект класса string как динамический массив

Объект класса string является динамическим массивом. Это означает, что для хранения добавляемых элементов необходимо увеличивать (или уменьшать) размер памяти, отводимой под хранение модифицируемой строки. Эти процессы называются выделением памяти и освобождением памяти. Эту работу берет на себя класс string. Для контейнеров, которые содержат динамически изменяемые структуры данных, существуют два понятия размер и объем.
Смысл этих понятий следующий. При создании объекта класса string в памяти резервируется некоторое адресное пространство, даже если начальный размер массива будет нулевой. Размер массива определяется количеством символов, хранящихся в массиве. Размер можно получить с помощью метода size(). При добавлении элементов, объем массива (т. е. выделенная память для хранения элементов) может увеличится автоматически. Это приведет к процессу перераспределения памяти во время которого все элементы массива будут перенесены в новую область памяти. Для предотвращения этого процесса, настоятельно рекомендуется резервировать емкость строки методом reserve(). (Это не означает запрет на увеличение или уменьшение массива. При необходимости, память может быть увеличена автоматически, если количество элементов станет больше). Для того, чтобы узнать какое количество символов может принять контейнер без увеличения объема, используется метод capacity(). Существует необязательный (для выполнения классом string) метод shrink_to_fit(), который запрашивает уменьшение объема capacity до size.

Методы для работы с объёмом
  • empty() – проверяет есть ли в строке символы, т. е. begin() == end() (возвращает true, если строка пустая, иначе false)
  • size(), length() – возвращает size_t число элементов в строке, иначе std::distance(begin(), end())
  • max_size() – возвращает size_t максимальное количество элементов, которое может содержать строка
  • reserve(new_capacity) – резервирует в памяти место под возможное изменене размера контейнера
  • capacity() – возвращает объем контейнера (size_t символов, которые может содержать контейнер)
  • shrink_to_fit() – необязательный запрос на оптимизацию (уменьшение capacity() до size())

Методы push_back() и pop_back()

Контейнер string устроен так, что вставка и удаление символа в конце массива производится очень быстро. Однако, с точки зрения скорости работы программы, операции вставки в произвольную позицию являются мало эффективными. Это неизбежная плата за работу со строкой любого типа - будь-то C-строка или строка типа string (этот тезис не распространяться на другие типы контейнеров). Дело в том, что классу приходится выполнять работу по сдвигу массива, а заодно, если потребуется, то и работу по выделению памяти, чтобы принять новые элементы. А это, зачастую, приводит к процессу перераспределения памяти.
Для вставки и удаления символов в конце массива используются методы push_back() и pop_back()

push_back

Этот метод добавляет переданный символ к концу строки. Это позволяет произвести "сборку строки" по определенным критериям. Рассмотрим задачу.
Дано четное число N (> 0) и символы C1 и C2. Вывести строку длины N, которая состоит из чередующихся символов C1 и C2, начиная с C1.

Программа cpp-25.7
#include <iostream>
using namespace std;

int main() {
	char C1, C2;
	int N;
	string S; // пустая строка
	cout << "C1 = "; cin >> C1;
	cout << "C2 = "; cin >> C2;
	cout << "N  = "; cin >> N;
	for (int i = 0; i < N; i++) {
		if (i & 1)
			S.push_back(C2);
		else
			S.push_back(C1);
	}
	cout << S << endl;
	return 0;
}

Вывод

C1 = a
C2 = b
N  = 20
abababababababababab
Операции конкатенации ("+" и "+=", которые будут рассмотрены позже) могут выступать как альтернатива использованию этого метода.
pop_back

Этот метод, напротив, удаляет элемент в конце строки.

Примеры решения задач
Вопросы
Темы сообщений
Задания А
Задания Б
Задания С
Ссылки
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Print Friendly, PDF & Email

Обсуждение закрыто.