Структуры (struct
) в основном используются для организации данных, члены которых могут принимать любые значения; иначе говоря, мы не можем определить для них никакого осмысленного инварианта (раздел 9.4.3).
9.4. Разработка класса
Проиллюстрируем языковые свойства, поддерживающие классы и основные методы их использования, на примере того, как — и почему — простую структуру данных можно преобразовать в класс с закрытыми деталями реализации и операциями.
Рассмотрим вполне тривиальную задачу: представить календарную дату (например, 14 августа 1954 года) в программе. Даты нужны во многих программах (для проведения коммерческих операций, описания погодных данных, календаря, рабочих записей, ведомостей и т.д.). Остается только вопрос: как это сделать?
9.4.1. Структуры и функции
Как можно представить дату? На этот вопрос большинство людей отвечают: “Указать год, месяц и день месяца”. Это не единственный и далеко не лучший ответ, но для наших целей он вполне подходит. Для начала попробуем создать простую структуру.
// простая структура Date (слишком просто?)
struct Date {
int y; // год
int m; // месяц года
int d; // день месяца
};
Date today; // переменная типа Date (именованный объект)
Объект типа Date
, например today
, может просто состоять из трех чисел типа int
.
В данном случае нет необходимости скрывать данные, на которых основана структура Date
, — это предположение будет использовано во всех вариантах этой структуры на протяжении всей главы. Итак, теперь у нас есть объекты типа Date;
что с ними можно делать? Все что угодно, в том смысле, что мы можем получить доступ ко всем членам объекта today
(и другим объектам типа Date
), а также читать и записывать их по своему усмотрению. Загвоздка заключается в том, что все это не совсем удобно. Все, что мы хотим делать с объектами типа Date
, можно выразить через чтение и запись их членов. Рассмотрим пример.
// установить текущую дату 24 декабря 2005 года
today.y = 2005;
today.m = 24;
today.d = 12;
Этот способ утомителен и уязвим для ошибок. Вы заметили ошибку? Все, что является утомительным, уязвимо для ошибок! Например, ответьте, имеет ли смысл следующий код?
Date x;
x.y = –3;
x.m = 13;
x.d = 32;
Вероятно нет, и никто не стал бы писать такую чушь — или стал? А что вы скажете о таком коде?
Date y;
y.y = 2000;
y.m = 2;
y.d = 29;
Был ли двухтысячный год високосным? Вы уверены?
Итак, нам нужны вспомогательные функции, которые выполняли бы для нас самые общие операции. В этом случае нам не придется повторять один и тот же код, а также находить и исправлять одни и те же ошибки снова и снова. Практически для любого типа самыми общими операциями являются инициализация и присваивание. Для типа Date
к общим операциям относится также увеличение значения объекта Date
. Итак, напишем следующий код:
// вспомогательные функции:
void init_day(Date& dd, int y, int m, int d)
{
// проверяет, является ли (y,m,d) правильной датой
// если да, то инициализирует объект dd
}
void add_day(Date& dd, int n)
{
// увеличивает объект dd на n дней
}
Попробуем использовать объект типа Date
.
void f()
{
Date today;
init_day(today, 12, 24, 2005); // Ой! (в 12-м году не было
// 2005-го дня)
add_day(today,1);
}
9.4.2. Функции-члены и конструкторы
Мы предусмотрели функцию инициализации для типа Date
, которая проверяет корректность его объектов. Однако функции проверки приносят мало пользы, если мы не можем их использовать. Например, допустим, что мы определили для типа Date
оператор вывода <<
(раздел 9.8):
void f()
{
Date today;
// ...