Язык программирования C++.

2.6. Определение собственных структур данных. Глава 2. Переменные и базовые типы. Часть I. Основы. Язык программирования C++.2.6. Определение собственных структур данных.

На самом простом уровне структура данных (data structure) — это способ группировки взаимосвязанных данных и стратегии их использования. Например, класс Sales_item группирует ISBN книги, количество проданных экземпляров и выручку от этой продажи. Он предоставляет также набор операций, таких как функция isbn() и операторы >>, <<, + и +=.

В языке С++ мы создаем собственные типы данных, определяя класс. Такие библиотечные типы, как string, istream и ostream, определены как классы, подобно типу Sales_item в главе 1. Поддержка классов в языке С++ весьма обширна, фактически части III и IV в значительной степени посвящены описанию средств, связанных с классами. Хотя класс Sales_item довольно прост, мы не сможем определить его полностью, пока не узнаем в главе 14, как писать собственные операторы.

2.6.1 Определение типа Sales_data. 2.6. Определение собственных структур данных. Глава 2. Переменные и базовые типы. Часть I. Основы. Язык программирования C++.2.6.1 Определение типа Sales_data.

Несмотря на то что мы еще не можем написать свой класс Sales_item полностью, уже вполне можно создать достаточно реалистичный класс, группирующий необходимые элементы данных. Стратегия использования этого класса заключается в том, что пользователи будут получать доступ непосредственно к элементам данных и смогут самостоятельно реализовать необходимые операции.

Поскольку создаваемая структура данных не поддерживает операций, назовем новую версию Sales_data, чтобы отличать ее от типа Sales_item. Определим класс следующим образом:

struct Sales_data {

 std::string bookNo;

 unsigned units_sold = 0;

 double revenue = 0.0;

};

Класс начинается с ключевого слова struct, сопровождаемого именем класса и (возможно пустым) телом класса. Тело класса заключено в фигурные скобки и формирует новую область видимости (см. раздел 2.2.4). Определенные в классе имена должны быть уникальны в пределах класса, но вне класса они могут повторяться.

Ближайшие фигурные скобки, заключающие тело класса, следует сопроводить точкой с запятой. Точка с запятой необходима, так как после тела класса можно определить переменные:

struct Sales_data { /* ... */ } accum, trans, *salesptr;

// эквивалентно, но лучше определять эти объекты так

struct Sales_data { /* ... */ };

Sales data accum, trans, *salesptr;

Точка с запятой отмечает конец (обычно пустого) списка объявления операторов. Обычно определение объекта в составе определения класса — это не лучшая идея. Объединение в одном операторе определений двух разных сущностей (класса и переменной) ухудшает читабельность кода.

Язык программирования C++.Забытая точка с запятой в конце определения класса — довольно распространенная ошибка начинающих программистов.

Переменные-члены класса.

В теле класса определены члены (member) класса. У нашего класса есть только переменные-члены (data member). Переменные-члены класса определяют содержимое объектов этого класса. Каждый объект обладает собственным экземпляром переменных-членов класса. Изменение переменных-членов одного объекта не изменяет данные в любом другом объекте класса Sales_data.

Переменные-члены определяют точно так же, как и обычные переменные: указывается базовый тип, затем список из одного или нескольких операторов объявления. У нашего класса будут три переменные-члены: член типа string по имени bookNo, член типа unsigned по имени units_sold и член типа double по имени revenue. Эти три переменные-члены будут у каждого объекта класса Sales_data.

Язык программирования C++.По новому стандарту переменной-члену можно предоставить внутриклассовый инициализатор (in-class initializer). Он используется для инициализации переменных-членов при создании объектов. Члены без инициализатора инициализируются по умолчанию (см. раздел 2.2.1). Таким образом, при определении объектов класса Sales_data переменные-члены units_sold и revenue будут инициализированы значением 0, а переменная-член bookNo — пустой строкой.

Внутриклассовые инициализаторы ограничены формой их использования (см. раздел 2.2.1): они должны либо быть заключены в фигурные скобки, либо следовать за знаком =. Нельзя определить внутриклассовый инициализатор в круглых скобках.

