правило языка, но это хороший совет: общепринятое использование операторов, такое как символ + для сложения, значительно облегчает понимание программы. Помимо всего прочего, этот совет является результатом сотен лет опыта использования математических обозначений.
Малопонятные операторы и необычное использование операторов могут запутать программу и стать источником ошибок. Более на эту тему мы распространяться не будем. Просто в следующих главах применим перегрузку операторов в соответствующих местах.
Интересно, что чаще всего для перегрузки выбирают не операторы +, –, *, и /, как можно было бы предположить, а =, ==, !=, <, [] и ().
9.7. Интерфейсы классов
Ранее мы уже указывали, что открытый интерфейс и реализация класса должны быть отделены друг от друга. Поскольку в языке С++ остается возможность использовать простые структуры struct, некоторые профессионалы могут не согласиться с этим утверждением. Однако как разработать хороший интерфейс? Чем хороший интерфейс отличается от плохого? Частично на эти вопросы можно ответить только с помощью примеров, но существует несколько общих принципов, которые поддерживаются в языке С++.
• Интерфейс должен быть полным.
• Интерфейс должен быть минимальным.
• Класс должен иметь конструкторы.
• Класс доложен поддерживать копирование (или явно запрещать его) (см. раздел 14.2.4).
• Следует предусмотреть тщательную проверку типов аргументов.
• Необходимо идентифицировать немодифицирующие функции-члены (см. раздел 9.7.4).
• Деструктор должен освобождать все ресурсы (см. раздел 17.5). См. также раздел 5.5, в котором описано, как выявлять ошибки и сообщать о них на этапе выполнения программы.
Первые два принципа можно подытожить так: “Интерфейс должен быть как можно более маленьким, но не меньше необходимого”. Интерфейс должен быть маленьким, потому что его легче изучить и запомнить, а программист, занимающийся реализацией класса, не будет терять время на реализацию излишних или редко используемых функций. Кроме того, небольшой интерфейс означает, что если что-то пойдет не так, как задумано, для поиска причины потребуется проверить лишь несколько функций. В среднем чем больше открытых функций, тем труднее найти ошибку, — пожалуйста, не усложняйте себе жизнь, создавая классы с открытыми данными. Но, разумеется, интерфейс должен быть полным, в противном случае он будет бесполезным. Нам не нужен интерфейс, который не позволяет нам делать то, что действительно необходимо.
Перейдем к изучению менее абстрактных и более реалистичных понятий, поддерживаемых в языке С++.
9.7.1. Типы аргументов
Определяя конструктор класса Date в разделе 9.4.3, мы использовали в качестве аргументов три переменные типа int. Это породило несколько проблем.
Date d1(4,5,2005); // Ой! Год 4, день 2005
Date d2(2005,4,5); // 5 апреля или 4 мая?
Первая проблема (недопустимый день месяца) легко решается путем проверки в конструкторе. Однако вторую проблему (путаницу между месяцем и днем месяца) невозможно выявить с помощью кода, написанного пользователем. Она возникает из-за того, что существуют разные соглашения о записи дат; например, 4/5 в США означает 5 апреля, а в Англии — 4 мая. Поскольку эту проблему невозможно устранить с помощью вычислений, мы должны придумать что-то еще. Очевидно, следует использовать систему типов.
// простой класс Date (использует тип Month)
class Date {
public:
enum Month {
jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};
Date(int y, Month m, int d); // проверка даты и инициализация
// ...
private:
int y; // год
Month m;
int d; // день
};
Когда мы используем тип Month, компилятор выдаст ошибку, если мы поменяем местами месяц и день. Кроме того, перечисление Month позволяет использовать символические имена. Такие имена, как правило, легче читать и записывать, чем работать с числами, подвергаясь риску ошибиться.
Date dx1(1998, 4, 3); // ошибка: 2-й аргумент не имеет
// тип Month
Date dx2(1998, 4, Date::mar); // ошибка: 2-й аргумент не имеет
// тип Month
Date dx2(4, Date::mar, 1998); // Ой: ошибка на этапе выполнения:
// день 1998
Date dx2(Date::mar, 4, 1998); // ошибка: 2-й аргумент не имеет
// тип Month
Date dx3(1998, Date::mar, 30); // OK
Этот код решает много проблем. Обратите внимание на квалификатор Date перечисления mar: Date::mar. Тем самым мы указываем, что это перечисление mar из класса Date. Это не эквивалентно обозначению Date.mar, поскольку Date — это не объект, а тип, а mar — не член класса, а символическая константа из перечисления, объявленного в классе. Обозначение :: используется после имени класса (или пространства имен; см. раздел 8.7), а . (точка) — после имени объекта.
Когда есть выбор, ошибки следует выявлять на этапе компиляции, а не на этапе выполнения программы. Мы предпочитаем, чтобы ошибки вылавливал компилятор, а не искать, в каком месте кода возникла ошибка. Кроме того, для выявления ошибок на этапе компиляции не требуется писать и выполнять специальный код для проверки.
А нельзя ли подобным образом выявить путаницу между днем месяца и годом? Можно, но решение этой проблемы будет не таким элегантным, как для типа Month; помимо всего прочего, возможно, что мы имели в виду именно четвертый год. Даже если мы ограничимся современной эпохой, в перечисление придется включать слишком много лет.
Вероятно, было бы лучше всего (не вникая в предназначение класса Date) написать следующий код:
class Year { // год в диапазоне [min:max)
static const int min = 1800;
static const int max = 2200;
public:
class Invalid { };
Year(int x) : y(x) { if (x<min || max<=x) throw Invalid(); }
int year() { return y; }
private:
int y;
};
class Date {
public:
enum Month {
jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};
Date(Year y, Month m, int d); // проверка даты и инициализация
// ...
private:
Year y;
Month m;
int d; // день
};
Теперь получаем фрагмент кода.