Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

int d;

};

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

Некоторые переменные должны изменяться, потому они так и называются, а некоторые — нет; иначе говоря, существуют переменные, которые не изменяются. Обычно их называют константами, и для них используется ключевое слово

const
. Рассмотрим пример.

void some_function(Date& d, const Date& start_of_term)

{

int a = d.day; // OK

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 года, а не в виде тройки (год, месяц, день). В этом случае нам придется изменить только функции-члены.

Поделиться с друзьями: