int x; int y; Point() : x(0), y(0) {} Point(int xValue, int yValue) : x(xValue), y(yValue) {)
};
Мы определили point как структуру, а не как полноценный класс. Правило, на основании которого мы так поступили, очень просто. Если абстракция представляет собой собрание других объектов без какого-либо собственного поведения, мы делаем ее структурой. Однако, когда наша абстракция подразумевает более сложное поведение, чем простой доступ к полям структуры, то нужно определять класс. В данном случае абстракция point - это просто пара координат (x, y). Для удобства предусмотрено два конструктора: один инициализирует точку нулевыми значениями координат, а другой - некоторыми заданными значениями.
Теперь определим экранный объект (DisplayItem). Это абстракция довольно обычна для систем с графическим интерфейсом (GUI) - она является базовым классом для всех объектов, которые можно отображать в окне. Мы хотим сделать его чем-то большим, чем просто совокупностью точек. Надо, чтобы клиенты могли рисовать, выбирать объекты и перемещать их по экрану, а также запрашивать их положение и состояние. Мы записываем нашу абстракцию в виде следующего объявления на C++:
class DisplayItem { public:
DisplayItem(); DisplayItem(const Point& location); virtual ~DisplayItem(); virtual void draw(); virtual void erase(); virtual void select(); virtual void unselect(); virtual void move(const Point& location); int isSelected() const; Point location() const; int isUnder(const Point& location) const;
protected: ... };
В этом объявлении мы намеренно опустили конструкторы, а также операторы для копирования, присваивания и проверки на равенство. Их мы оставим до следующего раздела.
Мы ожидаем, что у этого класса будет много наследников, поэтому деструктор и все модификаторы объявлены виртуальными. В особенности это относится к draw. Напротив, селекторы скорее всего не будут переопределяться в подклассах. Заметьте, что один из них, isUnder, должен вычислять, накрывает ли объект данную точку, а не просто возвращать значение какого-то свойства.
Объявим экземпляры указанных классов:
DisplayItem item1; DisplayItem* item2 = new DisplayItem(Point(75, 75)); DisplayItem* item3 = new DisplayItem(Point(100, 100)); DisplayItem* item4 = 0;
Рис. 3-1а показывает, что при выполнении этих операторов возникают четыре имени и три разных объекта. Конкретно, в памяти будут отведены четыре места под имена item1, item2, item3, item4. При этом item1 будет именем объекта класса DisplayItem, а три других будут указателями. Кроме того, лишь item2 и item3 будут на самом деле указывать на объекты класса DisplayItem. У объектов, на которые указывают item2 и item3, к тому же нет имен, хотя на них можно ссылаться "разыменовывая" соответствующие указатели: например, *item2. Поэтому мы можем сказать, что item2 указывает на отдельный объект класса DisplayItem, на имя которого мы можем косвенно ссылаться через *item2. Уникальная идентичность (но не обязательно имя) каждого объекта сохраняется на все время его существования, даже если его внутреннее состояние изменилось. Эта ситуация напоминает парадокс Зенона о реке: может ли река быть той же самый, если в ней каждый день течет разная вода?
Рассмотрим результат выполнения следующих операторов (рис. 3-1б):
item1.move(item2->location()); item4 = item3; item4->move(Point(38, 100));
Объект item1 и объект, на который указывает item2, теперь относятся к одной и той же точке экрана. Указатель item4 стал указывать на тот же объект, что и item3. Кстати, заметьте разницу между выражениями "объект item2" и "объект, на который указывает item2". Второе выражение более точно, хотя для краткости мы часто будем использовать их как синонимы.