Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп
Шрифт:
Интервал:
Закладка:
void Shape::set_point(int i, Point p) // не используется
{
points[i] = p;
}
Point Shape::point(int i) const
{
return points[i];
}
int Shape::number_of_points() const
{
return points.size();
}
В производном классе эти функции используются так:
void Lines::draw_lines() const
// рисует линии, соединяющие пары точек
{
for (int i=1; i<number_of_points(); i+=2)
fl_line(point(i–1).x,point(i–1).y,point(i).x,point(i).y);
}
Все эти тривиальные функции доступа могут вызвать у вас обеспокоенность. Эффективны ли они? Не замедляют ли работу программы? Увеличивают ли они размер генерируемого кода? Нет, компилятор всех их делает подставляемыми. Вызов функции number_of_points() занимает столько же байтов памяти и выполняет точно столько же инструкций, сколько и непосредственный вызов функции points.size().
Решения, касающиеся управления доступом, очень важны. Теперь мы могли бы создать почти минимальную версию класса Shape.
struct Shape { // слишком простое определение — не используется
Shape();
void draw() const; // работает с цветом и вызывает функцию
// draw_lines
virtual void draw_lines() const; // рисует линии
virtual void move(int dx, int dy); // перемещает фигуры +=dx
// и +=dy
vector<Point> points; // не используется всеми фигурами
Color lcolor;
Line_style ls;
Color fcolor;
}
Какие возможности обеспечивают эти двенадцать дополнительных функций-членов и два канала доступа к спецификациям (private: и protected:)? Главный ответ состоит в том, что защита класса от нежелательного изменения позволяет разработчику создавать лучшие классы с меньшими усилиями. Этот же аргумент относится и к инвариантам (см. раздел 9.4.3). Подчеркнем эти преимущества на примере определения классов, производных от класса Shape. В более ранних вариантах класса Shape мы использовали следующие переменные:
Fl_Color lcolor;
int line_style;
Оказывается, это очень ограничивает наши возможности (стиль линии, задаваемый переменной типа int, не позволяет элегантно задавать ширину линии, а класс Fl_Color не предусматривает невидимые линии) и приводит к довольно запутанному коду. Если бы эти две переменные были открытыми и использовались в пользовательской программе, то мы могли бы улучшить интерфейсную библиотеку только за счет взлома этого кода (поскольку в нем упоминаются имена lcolor и line_style).
Кроме того, функции доступа часто обеспечивают удобство обозначений. Например, инструкция s.add(p) читается и записывается легче, чем s.points.push_back(p).
14.2.3. Рисование фигур
Мы описали почти все, кроме ядра класса Shape.
void draw() const; // работает с цветом и вызывает функцию
// draw_lines
virtual void draw_lines() const; // рисует линии
Основная задача класса Shape — рисовать фигуры. Мы не можем удалить из класса Shape все остальные функции и оставить его вообще без данных о нем самом, не нанеся вреда нашей основной концепции (см. раздел 14.4); рисование — это главная задача класса Shape. Он выполняет ее с помощью библиотеки FLTK и операционной системы, но с точки зрения пользователя он выполнят только две функции.
• Функция draw() интерпретирует стиль и цвет, а затем вызывает функцию draw_lines().
• Функция draw_lines() подсвечивает пиксели на экране.
Функция draw() не использует никаких новаторских методов. Она просто вызывает функции библиотеки FLTK, чтобы задать цвет и стиль фигуры, вызывает функцию draw_lines(), чтобы выполнить реальное рисование на экране, а затем пытается восстановить цвет и фигуру, заданные до ее вызова.
void Shape::draw() const
{
Fl_Color oldc = fl_color();
// универсального способа идентифицировать текущий стиль
// не существует
fl_color(lcolor.as_int()); // задаем цвет
fl_line_style(ls.style(),ls.width()); // задаем стиль
draw_lines();
fl_color(oldc); // восстанавливаем цвет (предыдущий)
fl_line_style(0); // восстанавливаем стиль линии (заданный
// по умолчанию)
}
К сожалению, в библиотеке FLTK не предусмотрен способ идентификации текущего стиля, поэтому он просто устанавливается по умолчанию. Это пример компромисса, на который мы иногда идем, чтобы обеспечить простоту и мобильность программы. Мы не думаем, что эту функциональную возможность стоит реализовать в нашей интерфейсной библиотеке.
Обратите внимание на то, что функция Shape::draw() не работает с цветом заливки фигуры и не управляет видимостью линий. Эти свойства обрабатывают отдельные функции draw_lines(), которые лучше “знают”, как их интерпретировать. В принципе всю обработку цвета и стиля можно было бы перепоручить отдельным функциям draw_lines(), но для этого пришлось бы повторять много одних и тех же фрагментов кода.
Рассмотрим теперь, как организовать работу с функцией draw_lines(). Если немного подумать, то можно прийти к выводу, что функции-члену класса Shape было бы трудно рисовать все, что необходимо для создания любой разновидности фигуры. Для этого пришлось бы хранить в объекте класса Shape каждый пиксель каждой фигуры. Если мы используем вектор vector<Point>, то вынуждены хранить огромное количество точек. И что еще хуже, экран (т.е. устройство для вывода графических изображений) лучше “знает”, как это делать.
Для того чтобы избежать лишней работы и сохранять лишнюю информацию, примем другой подход: дадим каждому классу, производному от класса Shape, возможность самому определить, что он будет рисовать. Классы Text, Rectangle и Circle лучше “знают”, как нарисовать свои объекты. На самом деле все такие классы это “знают”. Помимо всего прочего, такие классы точно “знают” внутреннее представление информации. Например, объект класса Circle определяется точкой и радиусом, а не, скажем, отрезком линии. Генерирование требуемых битов для объекта класса Circle на основе точки и радиуса там, где это необходимо, и тогда, когда это необходимо, не слишком сложная и затратная работа. По этой причине в классе Circle определяется своя собственная функция draw_lines(), которую мы хотим вызывать, а не функция draw_lines() из класса Shape. Именно это означает слово virtual в объявлении функции Shape::draw_lines().
struct Shape {
// ...
virtual void