Теперь мы должны выбрать дату, заданную по умолчанию. Для этого вполне подходит первый день XXI столетия.
Date::Date
:y(2001), m(Date::jan), d(1)
{
}
Если не хотите встраивать значение, заданное по умолчанию, в код конструктора, то можете использовать константу (или переменную). Для того чтобы избежать использования глобальных переменных и связанных с ними проблем инициализации, можно использовать прием, описанный в разделе 8.6.2.
const Date& default_date
{
static Date dd(2001,Date::jan,1);
return dd;
}
Здесь
использовано ключевое слово
static
, чтобы переменная
dd
создавалась только один раз, а не каждый раз при очередном вызове функции
default_date
. Инициализация этой переменной происходит при первом вызове функции
default_date
. С помощью функции
default_date
легко определить конструктор, заданный по умолчанию, для класса
Date
.
Date::Date
:y(default_date.year),
m(default_date.month),
d(default_date.day)
}
Обратите внимание на то, что конструктор по умолчанию не обязан проверять значение, заданное по умолчанию; конструктор, создавший объект, вызвавший функцию
default_date
, уже сделал это. Имея конструктор для класса
Date
по умолчанию, мы можем создать векторы объектов класса
Date
.
vector<Date> birthdays(10);
Без конструктора по умолчанию мы были бы вынуждены сделать это явно.
vector<Date> birthdays(10,default_date);
9.7.4. Константные функции-члены
Некоторые переменные должны изменяться, потому они так и называются, а некоторые — нет; иначе говоря, существуют переменные, которые не изменяются. Обычно их называют константами, и для них используется ключевое слово
int b = start_of_term.day; // должно бы правильно (почему ?)
d.add_day(3); // отлично
start_of_term.add_day(3); // ошибка
}
Здесь подразумевается, что переменная
d
будет изменяться, а переменная
start_of_term
— нет; другими словами, функция
some_function
не может изменить переменную
start_of_term
. Откуда компилятору это известно? Дело в том, что мы сообщили ему об этом, объявив переменную
start_of_term
константой (
const
). Однако почему же с помощью функции
day
можно прочитать переменную
day
из объекта
start_of_term
? В соответствии с предыдущим определением класса
Date
функция
start_of_term.day
считается ошибкой, поскольку компилятор не знает, что функция
day
не изменяет свой объект класса
Date
. Об этом в программе нигде не сказано, поэтому компилятор предполагает, что функция
day
может модифицировать свой объект класса
Date
, и выдаст сообщение об ошибке.
Решить эту проблему можно, разделив операции над классом, на модифицирующие и немодифицирующие. Это не только помогает понять суть класса, но и имеет
очень важное практическое значение: операции, которые не модифицируют объект, можно применять к константным объектам. Рассмотрим пример.
class Date {
public:
// ...
int day const; // константный член: не может изменять
// объект
Month month const; // константный член: не может изменять
// объект
int year const; // константный член: не может изменять
// объект
void add_day(int n); // неконстантный член: может изменять
// объект
void add_month(int n); // неконстантный член: может изменять
// объект
void add_year(int n); // неконстантный член: может изменять
// объект
private:
int y; // год
Month m;
int d; // день месяца
};
Date d(2000, Date::jan, 20);
const Date cd(2001, Date::feb, 21);
cout << d.day << " — " << cd.day << endl; // OK
d.add_day(1); // OK
cd.add_day(1); // ошибка: cd — константа
Ключевое слово
const
в объявлении функции-члена стоит сразу после списка аргументов, чтобы обозначить, что эту функцию-член можно вызывать для константных объектов. Как только мы объявили функцию-член константной, компилятор берет с нас обещание не модифицировать объект. Рассмотрим пример.
int Date::day const
{
++d; // ошибка: попытка изменить объект в константной
// функции - члене
return d;
}
Естественно, как правило, мы не собираемся мошенничать. В основном компилятор обеспечивает защиту от несчастных случаев, что очень полезно при разработке сложных программ.
9.7.5. Члены и вспомогательные функции
Разрабатывая минимальный (хотя и полный) интерфейс, мы вынуждены оставлять за бортом много полезных операций. Функцию, которая могла бы быть просто, элегантно и эффективно реализована как самостоятельная функция (т.е. не функция-член), следует реализовать за пределами класса. Таким образом, функция не сможет повредить данные, хранящиеся в объекте класса. Предотвращение доступа к данным является важным фактором, поскольку обычные методы поиска ошибок “вращаются вокруг типичных подозрительных мест”; иначе говоря, если с классом что-то не так, мы в первую очередь проверяем функции, имеющие прямой доступ к его представлению: одна из них обязательно является причиной ошибки. Если таких функций десяток, нам будет намного проще работать, чем если их будет пятьдесят.
Пятьдесят функций для класса
Date
! Возможно, вы думаете, что мы шутим. Вовсе нет: несколько лет назад я делал обзор нескольких коммерческих библиотек для работы с календарем и обнаружил в них множество функций вроде
next_Sunday
,
next_workday
и т.д. Пятьдесят — это совсем не невероятное число для класса, разработанного для удобства пользователей, а не для удобства его проектирования, реализации и сопровождения.
Отметим также, что если представление изменяется, то переписать достаточно только функции, которые имеют к ней прямой доступ. Это вторая важная практическая причина для минимизации интерфейса. Разрабатывая класс
Date
, мы могли решить, что дату лучше представлять в виде целого числа дней, прошедших с 1 января 1900 года, а не в виде тройки (год, месяц, день). В этом случае нам придется изменить только функции-члены.