int y = x.mf; // ошибка: переменная mf является закрытой
// (т.е. недоступной)
Пользователь не может непосредственно ссылаться на закрытый член класса. Вместо этого он должен обратиться к открытой функции-члену, имеющей доступ к закрытым данным. Например:
class X {
int m;
int mf(int);
public:
int f(int i) { m=i; return mf(i); }
};
X x;
int y = x.f(2);
Различие
между закрытыми и открытыми данными отражает важное различие между интерфейсом (точка зрения пользователя класса) и деталями реализации (точка зрения разработчика класса). По мере изложения мы опишем эту концепцию более подробно и рассмотрим множество примеров. А пока просто укажем, что для обычных структур данных это различие не имеет значения. По этой причине для простоты будем рассматривать класс, не имеющий закрытых деталей реализации, т.е. структуру, в которой все члены по умолчанию являются открытыми. Рассмотрим пример.
struct X {
int m;
// ...
};
Он эквивалентен следующему коду:
class X {
public:
int m;
// ...
};
Структуры (
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);
}
Во-первых, отметим полезность таких “операций” — здесь они реализованы в виде вспомогательных функций. Проверка корректности даты довольно сложна и утомительна, поэтому, если бы мы не написали соответствующую функцию раз и навсегда, то скорее всего пропустили бы этот код и получили неправильную программу. Если мы определяем тип, то всегда хотим выполнять над его объектами какие-то операции. Точное количество и вид этих операций может изменяться. Точный вид реализации этих операций (в виде функций, функций-членов или операторов) также изменяется, но как только мы решили создать собственный тип, мы должны спросить себя: “Какие операции с этим типом можно выполнять?”