§19 Символьный тип. C-строка. Объекты класса string. Интернационализация

Символьный тип. Строки в стиле Cи

Символьный тип char

В C++ символьный тип данных char относится к целым типам, так как в переменных этого типа скрыт код символа в кодовой таблице (ASCII). Размер памяти, отводимой на хранение объекта типа char, равен 1 байт.
Также как и другие целые типы, тип char имеет две формы знаковую (signed) и беззнаковую (unsigned). Если используется беззнаковая форма char, то переменной этого типа можно присваивать целочисленные значения в промежутке [0; 255]. Таким образом, символьное значение будет передаваться по коду. Если же переменная инициализируется или ей присваивается символьное значение, то символьный литерал (один или два символа) должен заключаться в апострофы (одинарные кавычки):

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

Заметим, однако, что если локаль не определена, то корректно обрабатываться будут только символы первой части таблицы ASCII («аски»), поскольку она входит в таблицу кодировки UTF-8 без изменений, включая размер отводимый для хранения символа, т. е. 1 байт. Во время компоновки программы (ниже) компилятор выдаст предупреждение, что ‘Б’: «многознаковая символьная константа [-Wmultichar]» и «переполнение при неявном преобразовании константы [-Woverflow]».

char ascii = 'W';
char non_ascii = 'Б';
cout << ascii     << " => " << sizeof(ascii)
     << non_ascii << " => " << sizeof(non_ascii)
     << endl;

Это означает, что символ 'Б' не является однобайтовым. Запустить программу, конечно можно, но символ 'Б' не будет обработан корректно. Чтобы решить данную проблему нужно включить локализацию и использовать для работы с символами специальный тип широких символов wchar_t. Как это сделать мы обсудим ниже.
Поскольку, как мы уже сказали, тип char является целочисленным, то нередко можно встретить такой трюк для преобразования символ => число:

char cr = '7';
int s = '7' - '0';
cout << cr << " = " << s << endl;

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

#include <iostream>
using namespace std;

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

Получить символы можно и другими способами. Так в классе istream определен соответствующий метод get():

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

Если get() используется без параметров, то из потока ввода будет извлечен один символ. Если извлекаемое значение ничему не присваивается, то символ будет отброшен. Вторая форма имеет в качестве аргумента символьную переменную в которую сохраняется значение, находящееся на очереди в потоке. Можно, например, организовать посимвольное чтение данных в потоке следующим образом:
Программа 19.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;
}

Примечание. Чтение данных из потока завершится, если символом окажется точка.

Си-строка

C-строка представляет собой массив символов последним элементом которого является нулевой символ - '\0' (NULL) (не путать с символом '0'). Символьный C-массив должен объявляться следующим образом:

char S1[] = "Это символьная строка с завершающим нулевым символом";
// или, например, так
const int buff = 20;
char S2[buff]; 
cin.getline(S2, buff);

В символьный массив S2 войдет ровно 19 символов и завершающий символ (т. е. всего 20). В массив S1 завершающий символ будет добавлен автоматически. В С++ С-строка имеет тип const char*. Недостатки присущие C-массивам в равной степени относятся и к С-строкам, как к символьным массивам. Здесь более подробно освещается работа с С-строкой. В продолжении нашего курса для работы со строкой будет использоваться исключительно класс string о котором и пойдет речь в следующих разделах.

Класс string. Создание объектов класса string

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

#include <string>

Инициализация объекта класса string

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

string myStr;

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

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

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

//Копирование другой строки (копирующий конструктор)
string myStr(othe_Str);

//Создает строку из содержимого строки с завершающим нулем
string myStr(chart_Str);

//Тоже, но определнный длины count
string myStr(chart_Str, count);

//Заполнить count символами ch
string myStr(count, ch);

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

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

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

#include <iostream>
#include <string>
using namespace std;

