чем определение Date::month(), размещенное за пределами объявления класса. Определения коротких и простых функций можно размещать в объявлении класса.
Обратите внимание на то, что функция month() может обращаться к переменной m, даже несмотря на то, что переменная m определена позже (ниже) функции month(). Член класса может ссылаться на другой член класса независимо от того, в каком месте класса он определен. Правило, утверждающее, что имя переменной должно быть объявлено до ее использования, внутри класса ослабляется.
Определение функции-члена в классе приводит к следующим последствиям.
• Функция становится подставляемой (inlined), т.е. компилятор попытается сгенерировать код подставляемой функции вместо ее вызова. Это может дать значительное преимущество часто вызываемым функциям, таким как month().
• При изменении тела подставляемой функции-члена класса придется скомпилировать заново все модули, в которых он используется. Если тело функции определено за пределами объявления класса, то потребуется перекомпилировать только само определение класса. Отсутствие необходимости повторного компилирования при изменении тела функции может оказаться огромным преимуществом в больших программах.
Очевидное правило гласит: не помещайте тела функций-членов в объявление класса, если вам не нужна повышенная эффективность программы за счет использования небольших подставляемых функций. Большие функции, скажем, состоящие из пяти и более строк, ничего не выиграют от подстановки. Не следует делать подставляемыми функции, содержащие более одного-двух выражений.
9.4.5. Ссылка на текущий объект
Рассмотрим простой пример использования класса Date.
class Date {
// ...
int month() { return m; }
// ...
private:
int y, m, d; // год, месяц, день
};
void f(Date d1, Date d2)
{
cout << d1.month() << ' ' << d2.month() << 'n';
}
Откуда функции Date::month() известно, что при первом вызове следует вернуть значение переменной d1.m, а при втором — d2.m? Посмотрите на функцию Date::month() еще раз; ее объявление не имеет аргумента! Как функция Date::month() “узнает”, для какого объекта она вызывается? Функции-члены класса, такие как Date::month(), имеют неявный аргумент, позволяющий идентифицировать объект, для которого они вызываются. Итак, при первом вызове переменная m правильно ссылается на d1.m, а при втором — на d2.m. Другие варианты использования неявного аргумента описаны в разделе 17.10.
9.4.6. Сообщения об ошибках
Что делать при обнаружении некорректной даты? В каком месте кода происходит поиск некорректных дат? В разделе 5.6 мы узнали, что в этом случае следует сгенерировать исключение, и самым очевидным местом для этого является место первого создания объекта класса Date. Если мы создали правильные объекты класса Date и все функции-члены написаны правильно, то мы никогда не получим объект класса Date с неверным значением. Итак, следует предотвратить создание неправильных объектов класса Date.
// простой класс Date (предотвращаем неверные даты)
class Date {
public:
class Invalid { }; // используется как исключение
Date(int y, int m, int d); // проверка и инициализация даты
// ...
private:
int y, m, d; // год, месяц, день
bool check(); // если дата правильная, возвращает true
};
Мы поместили проверку корректности даты в отдельную функцию check(), потому что с логической точки зрения эта проверка отличается от инициализации, а также потому, что нам может потребоваться несколько конструкторов. Легко видеть, что закрытыми могут быть не только данные, но и функции.
Date::Date(int yy, int mm, int dd)
:y(yy), m(mm), d(dd) // инициализация данных - членов класса
{
if (!check()) throw Invalid(); // проверка корректности
}
bool Date::check() // возвращает true, если дата корректна
{
if (m<1 || 12<m) return false;
// ...
}
Имея это определение класса Date, можно написать следующий код:
void f(int x, int y)
try {
Date dxy(2009,x,y);
cout << dxy << 'n'; // объявление оператора << см. в разделе 9.8
dxy.add_day(2);
}
catch(Date::Invalid) {
error("invalid date"); // функция error() определена
// в разделе 5.6.3
}
Теперь мы знаем, что оператор << и функция add_day() всегда будут работать с корректными объектами класса Date. До завершения разработки класса Date, описанной в разделе 9.7, опишем некоторые свойства языка, которые потребуются нам для того, чтобы сделать это хорошо: перечисления и перегрузку операторов.
9.5. Перечисления
Перечисление enum (enumeration) — это очень простой тип, определенный пользователем, который задает множество значений (элементов перечисления) как символические константы. Рассмотрим пример.
enum Month {
jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};
“Тело” перечисления — это просто список его элементов. Каждому элементу перечисления можно задать конкретное значение, как это сделано выше с элементом jan, или предоставить компилятору подобрать подходящее значение. Если положиться на компилятор, то он присвоит каждому элементу перечисления число, на единицу превышающее значение предыдущего. Таким образом, наше определение перечисления Month присваивает каждому месяцу последовательные значения, начиная с единицы. Это эквивалентно следующему коду:
enum Month {
jan=1, feb=2, mar=3, apr=4, may=5, jun=6,
jul=7, aug=8, sep=9, oct=10, nov=11, dec=12
};
Однако это утомительно и открывает много возможностей для ошибок. Фактически мы сделали две опечатки, пока не получили правильный вариант; лучше все же предоставить компилятору делать простую, повторяющуюся, “механическую” работу. Компилятор такие задачи решает лучше, чем люди, и при этом не устает.
Если не инициализировать первый элемент перечисления, то счетчик начнет отсчет с нуля. Рассмотрим такой пример:
enum Day {
monday, tuesday, wednesday, thursday, friday, saturday, sunday
};
где monday==0 и sunday==6. На практике лучше всего выбирать начальное значение счетчика, равным нулю.
Перечисление Month можно использовать следующим образом:
Month m = feb;
m = 7; // ошибка: нельзя присвоить целое число перечислению
int n = m; // OK: целочисленной переменной можно присвоить