Идентичность – это такое свойство объекта, которое отличает его от всех других объектов.
Источником ошибок в объектно-ориентированном программировании является неумение отличать имя объекта от самого объекта.
Пример. Определим точку на плоскости.
struct Point {
int х, у; // координаты
Point(void); // конструктор по умолчанию (0,0)
Point(int xValue, int yValue); // конструктор
};
Теперь определим точку, отображаемую на экране дисплея (DisplayPoint). Ограничимся возможностями рисовать точку и перемещать ее по экрану, а также запрашивать ее положение. Мы записываем нашу абстракцию в виде следующего объявления на C++:
class DisplayPoint {
public:
DisplayPoint(); // конструктор по умолчанию (0,0)
DisplayPoint(const Point& location); // конструктор
~DisplayPoint(); // деструктор
void draw(); // рисует точку на экране
void move(const Point& location); // перемещает точку
Point location(); // возвращает координаты
...
};
Аргументы некоторых функций указаны с модификатором const. Он указывает, что значение объекта, передаваемого по ссылке или указателю, в функции не изменится. Литералы, константы и аргументы, требующие преобразования типа, можно передавать как const&-аргументы и нельзя – в качестве не const &-аргументов.
|
|
Объявим экземпляры класса DisplayPoint:
DisplayPoint Item1;
DisplayPoint * Item2 = new DisplayPoint(Point(75,75));
DisplayPoint * Item3 = new DisplayPoint(Point(100,100));
DisplayPoint * Item4 = 0;
При выполнении этих операторов возникают четыре имени и три разных объекта (рис. 3.1 а). В памяти будут отведены четыре места под имена Item1, Item2, Item3, Item4. При этом Item1 будет именем объекта класса DisplayPoint, а три других – указателями. Кроме того, лишь Item2 и Item3 будут на самом деле указывать на объекты класса. У объектов, на которые указывают Item2 и Item3, к тому же нет имен, хотя на них можно ссылаться «разыменовывая» соответствующие указатели (например, *Item2). Поэтому мы можем сказать, что Item2 указывает на отдельный объект класса, на имя которого мы можем косвенно ссылаться через *Item2.
Рис. 3.1 Идентичность объектов
Уникальная идентичность каждого объекта сохраняется на все время его существования, даже если его внутреннее состояние изменилось. При этом имя объекта не обязательно сохраняется.
Рассмотрим результат выполнения следующих операторов (рис. 3.1, б):
Item1.move(Item2 -> location());
Item4 = Item3;
Item4 -> move(Point(38, 100));
Объект Item1 и объект, на который указывает Item2, теперь относятся к одной и той же точке экрана. Указатель Item4 стал указывать на тот же объект, что и Item3. Хотя объект Item1 и объект, на который указывает Item2, имеют одинаковое состояние, они остаются разными объектами. Кроме того, мы изменили состояние объекта *Item3, использовав его новое косвенное имя Item4.
Ситуацию, когда объект именуется более чем одним способом несколькими синонимичными именами, называют структурной зависимостью.
|
|
Структурная зависимость порождает в объектно-ориентированном программировании много проблем. Трудность распознания побочных эффектов при действиях с синонимичными объектами часто приводит к утечкам памяти, неправильному доступу к памяти и, хуже того, непрогнозируемому изменению состояния. Рассмотрим результат выполнения следующих действий (рис. 3.1, в):
delete Item3;
Item2 = &Item1;
В первой строке мы уничтожили объект через указатель Item3, теперь значение указателя Item4 оказывается бессмысленным. Эта ситуация называется висячей ссылкой.
Во второй строке создается синоним: Item2 указывает на тот же объект, что и Item1. К сожалению, при этом произошла утечка памяти: объект, на который первоначально указывал Item2, не именуется ни прямо, ни косвенно и его идентичность потеряна.
В языках типа C++ такая память освобождается только тогда, когда завершается программа, создавшая объект. Такие утечки памяти могут вызвать и просто неудобство, и крупные сбои, особенно если программа должна непрерывно работать длительное время. Представьте себе утечку памяти в программе управления спутником. Перезапуск компьютера на спутнике в нескольких миллионах километров от Земли очень неудобен.
Для создания нового объекта, имеющего то же состояние, что и у существующего, необходимо вызвать конструктор копирования, имеющий следующее описание:
DisplayPoint(const DisplayPoint &); // конструктор копирования
Отсутствие этого специального конструктора вызывает копирующий конструктор, действующий по умолчанию, который копирует объект поэлементно. Это разумно не всегда. Когда объект содержит ссылки или указатели на другие объекты, такая операция приводит к созданию синонимов указателей на эти объекты.
Пример. Модифицируем описание класса DisplayPoint так, чтобы каждый его экземпляр содержал указатель на точку:
class DisplayPoint {
...
Point * DPoint
...
};
...
DisplayPoint Item1;
DisplayPoint Item2(Item1); // вызов конструктора копирования
Поэлементное копирование объекта Item1 приведет к тому, что указатели на агрегированные объекты типа Point у обоих объектов Item1 и Item2 будут указывать на один и тот же объект, содержащий местоположение отображаемой точки (рис. 3.2). Фактически, имеем структурную зависимость: оба объекта будут ответственны за отображение одной и той же точки. Этого ли мы хотели достичь?
Рис. 3.2. Результат поэлементного копирования
Присваивание – это тоже копирование, и в C++ его смысл можно изменять. Например, мы могли бы добавить в определение класса DisplayPoint следующую строку:
DisplayPoint operator=(const DisplayPoint &);
Теперь мы можем записать
DisplayPoint Item5;
Item5 = Item1;
Как и в случае копирующего конструктора, если оператор присваивания не переопределен явно, то по умолчанию объект копируется поэлементно.
Понятие идентичности тесно связано с вопросом равенства. Равенство можно понимать двумя способами. Во-первых, два имени могут обозначать один и тот же объект (Item1 и Item2 на рис. 3.1, в). Во-вторых, это может быть равенство состояний у двух разных объектов (Item1 и Item2 на рис. 3.1, б).
В С++ нет предопределенного оператора равенства, поэтому мы должны определить равенство и неравенство, объявив эти операторы при описании:
int operator ==(DisplayPoint&) const;
int operator!=(DisplayPoint&) const;