int main() {
	// Получаем C-строку
	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

Для ввода/вывода строки используются глобальные потоковые объекты cin и cout и операции >> и <<, соответственно. Если установлен флаг skipws начальные пробелы будут проигнорированы. В поток будут поступать символы, пока не будет встречен пробельный символ (к ним относятся следующие: собственно пробел, \t, \v, \r, \n и \f) или EOF. В программе пробельный символ будет расцениваться как нулевое значение (т. е. конец строки) и в строковую переменную будет записана только часть строки, до первого пробела, остальная часть останется на очереди - в потоке. Такую строку получает переменная myStr в программе 19.4 (показано на выводе, стр. 11). Произвести сцепку оставшихся слов в потоке можно циклом while. (Смотрите ниже).
Программа 19.4

#include <iostream>
#include <string>
using namespace std;

int main() {
	string myStr;
	cout << "Введите строку состоящую\n"
			"из нескольких слов,\n"
			"разделенных пробелами:\n";
	cin >> myStr;
	cout << myStr << endl;
	string S;
	while (!cin.eof() && cin.get() != '\n') {
		cin >> S;
		myStr += ' ' + S;
	}
	cout << myStr << endl;
	return 0;
}
Один два три
Один
Один два три

Чтобы ввести строку полностью, включая пробельные символы, необходимо использовать функцию getline().
Примечание. Не путайте эту глобальную функцию класса string с функцией cin.getline() (методом класса istream), которая обычно применяется для C-строки.
Синтаксис этой функции таков:

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

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

#include <iostream>
#include <string>
using namespace std;

int main() {
	string S1, S2;
	cout << "Введите строку S1\n";
	getline(cin, S1);
	cout << "#1 " << S1 << endl;
	char ch[3];
	cout << "ch = "; cin >> ch;
	cout << "Введите строку S2\n";
	//cin.ignore();
	getline(cin, S2);
	cout << ch << " " << S2 << endl;
	return 0;
}
Введите строку S1
Это первая строка
#1 Это первая строка
ch = #2
Введите строку S2
#2 

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

Локализация. Строки широких символов

Для работы с кодировками символов, которые превышают 8 бит (более "широких") предусмотрен специальный символьный тип wchar_t (wide character). Размер этого типа определяется компилятором и платформой. Так в Windows API wchar_t имеет фиксированный размер 16 бит (что не позволяет закодировать весь набор символов Unicode), а в GNU/Linux - 32 бита.
Прежде чем использовать строку широких символов, необходимо установить "локальный контекст" определяющий в какой кодировке широкий символ будет передаваться в потоки и какой, при этом, национальный стандарт будет использован для форматирования. Для этого необходимо включить директиву:

#include <locale>

В самой программе необходимо создать глобальный объект локального контекста, который соответствует локали окружения. Функции-члену locale() передается строка - имя локального контекста (для Linux это ru_RU.UTF-8), как аргумент:

locale::global(locale("ru_RU.UTF-8"));

Если необходимо, чтобы поток использовал иной локальный контекст, то его следует связать с другим локальным контекстом с помощью функции imbue, например:

wcout.imbue(locale("ru_RU.UTF-8"));
wcin.imbue(locale("ru_RU.UTF-8"));

В одной программе можно использовать несколько локальных контекстов.
Для работы с широкими символами существуют специальные версии типов и классов, которые используются с префиксом "w": wstring, wcout, wcin и т. п. Строка-литерал широких символов должна иметь суффикс "L".
Постановка задачи. Дана строка G1, содержащая как русские, так и английские буквы и символ G2. Подсчитать общее количество символов, равных G2 в строке G1. Если таких символов нет, то сообщить об этом. Получить новую строку символы которой записаны в обратном порядке.
Программа 19.6

#include <iostream>
#include <string>
#include <locale>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
    locale::global(locale("ru_RU.UTF-8"));
    wstring G1;
    //wstring G2;
    wcout << L"Введите строку G1:" << endl;
    getline(wcin, G1);
    wcout << L"Введите символ G2:" << endl;
    //getline(wcin, G2);
    wchar_t G2;
    wcin >> G2;
    int i = 0;
    for (auto &s : G1) {
        if (s == G2) i++;
    }
    reverse(G1.begin(), G1.end());
    if (G1.find(G2) == string::npos)
        wcout << L"Такого символа нет!" << endl;
    else
        wcout << L"В строке найдено " << i
              << L" символов \"" << G2 << "\"" << endl;
    wcout << G1 << endl;
    return 0;
}
Введите строку G1:
Это строка содержащая разные simvoli :))
Введите символ G2:
о
В строке найдено 3 символов о
)): ilovmis еынзар яащажредос акортс отЭ

Примечание. Смысл выражения string::npos, обозначающего, в данном контексте, признак конца строки, мы поясним на следующем уроке.

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

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