В разделе 7.2 указано, что язык С++ обладает еще одним ключевым словом, class, также используемым для определения собственной структуры данных. В этом разделе используем ключевое слово struct, поскольку пока еще не рассмотрены приведенные в главе 7 дополнительные средства, связанные с классом.

Упражнения раздела 2.6.1.

Упражнение 2.39. Откомпилируйте следующую программу и посмотрите, что будет, если не поставить точку с запятой после определения класса. Запомните полученное сообщение, чтобы узнать его в будущем.

struct Foo { /* пусто */ } // Примечание: нет точки с запятой

int main() {

 return 0;

}

Упражнение 2.40. Напишите собственную версию класса Sales_data.

2.6.2. Использование класса Sales_data. 2.6. Определение собственных структур данных. Глава 2. Переменные и базовые типы. Часть I. Основы. Язык программирования C++.2.6.2. Использование класса Sales_data.

В отличие от класса Sales_item, класс Sales_data не поддерживает операций. Пользователи класса Sales_data должны сами писать все операции, в которых они нуждаются. В качестве примера напишем новую версию программы из раздела 1.5.2, которая выводила сумму двух транзакций. Программа будет получать на входе такие транзакции:

0-201-78345-X 3 20.00

0-201-78345-X 2 25.00

Каждая транзакция содержит ISBN, количество проданных книг и цену, по которой была продана каждая книга.

Суммирование двух объектов класса Sales_data.

Поскольку класс Sales_data не предоставляет операций, придется написать собственный код, осуществляющий ввод, вывод и сложение. Будем подразумевать, что класс Sales_data определен в заголовке Sales_data.h. Определение заголовка рассмотрим в разделе 2.6.3.

Так как эта программа будет длиннее любой, написанной до сих пор, рассмотрим ее по частям. В целом у программы будет следующая структура:

#include <iostream>

#include <string>

#include "Sales_data.h"

int main() {

 Sales_data data1, data2;

 // код чтения данных в data1 и data2

 // код проверки наличия у data1 и data2 одинакового ISBN

 // если это так, то вывести сумму data1 и data2

}

Как и первоначальная программа, эта начинается с включения заголовков, необходимых для определения переменных, содержащих ввод. Обратите внимание, что, в отличие от версии Sales_item, новая программа включает заголовок string. Он необходим потому, что код должен манипулировать переменной-членом bookNo типа string.

Чтение данных в объект класса Sales_data.

Хотя до глав 3 и 10 мы не будем описывать библиотечный тип string подробно, упомянем пока лишь то, что необходимо знать для определения и использования члена класса, содержащего ISBN. Тип string содержит последовательность символов. Он имеет операторы >>, << и == для чтения, записи и сравнения строк соответственно. Этих знаний достаточно для написания кода чтения первой транзакции.

double price = 0; // цена за книгу, используемая для вычисления

                  // общей выручки

// читать первую транзакцию:

// ISBN, количество проданных книг, цена книги

std::cin >> data1.bookNo >> data1.units_sold >> price;

// вычислить общий доход из price и units_sold

data1.revenue = data1.units_sold * price;

Транзакции содержат цену, по которой была продана каждая книга, но структура данных хранит общий доход. Данные транзакции будем читать в переменную 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

 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. Создание собственных файлов заголовка. 2.6. Определение собственных структур данных. Глава 2. Переменные и базовые типы. Часть I. Основы. Язык программирования C++.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. Поскольку заголовок мог бы быть включен несколько раз, код необходимо писать так, чтобы обезопасить от многократного включения.

Язык программирования C++.После внесения любых изменений в заголовок необходимо перекомпилировать все использующие его файлы исходного кода, чтобы вступили в силу новые или измененные объявления.

Краткое введение в препроцессор.

Наиболее распространенный способ обезопасить заголовок от многократного включения подразумевает использование препроцессора. Препроцессор (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 будут проигнорированы.

Язык программирования C++.Имена переменных препроцессора не подчиняются правилам областей видимости языка С++.

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

Язык программирования C++.У заголовков должна быть защита, даже если они не включаются в другие заголовки. Защита заголовка проста в написании, и при привычном их определении не нужно размышлять, нужны они или нет.

Упражнения раздела 2.6.3.

Упражнение 2.42. Напишите собственную версию заголовка Sales_data.h и используйте его для новой версии упражнения из раздела 2.6.2.