Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Page 97/1103 Транзакции содержат цену, по которой была продана каждая книга, но структура данных хранит общий доход. Данные транзакции будем читать в переменную price (цена) типа double, исходя из которой и вычислим член revenue (доход). std::cin >> data1.bookNo >> data1.units_sold >> price; Для чтения значений членов bookNo и units_sold (продано экземпляров) объекта по имени data1 оператор ввода использует точечный оператор (см. раздел 1.5.2). Последний оператор присваивает произведение data1.units_sold и price переменной-члену revenue объекта data1. Затем программа повторяет тот же код для чтения данных в объект data2. // читать вторую транзакцию std::cin >> data2.bookNo >> data2.units_sold >> price; data2.revenue = data2.units_sold * price; Вывод суммы двух объектов класса Sales_data Следующая задача — проверить наличие у транзакций одинакового ISBN. Если это так, вывести их сумму, в противном случае отобразить сообщение об ошибке. if (data1.bookNo == data2.bookNo) { unsigned totalCnt = data1.units_sold + data2.units_sold; double totalRevenue = data1.revenue + data2.revenue; // вывести: ISBN, общее количество проданных экземпляров, // общий доход, среднюю цену за книгу std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " "; if (totalCnt != 0) std::cout << totalRevenue/totalCnt << std::endl; else std::cout << "(no sales)" << std::endl; return 0; // означает успех } else { // транзакции не для того же ISBN Page 98/1103 std::cerr << "Data must refer to the same ISBN" << std::endl; return -1; // означает неудачу } Первый оператор if сравнивает члены bookNo объектов data1 и data2. Если эти члены содержат одинаковый ISBN, выполняется код в фигурных скобках, суммирующий компоненты двух переменных. Поскольку необходимо вывести среднюю цену, сначала вычислим общее количество проданных экземпляров и общий доход, а затем сохраним их в переменных totalCnt и totalRevenue соответственно. Выводим эти значения, а затем проверяем, были ли книги проданы, и если да, то выводим вычисленную среднюю цену за книгу. Если никаких продаж не было, выводим сообщение, обращающее внимание на этот факт. Упражнения раздела 2.6.2 Упражнение 2.41. Используйте класс Sales_data для перезаписи кода упражнений из разделов 1.5.1, 1.5.2 и 1.6. А также определите свой класс Sales_data в том же файле, что и функция main(). 2.6.3. Создание собственных файлов заголовка Как будет продемонстрировано в разделе 19.7, класс можно определить в функции, однако такие классы ограничены по функциональным возможностям. Поэтому классы обычно не определяют в функциях. При определении класса за пределами функции в каждом файле исходного кода может быть только одно определение этого класса. Кроме того, если класс используется в нескольких разных файлах, определение класса в каждом файле должно быть тем же. Чтобы гарантировать совпадение определений класса в каждом файле, классы обычно определяют в файлах заголовка. Как правило, классы хранятся в заголовках, имя которых совпадает с именем класса. Например, библиотечный тип string определен в заголовке string. Точно так же, как уже было продемонстрировано, наш класс Sales_data определен в файле заголовка Sales_data.h. Заголовки (обычно) содержат сущности (такие как определения класса или переменных const и constexpr (см. раздел 2.4), которые могут быть определены в любом файле только однажды. Однако заголовки нередко должны использовать средства из других заголовков. Например, поскольку у класса Sales_data есть член типа string, заголовок Sales_data.h должен включать заголовок string. Как уже упоминалось, программы, использующие класс Sales_data, должны также включать заголовок string, чтобы использовать член bookNo. В результате использующие класс Sales_data программы будут включать заголовок string дважды: один раз непосредственно и один раз как следствие включения заголовка Sales_data.h. Поскольку заголовок мог бы быть включен несколько раз, код необходимо писать так, чтобы обезопасить от многократного включения. После внесения любых изменений в заголовок необходимо перекомпилировать все использующие его файлы исходного кода, чтобы вступили в силу новые или измененные объявления. Краткое введение в препроцессор Page 99/1103 Наиболее распространенный способ обезопасить заголовок от многократного включения подразумевает использование препроцессора. Препроцессор (preprocessor), унаследованный языком С++ от языка С, является программой, которая запускается перед компилятором и изменяет исходный текст программ. Наши программы уже полагаются на такое средство препроцессора, как директива #include. Когда препроцессор встречает директиву #include, он заменяет ее содержимым указанного заголовка. Программы С++ используют также препроцессор для защиты заголовка (header guard). Защита заголовка полагается на переменные препроцессора (см. раздел 2.3.2). Переменные препроцессора способны находиться в одном из двух состояний: она либо определена, либо не определена. Директива #define получает имя и определяет его как переменную препроцессора. Есть еще две директивы, способные проверить, определена ли данная переменная препроцессора или нет. Директива #ifdef истинна, если переменная была определена, а директива #ifndef истинна, если переменная не была определена. В случае истинности проверки выполняется все, что расположено после директивы #ifdef или #ifndef и до соответствующей директивы #endif. Эти средства можно использовать для принятия мер против множественного включения следующим образом: #ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; #endif При первом включении заголовка Sales_data.h директива #ifndef истинна, и препроцессор обработает строки после нее до директивы #endif. В результате переменная препроцессора SALES_DATA_H будет определена, а содержимое заголовка Sales_data.h скопировано в программу. Если впоследствии включить заголовок Sales_data.h в тот же файл, то директива #ifndef окажется ложна и строки между ней и директивой #endif будут проигнорированы. Имена переменных препроцессора не подчиняются правилам областей видимости языка С++. Переменные препроцессора, включая имена для защиты заголовка, должны быть уникальными во всей программе. Обычно мы гарантируем уникальность имен защиты заголовка, включая в него имя класса. Чтобы избежать конфликта имен с другими сущностями программы, имена переменных препроцессора обычно пишут полностью в верхнем регистре. У заголовков должна быть защита, даже если они не включаются в другие заголовки. Защита заголовка проста в написании, и при привычном их определении не нужно размышлять, Page 100/1103 нужны они или нет. Упражнения раздела 2.6.3 Упражнение 2.42. Напишите собственную версию заголовка Sales_data.h и используйте его для новой версии упражнения из раздела 2.6.2. Резюме Типы — фундаментальная часть всех программ С++. Каждый тип определяет требования по хранению и операциям, которые можно выполнять с объектами этого типа. Язык предоставляет набор фундаментальных встроенных типов, таких как int и char, которые тесно связаны с их представлением на аппаратных средствах машины. Типы могут быть неконстантными или константными; константный объект следует инициализировать. Будучи однажды инициализированным, значение константного объекта не может быть изменено. Кроме того, можно определить составные типы, такие как указатели или ссылки. Составной тип — это тип, определенный в терминах другого типа. Язык позволяет определять собственные типы, т.е. классы. Библиотека использует классы, чтобы предоставить набор таких высокоуровневых абстракций, как типы IO и string. Термины Адрес (address). Номер байта в памяти, начиная с которого располагается объект. Арифметический тип (arithmetic type). Встроенные типы, представляющие логические значения, символы, целые числа и числа с плавающей запятой. Базовый тип (base type). Спецификатор типа, возможно со спецификатором const, который предшествует оператору объявления в объявлении. Базовый тип представляет общий тип, на основании которого строятся операторы объявления в объявлении. Байт (byte). Наименьший адресуемый блок памяти. На большинстве машин байт составляет 8 битов. Беззнаковый тип (unsigned). Целочисленный тип данных, переменные которого способны хранить значения больше или равные нулю. В области видимости (in scope). Имя, которое видимо от текущей области видимости. Внешняя область видимости (outer scope). Область видимости, включающая другую область видимости. Внутренняя область видимости (inner scope). Область видимости, вложенная в другую область видимости. Внутриклассовый инициализатор (in-class initializer). Инициализатор, предоставленный как часть объявления переменной-члена класса. За внутриклассовым инициализатором следует символ =, или он заключается в фигурные скобки. Временный объект (temporary). Безымянный объект, создаваемый компилятором при Page 101/1103 вычислении выражения. Временный объект существует до конца вычисления всего выражения, для которого он был создан. Глобальная область видимости (global scope). Область видимости, внешняя для всех остальных областей видимости. Директива препроцессора #define. Определяет переменную препроцессора. Директива препроцессора #endif. Завершает область #ifdef или #ifndef. Директива препроцессора #ifdef. Выясняет, что данная переменная определена. Директива препроцессора #ifndef. Выясняет, что данная переменная не определена. Защита заголовка (header guard). Переменная препроцессора, предназначенная для предотвращения неоднократного подключения содержимого заголовка в один файл исходного кода. Знаковый тип (signed). Целочисленный тип данных, переменные которого способны хранить отрицательные и положительные числа, включая нуль. Идентификатор (identifier). Последовательность символов, составляющая имя. Идентификатор зависит от регистра символов. Инициализация (initialization). Присвоение переменной исходного значения при ее определении. Обычно переменные следует инициализировать. Инициализация по умолчанию (default initialization). Способ инициализации объектов при отсутствии явной инициализации. Инициализация объектов типа класса определяется классом. Объекты встроенного типа, определенного в глобальной области видимости, инициализируются значением 0, а определенные в локальной области видимости остаются неинициализированными и имеют неопределенное значение. Интегральный тип (integral type). То же, что и арифметический или целочисленный тип. Ключевое слово struct. Используется при определении структуры (класса). Ключевое слово typedef. Позволяет определить псевдоним для другого типа. Когда ключевое слово typedef присутствует в объявлении базового типа, определенные в объявлении имена становятся именами типа. Константная ссылка (const reference). Разговорный термин для ссылки на константный объект. Константное выражение (constant expression). Выражение, значение которого может быть вычислено во время компиляции. Константный указатель (const pointer). Указатель со спецификатором const. Контроль соответствия типов (type checking). Термин, описывающий процесс проверки компилятором соответствия способа использования объекта заявленному для него типу. Литерал (literal) Значение, такое как число, символ или строка символов. Это значение не может быть изменено. Символьные литералы заключают в одинарные кавычки, а строковые литералы в двойные. Литерал nullptr. Литеральная константа, означающая нулевой указатель. Page 102/1103 Локальная область видимости (local scope). Разговорный синоним для области действия блока кода. Массив (array). Структура данных, содержащая коллекцию неименованных объектов, к которым можно обращаться по индексу. Более подробная информация о массивах приведена в разделе 3.5. Неинициализированная переменная (uninitialized variable). Переменная, определенная без исходного значения. Обычно попытка доступа к значению неинициализированной переменной приводит к неопределенному поведению. Неопределенное поведение (undefined behavior). Случай, для которого стандарт языка не определяет значения. Осознанно или неосознанно, но полагаться на неопределенное поведение нельзя. Оно является источником трудно обнаруживаемых ошибок времени выполнения, проблем безопасности и переносимости. Непечатаемый символ (nonprintable character). Символ, не имеющий видимого представления, например символ возврата на один символ, символ новой строки и т.д. Нулевой указатель (null pointer). Указатель со значением 0. Нулевой указатель допустим, но не указывает ни на какой объект. Область видимости (scope). Часть программы, в которой имена имеют смысл. Язык С++ имеет несколько уровней областей видимости. Глобальная (global) — имена, определенные вне остальных областей видимости. Класса (class) — имена, определенные классом. Пространства имен (namespace) — имена, определенные в пространстве имен. Блока (block) — имена, определенные в блоке операторов, т.е. в паре фигурных скобок. Области видимости могут быть вложенными. Как только имя объявлено, оно доступно до конца той области видимости, в которой было объявлено. Объект (object). Область памяти, которая имеет тип. Переменная — это объект, который имеет имя. Объявление (declaration). Уведомление о существовании переменной, функции или типа, определяемых в другом месте программы. Никакие имена не могут быть использованы, пока они не определены или не объявлены. Объявление псевдонима (alias declaration). Определяет синоним для другого типа. Объявление в формате using имя = тип объявляет имя как синоним типа тип . Оператор &. Оператор обращения к адресу. Возвращает адрес объекта, к которому он был применен. Оператор *. Оператор обращения к значению. Обращение к значению указателя возвращает Page 103/1103 объект, на который указывает указатель. Присвоение результату оператора обращения к значению присваивает новое значение основному объекту. Оператор объявления (declarator). Часть объявления, включающая определяемое имя и, необязательно, модификатор типа. Определение (definition). Резервирует область в памяти для хранения данных переменной и (необязательно) инициализирует ее значение. Никакие имена не могут быть использованы, пока они не определены или не объявлены. Переменная (variable). Именованный объект или ссылка. В языке С++ переменные должны быть объявлены перед использованием. Переменная constexpr. Переменная, которая представляет константное выражение. Функции constexpr рассматриваются в разделе 6.5.2. Переменная препроцессора (preprocessor variable). Переменная, используемая препроцессором. Препроцессор заменяет каждую переменную препроцессора ее значением прежде, чем программа будет откомпилирована. Переменная-член (data member). Элемент данных, которые составляют объект. Каждый объект некоего класса обладает собственными экземплярами переменных-членов. Переменные-члены могут быть инициализированы в объявлении класса. Преобразование (conversion). Процесс, в результате которого значение одного типа преобразуется в значение другого. Преобразования между встроенными типами определены в самом языке. Препроцессор (preprocessor). Препроцессор — это программа, автоматически запускаемая перед компилятором С++. Псевдоним типа (type alias). Имя, являющееся синонимом для другого типа. Определяется при помощи ключевого слова typedef или объявления псевдонима. Раздельная компиляция (separate compilation). Возможность разделить программу на несколько отдельных файлов исходного кода. Связывание (bind). Соединение имени с указанной сущностью, чтобы использование имени приводило к использованию основной сущности. Например, ссылка — это имя, связанное с объектом. Слово (word). Специфический для каждой машины размер блока памяти, применяемый при целочисленных вычислениях. Обычно размер слова достаточно велик, чтобы содержать адрес. 32-битовое слово обычно занимает 4 байта. Составной тип (compound type). Тип, определенный в терминах другого типа. Спецификатор auto. Спецификатор типа, позволяющий вывести тип переменной из ее инициализатора. Спецификатор const верхнего уровня (top-level const). Спецификатор const, указывающий, что объект не может быть изменен. Спецификатор const нижнего уровня (low-level const). Спецификатор const не верхнего уровня. Такие спецификаторы const являются неотъемлемой частью типа и никогда не игнорируются. Page 104/1103 Спецификатор const. Спецификатор типа, определяющий объекты, которые не могут быть изменены. Константные объекты следует инициализировать, поскольку нет никакого способа присвоить им значение после определения. Спецификатор decltype. Спецификатор типа, позволяющий вывести тип переменной или выражения. Спецификатор типа (type specifier). Имя типа. Списочная инициализация (list initialization). Форма инициализации, подразумевающая использование фигурных скобок для включения одного или нескольких инициализаторов. Ссылка (reference). Псевдоним другого объекта. Ссылка на константу (reference to const). Ссылка, неспособная изменить значение объекта, на который она ссылается. Ссылка на константу может быть связана с константным, неконстантным объектом или с результатом выражения. Тип string. Библиотечный тип, представляющий последовательность символов переменной длины. Тип void*. Специальный тип указателя, способного указывать на любой неконстантный тип. Обращение к значению таких указателей невозможно. Тип void. Специальный тип без значения и допустимых операций. Нельзя определить переменную типа void. Указатель (pointer). Объект, способный содержать адрес объекта, следующий адрес за концом объекта или нуль. Указатель на константу (pointer to const). Указатель, способный содержать адрес константного объекта. Указатель на константу не может использоваться для изменения значения объекта, на который он указывает. Управляющая последовательность (escape sequence). Альтернативный механизм представления символов. Обычно используется для представления непечатаемых символов, таких как символ новой строки или табуляции. Управляющая последовательность состоит из символа наклонной черты влево, сопровождаемой символом, восьмеричным числом из трех цифр, или символа x, сопровождаемого шестнадцатеричным числом. Член класса (class member, member). Часть класса. Глава 3 Типы string, vector и массивы Кроме встроенных типов, рассмотренных в главе 2, язык С++ предоставляет богатую библиотеку абстрактных типов данных. Важнейшими библиотечными типами являются тип string, поддерживающий символьные строки переменной длины, и тип vector, определяющий коллекции переменного размера. С типами string и vector связаны типы, известные как итераторы (iterator). Они используются для доступа к символам строк и элементам векторов. Типы string и vector, определенные в библиотеке, являются абстракциями более простого Page 105/1103 встроенного типа массива. Эта главы посвящена массивам и введению в библиотечные типы vector и string. Встроенные типы, рассмотренные в главе 2, определены непосредственно языком С++. Эти типы представляют средства, которые сами по себе присущи большинству компьютеров, такие как числа или символы. Стандартная библиотека определяет множество дополнительных типов, высокоуровневый характер которых аппаратными средствами компьютеров, как правило, не реализуется непосредственно. В данной главе представлены два важнейших библиотечных типа: string и vector. Тип string — это последовательность символов переменной длины. Тип vector содержит последовательность объектов указанного типа переменной длины. Мы также рассмотрим встроенный тип массива. Как и другие встроенные типы, массивы представляют возможности аппаратных средств. В результате массивы менее удобны в использовании, чем библиотечные типы string и vector. Однако, прежде чем начать исследование библиотечных типов, рассмотрим механизм, упрощающий доступ к именам, определенным в библиотеке. 3.1. Пространства имен и объявления using До сих пор имена из стандартной библиотеки упоминались в программах явно, т.е. перед каждым из них было указано имя пространства имен std. Например, при чтении со стандартного устройства ввода применялась форма записи std::cin. Здесь использован оператор области видимости :: (см. раздел 1.2). Он означает, что имя, указанное в правом операнде оператора, следует искать в области видимости, указанной в левом операнде. Таким образом, код std::cin означает, что используемое имя cin определено в пространстве имен std. При частом использовании библиотечных имен такая форма записи может оказаться чересчур громоздкой. К счастью, существуют и более простые способы применения членов пространств имен. Самый надежный из них — объявление using (using declaration). Другие способы, позволяющие упростить использование имен из других пространств, рассматриваются в разделе 18.2.2. Объявление using позволяет использовать имена из другого пространства имен без указания префикса имя_пространства_имен ::. Объявление using имеет следующий формат: using пространство_имен :: имя ; После того как объявление using было сделано один раз, к указанному в нем имени можно обращаться без указания пространства имен. #include <iostream> Page 106/1103 // объявление using; при использовании имени cin теперь // подразумевается, что оно принадлежит пространству имен std using std::cin; int main() { int i; cin >> i; // ok: теперь cin - синоним std::cin cout << i; // ошибка: объявления using нет; здесь нужно указать // полное имя std::cout << i; // ok: явно указано применение cout из // пространства имен std return 0; } Для каждого имени необходимо индивидуальное объявление using Каждое объявление using применяется только к одному элементу пространства имен. Это позволяет жестко задавать имена, используемые в каждой программе. Например, программу из раздела 1.2 можно переписать следующим образом: #include <iostream> // объявления using для имен из стандартной библиотеки using std::cin; using std::cout; using std::endl; int main() { cout << "Enter two numbers:" << endl; int v1, v2; Page 107/1103 cin >> v1 >> v2; cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << endl; return 0; } Объявления using для имен cin, cout и endl означают, что их можно теперь использовать без префикса std::. Напомню, что программы С++ позволяют поместить каждое объявление using в отдельную строку или объединить в одной строке несколько объявлений. Важно не забывать, что для каждого используемого имени необходимо отдельное объявление using, и каждое из них должно завершаться точкой с запятой. Заголовки не должны содержать объявлений using Код в заголовках (см. раздел 2.6.3) обычно не должен использовать объявления using. Дело в том, что содержимое заголовка копируется в текст программы, в которую он включен. Если в заголовке есть объявление using, то каждая включающая его программа получает то же объявление using. В результате программа, которая не намеревалась использовать определенное библиотечное имя, может случайно столкнуться с неожиданным конфликтом имен. Примечание для читателя Начиная с этого момента подразумевается, что во все примеры включены объявления using для имен из стандартной библиотеки. Таким образом, в тексте и примерах кода далее упоминается cin, а не std::cin. Кроме того, для экономии места в примерах кода не будем показывать далее объявления using и необходимые директивы #include. В табл. А.1 приложения А приведены имена и соответствующие заголовки стандартной библиотеки, которые использованы в этой книге. Читатели не должны забывать добавить соответствующие объявления #include и using в свои примеры перед их компиляцией. Упражнения раздела 3.1 Упражнение 3.1. Перепишите упражнения из разделов 1.4.1 и 2.6.2, используя соответствующие объявления using. 3.2. Библиотечный тип string Строка (string) — это последовательность символов переменной длины. Чтобы использовать тип string, необходимо включить в код заголовок string. Поскольку тип string принадлежит библиотеке, он определен в пространстве имен std. Наши примеры подразумевают наличие следующего кода: #include <string> using std::string; В этом разделе описаны наиболее распространенные операции со строками; а дополнительные операции рассматриваются в разделе 9.5. Кроме определения операций, предоставляемых библиотечными типами, стандарт налагает Page 108/1103 также требования на эффективность их конструкторов. В результате библиотечные типы оказались весьма эффективны в использовании. 3.2.1. Определение и инициализация строк Каждый класс определяет, как могут быть инициализированы объекты его типа. Класс может определить много разных способов инициализации объектов своего типа. Каждый из способов отличается либо количеством предоставляемых инициализаторов, либо типами этих инициализаторов. Список наиболее распространенных способов инициализации строк приведен в табл. 3.1, а некоторые из примеров приведены ниже. string s1; // инициализация по умолчанию; s1 - пустая строка string s2 = s1; // s2 - копия s1 string s3 = "hiya"; // s3 - копия строкового литерала string s4(10, 'c'); // s4 - cccccccccc Инициализация строки по умолчанию (см. раздел 2.2.1) создает пустую строку; т.е. объект класса string без символов. Когда предоставляется строковый литерал (см. раздел 2.1.3), во вновь созданную строку копируются символы этого литерала, исключая завершающий нулевой символ. При предоставлении количества и символа строка содержит указанное количество экземпляров данного символа. Таблица 3.1. Способы инициализации объекта класса string string s1 Инициализация по умолчанию; s1 — пустая строка string s2(s1) s2 — копия s1 string s2 = s1 Эквивалент s2(s1), s2 — копия s1 string s3("value") s3 — копия строкового литерала, нулевой символ не включен string s3 = "value" Эквивалент s3("value"), s3 — копия строкового литерала string s4(n, 'c') Инициализация переменной s4 символом 'c' в количестве n штук Прямая инициализация и инициализация копией В разделе 2.2.1 упоминалось, что язык С++ поддерживает несколько разных форм инициализации. Давайте на примере класса string начнем изучать, чем эти формы отличаются друг от друга. Когда переменная инициализируется с использованием знака =, компилятор просят скопировать инициализирующий объект в создаваемый объект, т.е. выполнить инициализацию копией (copy initialization). В противном случае без знака = осуществляется прямая инициализация (direct initialization). Когда имеется одиночный инициализатор, можно использовать и прямую форму, и инициализацию копией. Но при инициализации переменной несколькими значениями, как при Page 109/1103 инициализации переменной s4 выше, следует использовать прямую форму инициализации. string s5 = "hiya"; // инициализация копией string s6("hiya"); // прямая инициализация string s7(10, 'c'); // прямая инициализация; s7 - сссссссссс Если необходимо использовать несколько значений, можно применить косвенную форму инициализации копией при явном создании временного объекта для копирования. string s8 = string(10, 'c'); // инициализация копией; s8 - сссссссссс Инициализатор строки s8 — string(10, 'c') — создает строку заданного размера, заполненную указанным символьным значением, а затем копирует ее в строку s8. Это эквивалентно следующему коду: string temp(10, 'c'); // temp - сссссссссс string s8 = temp; // копировать temp в s8 Хотя используемый для инициализации строки s8 код вполне допустим, он менее читабелен и не имеет никаких преимуществ перед способом, которым была инициализирована переменная s7. 3.2.2. Операции со строками Наряду с определением способов создания и инициализации объектов класс определяет также операции, которые можно выполнять с объектами класса. Класс может определить обладающие именем операции, такие как функция isbn() класса Sales_item (см. раздел 1.5.2). Класс также может определить то, что означают различные символы операторов, такие как << или +, когда они применяются к объектам класса. Наиболее распространенные операции класса string приведены в табл. 3.2. Таблица 3.2. Операции класса string os << s Выводит строку s в поток вывода os. Возвращает поток os is >> s Читает разделенную пробелами строку s из потока is. Возвращает поток is getline(is, s) Читает строку ввода из потока is в переменную s. Возвращает поток is s.empty() Возвращает значение true, если строка s пуста. В противном случае возвращает значение false s.size() Возвращает количество символов в строке s s[n] Возвращает ссылку на символ в позиции n строки s; позиции отсчитываются от 0 s1 + s2 Page 110/1103 Возвращает строку, состоящую из содержимого строк s1 и s2 s1 = s2 Заменяет символы строки s1 копией содержимого строки s2 s1 == s2 s1 != s2 Строки s1 и s2 равны, если содержат одинаковые символы. Регистр символов учитывается <, <=, >, >= Сравнение зависит от регистра и полагается на алфавитный порядок символов Чтение и запись строк Как уже упоминалось в главе 1, для чтения и записи значений встроенных типов, таких как int, double и т.д., используется библиотека iostream. Для чтения и записи строк используются те же операторы ввода и вывода. // Обратите внимание: перед компиляцией этот код следует дополнить // директивами #include и объявлениями using int main() { string s; // пустая строка cin >> s; // чтение разделяемой пробелами строки в s cout << s << endl; // запись s в поток вывода return 0; } Программа начинается с определения пустой строки по имени s. Следующая строка читает данные со стандартного устройства ввода и сохраняет их в переменной s. Оператор ввода строк читает и отбрасывает все предваряющие непечатаемые символы (например, пробелы, символы новой строки и табуляции). Затем он читает значащие символы, пока не встретится следующий непечатаемый символ. Таким образом, если ввести " Hello World! " (обратите внимание на предваряющие и завершающие пробелы), фактически будет получено значение "Hello" без пробелов. Подобно операторам ввода и вывода встроенных типов, операторы строк возвращают как результат свой левый операнд. Таким образом, операторы чтения или записи можно объединять в цепочки. string s1, s2; cin >> s1 >> s2; // сначала прочитать в переменную s1, // а затем в переменную s2 Page 111/1103 cout << s1 << s2 << endl; // отобразить обе строки Если в этой версии программы осуществить предыдущий ввод, " Hello World! ", выводом будет "HelloWorld!". Чтение неопределенного количества строк В разделе 1.4.3 уже рассматривалась программа, читающая неопределенное количество значений типа int. Напишем подобную программу, но читающую строки. int main() { string word; while (cin >> word) // читать до конца файла cout << word << endl; // отобразить каждое слово с новой строки return 0; } Здесь чтение осуществляется в переменную типа string, а не int. Условие оператора while, напротив, выполняется так же, как в предыдущей программе. Условие проверяет поток после завершения чтения. Если поток допустим, т.е. не встретился символ конца файла или недопустимое значение, выполняется тело цикла while. Оно выводит прочитанное значение на стандартное устройство вывода. Как только встречается конец файла (или недопустимый ввод), цикл while завершается. Применение функции getline() для чтения целой строки Иногда игнорировать пробелы во вводе не нужно. В таких случаях вместо оператора >> следует использовать функцию getline(). Функция getline() получает поток ввода и строку. Функция читает предоставленный поток до первого символа новой строки и сохраняет прочитанное, исключая символ новой строки, в своем аргументе типа string. Встретив символ новой строки, даже если это первый символ во вводе, функция getline() прекращает чтение и завершает работу. Если символ новой строки во вводе первый, то возвращается пустая строка. Подобно оператору ввода, функция getline() возвращает свой аргумент типа istream. В результате функцию getline() можно использовать в условии, как и оператор ввода (см. раздел 1.4.3). Например, предыдущую программу, которая выводила по одному слову в строку, можно переписать так, чтобы она вместо этого выводила всю строку: int main() { string line; // читать строки до конца файла while (getline(cin, line)) cout << line << endl; Page 112/1103 return 0; } Поскольку переменная line не будет содержать символа новой строки, его придется вывести отдельно. Для этого, как обычно, используется манипулятор endl, который, кроме перевода строки, сбрасывает буфер вывода. Символ новой строки, прекращающий работу функции getline(), отбрасывается и в строковой переменной не сохраняется .Строковые операции size() и empty() Функция empty() (пусто) делает то, что и ожидается: она возвращает логическое значение true (раздел 2.1), если строка пуста, и значение false — в противном случае. Подобно функции-члену isbn() класса Sales_item (см. раздел 1.5.2), функция empty() является членом класса string. Для вызова этой функции используем точечный оператор, позволяющий указать объект, функцию empty() которого необходимо вызвать. А теперь пересмотрим предыдущую программу так, чтобы она выводила только непустые строки: // читать ввод построчно и отбрасывать пустые строки while (getline(cin, line)) if (!line.empty()) cout << line << endl; Условие использует оператор логического NOT ( оператор !). Он возвращает инверсное значение своего операнда типа bool. В данном случае условие истинно, если строка line не пуста. Функция size() возвращает длину строки (т.е. количество символов в ней). Давайте используем ее для вывода строк длиной только больше 80 символов. string line; // читать ввод построчно и отображать строки длиной более 80 символов while (getline(cin, line)) if (line.size() > 80) cout << line << endl; Тип string::size_type Вполне логично ожидать, что функция size() возвращает значение типа int, а учитывая сказанное в разделе 2.1.1, вероятней всего, типа unsigned. Но вместо этого функция size() возвращает значение типа string::size_type. Этот тип требует более подробных объяснений. Page 113/1103 В классе string (и нескольких других библиотечных типах) определены вспомогательные типы данных. Эти вспомогательные типы позволяют использовать библиотечные типы машинно-независимым способом. Тип size_type — это один из таких вспомогательных типов. Чтобы воспользоваться типом size_type, определенным в классе string, применяется оператор области видимости (оператор ::), указывающий на то, что имя size_type определено в классе string. Хотя точный размер типа string::size_type неизвестен, можно с уверенностью сказать, что этот беззнаковый тип (см. раздел 2.1.1) достаточно большой, чтобы содержать размер любой строки. Любая переменная, используемая для хранения результата операции size() класса string, должна иметь тип string::size_type. По общему признанию, довольно утомительно вводить каждый раз тип string::size_type. По новому стандарту можно попросить компилятор самостоятельно применить соответствующий тип при помощи спецификаторов auto или decltype (см. раздел 2.5.2): auto len = line.size(); // len имеет тип string::size_type Поскольку функция size() возвращает беззнаковый тип, следует напомнить, что выражения, в которых смешаны знаковые и беззнаковые данные, могут дать непредвиденные результаты (см. раздел 2.1.2). Например, если переменная n типа int содержит отрицательное значение, то выражение s.size() < n почти наверняка истинно. Оно возвращает значение true потому, что отрицательное значение переменной n преобразуется в большое беззнаковое значение. Проблем преобразования между беззнаковыми и знаковыми типами можно избежать, если не использовать переменные типа int в выражениях, где используется функция size(). Сравнение строк Класс string определяет несколько операторов для сравнения строк. Эти операторы сравнивают строки посимвольно. Результат сравнения зависит от регистра символов, символы в верхнем и нижнем регистре отличаются. Операторы равенства (== и !=) проверяют, равны или не равны две строки соответственно. Две строки равны, если у них одинаковая длина и одинаковые символы. Операторы сравнения (<, >, <=, >=) проверяют, меньше ли одна строка другой, больше, меньше или равна, больше или равна другой. Эти операторы используют ту же стратегию, старшинство символов в алфавитном порядке в зависимости от регистра. 1. Если длина у двух строк разная и если каждый символ более короткой строки совпадает с соответствующим символом более длинной, то короткая строка меньше длинной. 2. Если символы в соответствующих позициях двух строк отличаются, то результат сравнения определяется первым отличающимся символом. Для примера рассмотрим следующие строки: string str = "Hello"; string phrase = "Hello World"; string slang = "Hiya"; Согласно правилу 1 строка str меньше строки phrase. Согласно правилу 2 строка slang больше, чем строки str и phrase. Присвоение строк Page 114/1103 Как правило, библиотечные типы столь же просты в применении, как и встроенные. Поэтому большинство библиотечных типов поддерживают присвоение. Строки не являются исключением, один объект класса string вполне можно присвоить другому. string st1(10, 'c'), st2; // st1 - сссссссссс; st2 - пустая строка st1 = st2; // присвоение: замена содержимого st1 копией st2 // теперь st1 и st2 - пустые строки Сложение двух строк Результатом сложения двух строк является новая строка, объединяющая содержимое левого операнда, а затем правого. Таким образом, при применении оператора суммы (оператор +) к строкам результатом будет новая строка, символы которой являются копией символов левого операнда, сопровождаемые символами правого операнда. Составной оператор присвоения (оператор +=) (см. раздел 1.4.1) добавляет правый операнд к строке слева: string s1 = "hello, ", s2 = "world\n"; string s3 = s1 + s2; // s3 - hello, world\n s1 += s2; // эквивалентно s1 = s1 + s2 Сложение строк и символьных строковых литералов Как уже упоминалось в разделе 2.1.2, один тип можно использовать там, где ожидается другой тип, если есть преобразование из данного типа в ожидаемый. Библиотека string позволяет преобразовывать как символьные, так и строковые литералы (см. раздел 2.1.3) в строки. Поскольку эти литералы можно использовать там, где ожидаются строки, предыдущую программу можно переписать следующим образом: string s1 = "hello", s2 = "world"; // в s1 и s2 нет пунктуации string s3 = s1 + ", " + s2 + '\n'; Когда объекты класса string смешиваются со строковыми или символьными литералами, то по крайней мере один из операндов каждого оператора + должен иметь тип string. string s4 = s1 + ", "; // ok: сложение строки и литерала string s5 = "hello" + ", "; // ошибка: нет строкового операнда string s6 = s1 + ", " + "world"; // Page 115/1103 ok: каждый + имеет // строковый операнд string s7 = "hello" + ", " + s2; // ошибка: нельзя сложить строковые // литералы В инициализации переменных s4 и s5 задействовано только по одному оператору, поэтому достаточно просто проверить его корректность. Инициализация переменной s6 может показаться странной, но работает она аналогично объединенным в цепочку операторам ввода или вывода (см. раздел 1.2). Это эквивалентно следующему коду: string s6 = (s1 + ", ") + "world"; Часть s1 + ", " выражения возвращает объект класса string, она составляет левый операнд второго оператора +. Это эквивалентно следующему коду: string tmp = s1 + ", "; // ok: + имеет строковый операнд s6 = tmp + "world"; // ok: + имеет строковый операнд С другой стороны, инициализация переменной s7 недопустима, и это становится очевидным, если заключить часть выражения в скобки: string s7 = ("hello" + ", ") + s2; // ошибка: нельзя сложить строковые // литералы Теперь довольно просто заметить, что первая часть выражения суммирует два строковых литерала. Поскольку это невозможно, оператор недопустим. По историческим причинам и для совместимости с языком С строковые литералы не принадлежат к типу string стандартной библиотеки. При использовании строковых литералов и библиотечного типа string, не следует забывать, что это разные типы.Упражнения раздела 3.2.2 Упражнение 3.2. Напишите программу, читающую со стандартного устройства ввода по одной строке за раз. Измените программу так, чтобы читать по одному слову за раз. Упражнение 3.3. Объясните, как символы пробелов обрабатываются в операторе ввода класса string и в функции getline(). Упражнение 3.4. Напишите программу, читающую две строки и сообщающую, равны ли они. В Page 116/1103 противном случае программа сообщает, которая из них больше. Затем измените программу так, чтобы она сообщала, одинаковая ли у строк длина, а в противном случае — которая из них длиннее. Упражнение 3.5. Напишите программу, читающую строки со стандартного устройства ввода и суммирующую их в одну большую строку. Отобразите полученную строку. Затем измените программу так, чтобы отделять соседние введенные строки пробелами. 3.2.3. Работа с символами строки Зачастую приходится работать с индивидуальными символами строки. Например, может понадобиться выяснить, является ли определенный символ пробелом, или изменить регистр символов на нижний, или узнать, присутствует ли некий символ в строке, и т.д. Одной из частей этих действий является доступ к самим символам строки. Иногда необходима обработка каждого символа, а иногда лишь определенного символа, либо может понадобиться прекратить обработку, как только выполнится некое условие. Кроме того, это наилучший способ справиться со случаями, когда задействуются разные языковые и библиотечные средства. Другой частью обработки символов является выяснение и (или) изменение их характеристик. Эта часть задачи выполняется набором библиотечных функций, описанных в табл. 3.3. Данные функции определены в заголовке cctype. Таблица 3.3. Функции cctype isalnum(с) Возвращает значение true, если с является буквой или цифрой isalpha(с) Возвращает значение true, если с — буква iscntrl(с) Возвращает значение true, если с — управляющий символ isdigit(с) Возвращает значение true, если с — цифра isgraph(с) Возвращает значение true, если с — не пробел, а печатаемый символ islower(с) Возвращает значение true, если с — символ в нижнем регистре isprint(с) Возвращает значение true, если с — печатаемый символ ispunct(с) Возвращает значение true, если с — знак пунктуации (т.е. символ, который не является управляющим символом, цифрой, символом или печатаемым отступом) isspace(с) Возвращает значение true, если с — символ отступа (т.е. пробел, табуляция, вертикальная табуляция, возврат, новая строка или прогон страницы) isupper(с) Возвращает значение true, если с — символ в верхнем регистре isxdigit(с) Возвращает значение true, если с — шестнадцатеричная цифра tolower(с) Если с — прописная буква, возвращает ее эквивалент в нижнем регистре, в противном случае возвращает символ с неизменным toupper(с) Если с — строчная буква, возвращает ее эквивалент в верхнем регистре, в противном случае возвращает символ с неизменным Совет. Используйте версии С++ библиотечных заголовков языка С Кроме средств, определенных специально для языка С++, его библиотека содержит также библиотеку языка С. Имена заголовков языка С имеют формат имя .h. Версии этих же заголовков языка С++ имеют формат c имя , т.е. суффикс .h удален, а имени предшествует символ с, означающий, что этот заголовок принадлежит библиотеке С. Следовательно, у заголовка cctype то же содержимое, что и у заголовка ctype.h, но в форме, соответствующей программе С++. В частности, имена, определенные в заголовках с имя, Page 117/1103 определены также в пространстве имен std, тогда как имена, определенные в заголовках .h, — нет. Как правило, в программах на языке С++ используют заголовки версии c имя , а не имя .h. Таким образом, имена из стандартной библиотеки будут быстро найдены в пространстве имен std. Использование заголовка .h возлагает на программиста дополнительную заботу по отслеживанию, какие из библиотечных имен унаследованы от языка С, а какие принадлежат языку С++.Обработка каждого символа, использование серийного оператора for Если необходимо сделать нечто с каждым символом в строке, то наилучшим подходом является использование оператора, введенного новым стандартом, — серийный оператор for (range for). Этот оператор перебирает элементы данной ему последовательности и выполняет с каждым из них некую операцию. Его синтаксическая форма такова: for ( объявление : выражение ) оператор где выражение — это объект типа, который представляет последовательность, а объявление определяет переменную, которая будет использована для доступа к элементам последовательности. На каждой итерации переменная в объявлении инициализируется значением следующего элемента в выражении . Строка представляет собой последовательность символов, поэтому объект типа string можно использовать как выражение в серийном операторе for. Например, серийный оператор for можно использовать для вывода каждого символа строки в отдельной строке вывода. string str("some string"); // вывести символы строки str по одному на строку for (auto с : str) // для каждого символа в строке str cout << с << endl; // Page 118/1103 вывести текущий символ и символ новой строки Цикл for ассоциирует переменную с с переменной str типа string. Управляющая переменная цикла определяется тем же способом, что и любая другая переменная. В данном случае используется спецификатор auto (см. раздел 2.5.2), чтобы позволить компилятору самостоятельно определять тип переменной с, которым в данном случае будет тип char. На каждой итерации следующий символ строки str будет скопирован в переменную с. Таким образом, можно прочитать этот цикл так: "Для каждого символа с в строке str" сделать нечто. Под "нечто" в данном случае подразумевается вывод текущего символа, сопровождаемого символом новой строки. Рассмотрим более сложный пример и используем серийный оператор for, а также функцию ispunct() для подсчета количества знаков пунктуации в строке: string s("Hello World!!!"); // punct_cnt имеет тот же тип, что и у возвращаемого значения // функции s.size(); см. p. 2.5.3 decltype(s.size()) punct_cnt = 0; // подсчитать количество знаков пунктуации в строке s for (auto с : s) // для каждого символа в строке s if (ispunct(c)) // если символ знак пунктуации ++punct_cnt; // увеличить счетчик пунктуаций cout << punct_cnt << " punctuation characters in " << s << endl; Вывод этой программы таков: 3 punctuation characters in Hello World!!! Здесь для объявления счетчика punct_cnt используется спецификатор decltype (см. раздел 2.5.3). Его тип совпадает с типом возвращаемого значения функции s.size(), которым является тип string::size_type. Для обработки каждого символа в строке используем серийный оператор for. На сей раз проверяется, является ли каждый символ знаком пунктуации. Если да, то используем оператор инкремента (см. раздел 1.4.1) для добавления единицы к счетчику. Когда серийный оператор for завершает работу, отображается результат. Использование серийного оператора for для изменения символов в строке Если необходимо изменить значение символов в строке, переменную цикла следует определить как ссылочный тип (см. раздел 2.3.1). Помните, что ссылка — это только другое Page 119/1103 имя для данного объекта. При использовании ссылки в качестве управляющей переменной она будет по очереди связана с каждым элементом последовательности. Используя ссылку, можно изменить символ, с которым она связана. Предположим, что вместо подсчета знаков пунктуации необходимо преобразовать все буквы строки в верхний регистр. Для этого можно использовать библиотечную функцию toupper(), которая возвращает полученный символ в верхнем регистре. Для преобразования всей строки необходимо вызвать функцию toupper() для каждого символа и записать результат в тот же символ: string s("Hello World!!!"); // преобразовать s в верхний регистр for (auto &с : s) // для каждого символа в строке s // (примечание: с - ссылка) с = toupper(с); // с - ссылка, поэтому присвоение изменяет // символ в строке s cout << s << endl; Вывод этого кода таков: HELLO WORLD!!! На каждой итерации переменная с ссылается на следующий символ строки s. При присвоении значения переменной с изменяется соответствующий символ в строке s. с = toupper(с); // с - ссылка, поэтому присвоение изменяет // символ в строке s Таким образом, данное выражение изменяет значение символа, с которым связана переменная с. По завершении этого цикла все символы в строке str будут в верхнем регистре. Обработка лишь некоторых символов Серийный оператор for работает хорошо, когда необходимо обработать каждый символ. Но иногда необходим доступ только к одному символу или к некоторому количеству символов на основании некоего условия. Например, можно преобразовать в верхний регистр только первый символ строки или только первое слово в строке. Page 120/1103 Существуют два способа доступа к отдельным символам в строке: можно использовать индексирование или итератор. Более подробная информация об итераторах приведена в разделе 3.4 и в главе 9. Оператор индексирования ( оператор []) получает значение типа string::size_type (раздел 3.2.2), обозначающее позицию символа, к которому необходим доступ. Оператор возвращает ссылку на символ в указанной позиции. Индексация строк начинается с нуля; если строка s содержит по крайней мере два символа, то первым будет символ s[0], вторым — s[1], а последним символом является s[s.size() - 1]. Значения, используемые для индексирования строк, не должны быть отрицательными и не должны превосходить размер строки (>= 0 и < size()). Результат использования индекса вне этого диапазона непредсказуем. Непредсказуема также индексация пустой строки. Значение оператора индексирования называется индексом (index). Индекс может быть любым выражением, возвращающим целочисленное значение. Если у индекса будет знаковый тип, то его значение преобразуется в беззнаковый тип size_type (см. раздел 2.1.2). Следующий пример использует оператор индексирования для вывода первого символа строки: if (!s.empty()) // удостоверившись, что символ для вывода есть, cout << s[0] << endl; // вывести первый символ строки s Прежде чем обратиться к символу, удостоверимся, что строка s не пуста. При каждом использовании индексирования следует проверять наличие значения в данной области. Если строка s пуста, то значение s[0] неопределенно. Если строка не константа (см. раздел 2.4), возвращенному оператором индексирования символу можно присвоить новое значение. Например, первый символ можно перевести в верхний регистр следующим образом: string s("some string"); if (!s.empty()) // удостовериться в наличии символа s[0] s[0] = toupper(s[0]); // присвоить новое значение первому символу Вывод этой программы приведен ниже. Some string Использование индексирования для перебора В следующем примере переведем в верхний регистр первое слово строки s: Page 121/1103 // обрабатывать символы строки s, пока они не исчерпаются или // не встретится пробел for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) s[index] = toupper(s[index]); // преобразовать в верхний регистр Вывод этой программы таков: SOME string Цикл for (см. раздел 1.4.2) использует переменную index для индексирования строки s. Для присвоения переменной index соответствующего типа используется спецификатор decltype. Переменную index инициализируем значением 0, чтобы первая итерация началась с первого символа строки s. На каждой итерации значение переменной index увеличивается, чтобы получить следующий символ строки s. В теле цикла текущий символ переводится в верхний регистр. В условии цикла for используется новая часть — оператор логического AND ( оператор &&). Этот оператор возвращает значение true, если оба операнда истинны, и значение false в противном случае. Важно то, что этот оператор гарантирует обработку своего правого операнда, только если левый операнд истинен. В данном случае это гарантирует, что индексирования строки s не будет, если переменная index находится вне диапазона. Таким образом, часть s[index] выполняется, только если переменная index не равна s.size(). Поскольку инкремент переменной index никогда не превзойдет значения s.size(), переменная index всегда будет меньше s.size().Внимание! Индексирование не контролируется При использовании индексирования следует самому позаботиться о том, чтобы индекс оставался в допустимом диапазоне. Индекс должен быть >= 0 и < size() строки. Для упрощения кода, использующего индексирование, в качестве индекса всегда следует использовать переменную типа string::size_type. Поскольку это беззнаковый тип, индекс не может быть меньше нуля. При использовании значения типа size_type в качестве индекса достаточно проверять только то, что значение индекса меньше значения, возвращаемого функцией size(). Библиотека не обязана проверять и не проверяет значение индекса. Результат использования индекса вне диапазона непредсказуем. Использование индексирования для произвольного доступа В предыдущем примере преобразования регистра символов последовательности индекс перемещался на одну позицию за раз. Но можно также вычислить индекс и непосредственно обратиться к выбранному символу. Нет никакой необходимости получать доступ к символам Page 122/1103 последовательно. Предположим, например, что имеется число от 0 до 15, которое необходимо представить в шестнадцатеричном виде. Для этого можно использовать строку, инициализированную шестнадцатью шестнадцатеричными цифрами. const string hexdigits = "0123456789ABCDEF"; // возможные // шестнадцатеричные цифры cout << "Enter a series of numbers between 0 and 15" << " separated by spaces. Hit ENTER when finished: " << endl; string result; // будет содержать результирующую // шестнадцатеричную строку string::size_type n; // содержит введенное число while (cin >> n) if (n < hexdigits.size()) // игнорировать недопустимый ввод result += hexdigits[n]; // выбрать указанную // шестнадцатеричную цифру cout << "Your hex number is: " << result << endl; Если ввести следующие числа: 12 0 5 15 8 15 то результат будет таким: Your hex number is: C05F8F Программа начинается с инициализации строки hexdigits, содержащей шестнадцатеричные цифры от 0 до F. Сделаем эту строку константной (см. раздел 2.4), поскольку содержащиеся в ней значения не должны изменяться. Для индексирования строки hexdigits используем в Page 123/1103 цикле введенное значение n. Значением hexdigits[n] является символ, расположенный в позиции n строки hexdigits. Например, если n равно 15, то результат — F; если 12, то результат — С и т.д. Полученная цифра добавляется к переменной result, которая и выводится, когда весь ввод прочитан. Всякий раз, когда используется индексирование, следует позаботиться о том, чтобы индекс оставался в диапазоне. В этой программе индекс, n, имеет тип string::size_type, который, как известно, является беззнаковым. В результате значение переменной n гарантированно будет больше или равно 0. Прежде чем использовать переменную n для индексирования строки hexdigits, удостоверимся, что ее значение меньше, чем hexdigits.size(). Упражнения раздела 3.2.3 Упражнение 3.6. Используйте серийный оператор for для замены всех символов строки на X. Упражнение 3.7. Что будет, если определить управляющую переменную цикла в предыдущем упражнении как имеющую тип char? Предскажите результат, а затем измените программу так, чтобы использовался тип char, и убедитесь в своей правоте. Упражнение 3.8. Перепишите программу первого упражнения, сначала используя оператор while, а затем традиционный цикл for. Какой из этих трех подходов вы предпочтете и почему? Упражнение 3.9. Что делает следующая программа? Действительно ли она корректна? Если нет, то почему? string s; cout << s[0] << endl; Упражнение 3.10. Напишите программу, которая читает строку символов, включающую знаки пунктуации, и выведите ее, но уже без знаков пунктуации. Упражнение 3.11. Допустим ли следующий серийный оператор for? Если да, то каков тип переменной с? const string s = "Keep out!"; for (auto &c : s) {/*...*/} 3.3. Библиотечный тип vector Вектор (vector) — это коллекция объектов одинакового типа, каждому из которых присвоен целочисленный индекс, предоставляющий доступ к этому объекту. Вектор — это контейнер (container), поскольку он "содержит" другие объекты. Более подробная информация о контейнерах приведена в части II. Чтобы использовать вектор, необходимо включить соответствующий заголовок. В примерах подразумевается также, что включено соответствующее объявление using. #include <vector> using std::vector; Тип vector — это Page 124/1103 шаблон класса (class template). Язык С++ поддерживают шаблоны и классов, и функций. Написание шаблона требует довольно глубокого понимания языка С++. До главы 16 мы даже не будем рассматривать создание собственных шаблонов! К счастью, чтобы использовать шаблоны, вовсе не обязательно уметь их создавать. Шаблоны сами по себе не являются ни функциями, ни классами. Их можно считать инструкцией для компилятора по созданию классов или функций. Процесс, используемый компилятором для создания классов или функций по шаблону, называется созданием экземпляра (instantiation) шаблона. При использовании шаблона необходимо указать, экземпляр какого класса или функции должен создать компилятор. Для создания экземпляра шаблона класса следует указать дополнительную информацию, характер которой зависит от шаблона. Эта информация всегда задается одинаково: в угловых скобках после имени шаблона. В случае вектора предоставляемой дополнительной информацией является тип объектов, которые он должен содержать: vector<int> ivec; // ivec содержит объекты типа int vector<Sales_item> Sales_vec; // содержит объекты класса Sales_item vector<vector<string>> file; // вектор, содержащий другие векторы В этом примере компилятор создает три разных экземпляра шаблона vector: vector<int>, vector<Sales_item> и vector<vector<string>>. vector — это шаблон, а не класс. Классам, созданным по шаблону vector, следует указать тип хранимого элемента, например vector<int>. Можно определить векторы для содержания объектов практически любого типа. Поскольку ссылки не объекты (см. раздел 2.3.1), не может быть вектора ссылок. Однако векторы большинства других (не ссылочных) встроенных типов и типов классов вполне могут существовать. В частности, может быть вектор, элементами которого являются другие векторы. Следует заметить, что прежние версии языка С++ использовали несколько иной синтаксис определения вектора, элементы которого сами являлись экземплярами шаблона vector (или другого типа шаблона). Прежде необходимо было ставить пробел между закрывающей угловой скобкой внешней части vector и типом его элемента: т.е. vector<vector<int> >, а не vector<vector<int>>. Некоторые компиляторы могут потребовать объявления вектора векторов в старом стиле, например vector<vector<int> >. 3.3.1. Определение и инициализация векторов Page 125/1103 Подобно любому типу класса, шаблон vector контролирует способ определения и инициализации векторов. Наиболее распространенные способы определения векторов приведены в табл. 3.4. Инициализация вектора по умолчанию (см. раздел 2.2.1) позволяет создать пустой вектор определенного типа: vector<string> svec; // инициализация по умолчанию; // у svec нет элементов Могло бы показаться, что пустой вектор бесполезен. Однако, как будет продемонстрировано вскоре, элементы в вектор можно без проблем добавлять и во время выполнения. В действительности наиболее распространенный способ использования векторов подразумевает определение первоначально пустого вектора, в который элементы добавляются по мере необходимости во время выполнения. Таблица 3.4. Способы инициализации векторов vector<T> v1 Вектор, содержащий объекты типа T. Стандартный конструктор v1 пуст vector<T> v2(v1) Вектор v2 — копия всех элементов вектора v1 vector<T> v2 = v1 Эквивалент v2(v1), v2 — копия элементов вектора v1 vector<T> v3(n, val) Вектор v3 содержит n элементов со значением val vector<T> v4(n) Вектор v4 содержит n экземпляров объекта типа T, инициализированного значением по умолчанию vector<T> v5{a,b,с ...} Вектор v5 содержит столько элементов, сколько предоставлено инициализаторов; элементы инициализируются соответствующими инициализаторами vector<T> v5 = {a,b,с ... } Эквивалент v5{a,b,c ... } При определении вектора для его элементов можно также предоставить исходное значение (или значения). Например, можно скопировать элементы из другого вектора. При копировании векторов каждый элемент нового вектора будет копией соответствующего элемента исходного. Оба вектора должны иметь тот же тип: vector<int> ivec; // первоначально пустой // присвоить ivec несколько значений vector<int> ivec2(ivec); // копировать элементы ivec в ivec2 vector<int> ivec3 = ivec; // копировать элементы ivec в ivec3 vector<string> svec(ivec2); // svec содержит строки, // Page 126/1103 а не целые числа Списочная инициализация вектора Согласно новому стандарту, еще одним способом предоставления значений элементам вектора является списочная инициализация (см. раздел 2.2.1), т.е. заключенный в фигурные скобки список любого количества начальных значений элементов: vector<string> articles = {"a", "an", "the"}; В результате у вектора будет три элемента: первый со значением "а", второй — "an", последний — "the". Как уже упоминалось, язык С++ предоставляет несколько форм инициализации (см. раздел 2.2.1). Во многих, но не во всех случаях эти формы инициализации можно использовать взаимозаменяемо. На настоящий момент приводились примеры двух форм инициализации: инициализация копией (с использованием знака =) (см. раздел 3.2.1), когда предоставляется только один инициализатор; и внутриклассовая инициализация (см. раздел 2.6.1). Третий способ подразумевает предоставление списка значений элементов, заключенных в фигурные скобки (списочная инициализация). Нельзя предоставить список инициализаторов, используя круглые скобки. vector<string> v1{"a", "an", "the"}; // списочная инициализация vector<string> v2("a", "an", "the"); // ошибка Создание определенного количества элементов Вектор можно также инициализировать набором из определенного количества элементов, обладающих указанным значением. Счетчик задает количество элементов, а за ним следует исходное значение для каждого из этих элементов. vector<int> ivec(10, -1); // десять элементов типа int, каждый из // которых инициализирован значением -1 vector<string> svec(10, "hi!"); // десять строк, инициализированных // значением "hi!" Инициализация значения Иногда инициализирующее значение можно пропустить и указать только размер. В этом случае произойдет инициализация значения (value initialization), т.е. библиотека создаст инициализатор элемента сама. Это созданное библиотекой значение используется для инициализации каждого элемента в контейнере. Значение инициализатора элемента вектора зависит от типа его элементов. Если вектор хранит элементы встроенного типа, такие как int, то инициализатором элемента будет значение 0. Если элементы имеют тип класса, такой как string, то инициализатором Page 127/1103 элемента будет его значение по умолчанию. vector<int> ivec(10); // десять элементов, инициализированных // значением 0 vector<string> svec(10); // десять элементов, инициализированных // пустой строкой Эта форма инициализации имеет два ограничения. Первое — некоторые классы всегда требуют явного предоставления инициализатора (см. раздел 2.2.1). Если вектор содержит объекты, тип которых не имеет значения по умолчанию, то начальное значение элемента следует предоставить самому; невозможно создать векторы таких типов, предоставив только размер. Второе ограничение заключается в том, что при предоставлении количества элементов без исходного значения необходимо использовать прямую инициализацию (direct initialization): vector<int> vi = 10; // ошибка: необходима прямая инициализация Здесь число 10 используется для указания на то, как создать вектор, — необходимо, чтобы он обладал десятью элементами с инициализированными значениями. Число 10 не "копируется" в вектор. Следовательно, нельзя использовать форму инициализации копией. Более подробная информация об этом ограничении приведена в разделе 7.5.4. Списочный инициализатор или количество элементов В некоторых случаях смысл инициализации зависит от того, используются ли при передаче инициализаторов фигурные скобки или круглые. Например, при инициализации вектора vector<int> одиночным целочисленным значением это значение могло бы означать либо размер вектора, либо значение элемента. Точно так же, если предоставить два целочисленных значения, то они могли бы быть размером и исходным значением или значениями для двух элементов вектора. Для определения предназначения используются фигурные или круглые скобки. vector<int> v1(10); // v1 имеет десять элементов со значением 0 vector<int> v2{10}; // v2 имеет один элемент со значением 10 vector<int> v3(10, 1); // v3 имеет десять элементов со значением 1 Page 128/1103 vector<int> v4{10, 1}; // v4 имеет два элемента со значениями 10 и 1 Круглые скобки позволяют сообщить, что предоставленные значения должны использоваться для создания объекта. Таким образом, векторы v1 и v3 используют свои инициализаторы для определения размера вектора, а также размера и значения его элементов соответственно. Использование фигурных скобок, {...}, означает попытку списочной инициализации. Таким образом, если класс способен использовать значения в фигурных скобках как список инициализаторов элементов, то он так и сделает. Если это невозможно, то следует рассмотреть другие способы инициализации объектов. Значения, предоставленные при инициализации векторов v2 и v4, рассматриваются как значения элементов. Это списочная инициализация объектов; у полученных векторов будет один и два элемента соответственно. С другой стороны, если используются фигурные скобки и нет никакой возможности использовать инициализаторы для списочной инициализации объектов, то эти значения будут использоваться для создания объектов. Например, для списочной инициализации вектора строк следует предоставлять значения, которые можно рассматривать как строки. В данном случае нет никаких сомнений, осуществляется ли списочная инициализация элементов или создание вектора указанного размера. vector<string> v5{"hi"}; // списочная инициализация: v5 имеет // один элемент vector<string> v6("hi"); // ошибка: нельзя создать вектор из // строкового литерала vector<string> v7{10}; // v7 имеет десять элементов, инициализированных // значением по умолчанию vector<string> v8{10, "hi"}; // v8 имеет десять элементов со // значением "hi" Хотя фигурные скобки использованы во всех этих определениях, кроме одного, только вектор v5 имеет списочную инициализацию. Для списочной инициализации вектора значения в фигурных скобках должны соответствовать типу элемента. Нельзя использовать объект типа int для инициализации строки, поэтому инициализаторы векторов v1 и v8 не могут быть Page 129/1103 |