push_back

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

#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

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

Конкатенация. Метод append()

В операции конкатенации (лат. concatenatio - «сцепле́ние») происходит объединение строк. Результатом сложения двух строк - является новая строка string. В этих операциях могут участвовать наряду с объектами класса string строковые литералы, одиночные символы и C-строки. Если объекту класса string осуществляется попытка присвоить значение конкатенации литералов (в конкатенации не участвуют объекты класса string), то это приведет к ошибке. Для конкатенации применяются операции "+" и "+=". Перегруженная операция "+=" трактуется как "добавление в конец" строки. Например:

str3 = str1 + str2;
str1 += str3;
str2 = str1 + "wwwwww";

В результате выполнения первой инструкции в str3 будет записана вначале строка str1, а затем строка str2. (Значение, которое строка str3 имела до операции присваивания, будет утрачено). Результат второй инструкции: к символам строки str1 будут дописаны символы строки str3. В третьей инструкции строке str2 будет передано значение строки str1, дополненной литералом.

append()

Фактически операция "+=" может, в некоторых случаях, заменить существующий метод append(). Однако, данный метод гибче, так как позволяет добавлять элементы различными способами, например:
1. подстроку из строки other_str в промежутке [pos, pos + count):

myStr.append(other_str, pos, count);

2. подстроку определённую итераторами first и last:

myStr.append(firs, last);

3. или более лаконичное добавление count символов ch:

myStr.append(count, ch);

Дана непустая строка S и целое число N (> 0). Вывести строку, содержащую символы строки S, между которыми вставлено по N символов '*'.
Программа 19.8

#include <iostream>
#include <string>
using namespace std;

int main() {
	string S;
	size_t i(0), N;
	cout << "Введите строку:\n";
	getline(cin, S);
	cout << "Введите число N:\n";
	cin >> N;
	string subS(N, '*');
	string *temp = new string;
	temp->reserve(N * S.size());
	while (i < S.size()) {
		*temp += S[i] + subS;
		i++;
	}
	S.assign(*temp);
	delete temp;
	S.resize(S.size() - subS.size());
	cout << "Результат:\n"
		 << S;
	return 0;
}
Введите строку:
QQQQQQQQQQQ
Введите число N:
3
Результат:
Q***Q***Q***Q***Q***Q***Q***Q***Q***Q***Q

Лексикографическое сравнение

Лексикографический порядок можно представить в виде следующей последовательности: А < АА < ААА < ААБ < ААВ < АБ < Б < … < ЯЯЯ. Вначале сравниваются первые элементы, если они равны, то сравниваются вторые, затем третьи и т. д. Для сравнения строк используются обычные операции сравнения ==, !=, <, <=, >, >=. В качестве операндов в операциях сравнения могут выступать объекты типа string, C-строки и строковые литералы. Например:
Программа 19.8

#include <iostream>
#include <string>
using namespace std;

int main() {
	string str1, str2;
	cout << "Введите строку 1:\n";
	getline(cin, str1);
	cout << "Введите строку 2:\n";
	getline(cin, str2);
	cout.setf(ios::boolalpha);
	cout << str1 << " > " <<  str2 << " = ";
	if (str1 > str2)
		cout << true << endl;
	else
		cout << false << endl;

	return 0;
}
Введите строку 1:
Луна
Введите строку 2:
Солнце
Луна > Солнце = false

Домашнее задание
  1. Дана строка латинских символов. Преобразовать в ней все строчные буквы в прописные, а прописные — в строчные.
  2. Даны целые положительные числа N1 и N2 и строки S1 и S2. Получить из этих строк новую строку, содержащую первые N1 символов строки S1 и последние N2 символов строки S2 (в указанном порядке).
Презентация

lesson-20string

Литература
  1. Прата, Стивен. Язык программирования C++. Лекции и упражнения, 6-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильяме», 2012
  2. Липпман Б. Стенли, Жози Лажойе, Барбара Э. Му. Язык программирования С++. Базовый курс. Изд. 5-е. М: ООО "И. Д. Вильямс", 2014
  3. Эллайн А. C++. От ламера до программера. СПб.: Питер, 2015
  4. Джосаттис Н. М. Стандартная библиотека C++: справочное руководство, 2-е изд.-М.: Вильямс, 2014
  5. Широкий символ


Добавить комментарий