Язык программирования 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.
Несмотря на то что мы еще не можем написать свой класс 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;
Точка с запятой отмечает конец (обычно пустого) списка объявления операторов. Обычно определение объекта в составе определения класса — это не лучшая идея. Объединение в одном операторе определений двух разных сущностей (класса и переменной) ухудшает читабельность кода.
Забытая точка с запятой в конце определения класса — довольно распространенная ошибка начинающих программистов.
Переменные-члены класса.
В теле класса определены члены (member) класса. У нашего класса есть только переменные-члены (data member). Переменные-члены класса определяют содержимое объектов этого класса. Каждый объект обладает собственным экземпляром переменных-членов класса. Изменение переменных-членов одного объекта не изменяет данные в любом другом объекте класса Sales_data.
Переменные-члены определяют точно так же, как и обычные переменные: указывается базовый тип, затем список из одного или нескольких операторов объявления. У нашего класса будут три переменные-члены: член типа string по имени bookNo, член типа unsigned по имени units_sold и член типа double по имени revenue. Эти три переменные-члены будут у каждого объекта класса Sales_data.
По новому стандарту переменной-члену можно предоставить внутриклассовый инициализатор (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.
В отличие от класса 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. Создание собственных файлов заголовка.
Как будет продемонстрировано в разделе 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. Поскольку заголовок мог бы быть включен несколько раз, код необходимо писать так, чтобы обезопасить от многократного включения.
После внесения любых изменений в заголовок необходимо перекомпилировать все использующие его файлы исходного кода, чтобы вступили в силу новые или измененные объявления.
Краткое введение в препроцессор.
Наиболее распространенный способ обезопасить заголовок от многократного включения подразумевает использование препроцессора. Препроцессор (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 будут проигнорированы.
Имена переменных препроцессора не подчиняются правилам областей видимости языка С++.
Переменные препроцессора, включая имена для защиты заголовка, должны быть уникальными во всей программе. Обычно мы гарантируем уникальность имен защиты заголовка, включая в него имя класса. Чтобы избежать конфликта имен с другими сущностями программы, имена переменных препроцессора обычно пишут полностью в верхнем регистре.
У заголовков должна быть защита, даже если они не включаются в другие заголовки. Защита заголовка проста в написании, и при привычном их определении не нужно размышлять, нужны они или нет.
Упражнения раздела 2.6.3.
Упражнение 2.42. Напишите собственную версию заголовка Sales_data.h и используйте его для новой версии упражнения из раздела 2.6.2.
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.
- 392.
- 393.
- 394.
- 395.
- 396.
- 397.
- 398.
- 399.
- 400.
- 401.
- 402.
- 403.
- 404.
- 405.
- 406.
- 407.
- 408.
- 409.
- 410.
- 411.
- 412.
- 413.
- 414.
- 415.
- 416.
- 417.
- 418.
- 419.
- 420.
- 421.
- 422.
- 423.
- 424.
- 425.
- 426.
- 427.
- 428.
- 429.
- 430.
- 431.
- 432.