4.5 Дружественные функции и классы
Иногда желательно иметь непосредственный доступ извне к скрытым полям класса, то есть расширить интерфейс класса. Для этого служат дру- жественные функции и дружественные классы.
4.5.1 Дружественная функция
Дружественная функция объявляется внутри класса, к элементам кото- рого ей нужен доступ, с ключевым словом friend
. В качестве параметра ей должен передаваться объект или ссылка на объект класса, поскольку указа- тель this ей не передается. Одна функция может «дружить» сразу с не- сколькими классами.
Дружественная функция может быть обычной функцией или методом другого ранее определенного класса. На нее не распространяется действие
36 спецификаторов доступа, место размещения ее объявления в классе безраз- лично.
В качестве примера ниже приведено описание двух функций, друже- ственных классу monster
. Функция kill является методом класса hero
, а функция steal_ammo не принадлежит ни одному классу. Обеим функциям в качестве параметра передается ссылка на объект класса monster class monster;
// Предварительное объявление класса class hero
{ void kill(monster &);
}; class monster
{ friend int steal_ammo(monster &);
/* Класс hero должен быть определен ранее */ friend void hero::kill(monster &);
}; int steal_ammo(monster &M){return --M.ammo;} void hero::kill(monster &M){M.health = 0; M.ammo = 0;}
4.5.2 Дружественная класс
Если все методы какого-либо класса должны иметь доступ к скрытым полям другого, весь класс объявляется дружественным с помощью ключе- вого слова friend
. В приведенном ниже примере класс mistress объявля- ется дружественным классу hero
: class hero {
... friend class mistress;
}; class mistress{
... void f1(); void f1();
};
Функции f1
и f2
являются дружественными по отношению к классу hero
(хотя и описаны без ключевого слова friend
) и имеют доступ ко всем его полям.
Объявление friend не является спецификатором доступа и не наследу- ется. Обратите внимание на то, что класс сам определяет, какие функции и классы являются дружественными, а какие нет.
37
4.6 Деструкторы Деструктор – это особый вид метода, применяющийся для освобождения памяти, занимаемой объектом. Деструктор вызывается автоматически, ко- гда объект выходит из области видимости:
для локальных переменных – при выходе из блока, в
котором они объ- явлены;
для глобальных – как часть процедуры выхода из main
;
для объектов, заданных через указатели, деструктор вызывается не- явно при использовании операции delete
(автоматический вызов деструк- тора при выходе указателя из области действия не производится).
При уничтожении массива деструктор вызывается для каждого элемента удаляемого массива. Для динамических объектов деструктор вызывается при уничтожении объекта операцией delete
. При выполнении операции delete[]
деструктор вызывается для каждого элемента удаляемого мас- сива.
Имя деструктора начинается с тильды (
), непосредственно за которой следует имя класса. Деструктор:
не имеет аргументов и возвращаемого значения;
не может быть объявлен как const или static
;
не наследуется;
может быть виртуальным;
может вызываться явным образом путем указания полностью уточ- ненного имени; это необходимо для объектов, которым с помощью new вы- делялся конкретный адрес.
Если деструктор явным образом не определен, компилятор создает его автоматически.
Описывать в классе деструктор явным образом требуется в случае, когда объект содержит указатели на память, выделяемую динамически – иначе при уничтожении объекта память, на которую ссылались его поля-указа- тели, не будет помечена как свободная. Указатель на деструктор определить нельзя.
Деструктор для рассматриваемого примера должен выглядеть так: monster::monster() {delete [] name;}
38
5. ПЕРЕГРУЗКА ОПЕРАЦИЙ В ООП 5.1 Перегрузка операций С++ позволяет переопределить действие большинства операций так, чтобы при использовании с объектами конкретного класса они выполняли заданные функции. Эта дает возможность использовать собственные типы данных точно так же, как стандартные. Обозначения собственных операций вводить нельзя.
Можно перегружать любые операции, существующие в
С++, за исключением:
.*
?:
::
#
##
sizeof
Перегрузка операций осуществляется с помощью функций специального вида (функций-операций) и подчиняется следующим правилам:
сохраняются количество аргументов, приоритеты операций и правила ассоциации (справа налево или слева направо) по сравнению с использова- нием в стандартных типах данных;
нельзя переопределить операцию по отношению к стандартным типам данных;
функция-операция не может иметь аргументов по умолчанию;
функции-операции наследуются (за исключением =).
Функцию-операцию можно определить тремя способами: она должна быть либо методом класса, либо дружественной функцией класса, либо обычной функцией. В двух последних случаях функция должна принимать хотя бы один аргумент, имеющий тип класса, указателя или ссылки на класс
(особый случай: функция-операция, первый параметр которой – стандарт- ного типа, не может определяться как метод класса).
Функция-операция содержит ключевое слово operator
, за которым сле- дует знак переопределяемой операции: тип operator операция ( список параметров) { тело функции }
5.2 Перегрузка унарных операций Унарная функция-операция, определяемая внутри класса, должна быть представлена с
помощью нестатического метода без параметров, при этом операндом является вызвавший ее объект, например: class monster
{... monster & operator ++() {++health; return *this;}} monster Vasia; cout << (++Vasia).get_health();
39
Если функция определяется вне класса, она должна иметь один параметр типа класса: class monster
{... friend monster & operator ++( monster &M);}; monster& operator ++(monster &M) {++M.health; return M;}
Если не описывать функцию внутри класса как дружественную, нужно учитывать доступность изменяемых полей (в данном случае поле health недоступно извне, так как описано со спецификатором private
, поэтому для его изменения требуется использование соответствующего метода, не описанного в приведенном примере).
Операции постфиксного инкремента и декремента должны иметь пер- вый параметр типа int
. Он используется только для того, чтобы отличить их от префиксной формы: class monster
{... monster operator ++(int){monster M(*this); health++; return M;}}; monster Vasia; cout << (Vasia++).get_health();
5.3 Перегрузка бинарных операций
Бинарная функция-операция, определяемая внутри класса, должна быть представлена с помощью нестатического метода с параметрами, при этом вызвавший ее объект считается первым операндом: class monster
{ bool operator >( const monster &M)
{ if( health > M.get_health()) return true; return false;
}
};
Если функция определяется вне класса, она должна иметь два параметра типа класса: bool operator >(const monster &M1, const monster &M2)
{ if( M1.get_health() > M2.get_health()) return true; return false;
}
Бинарные арифметические операции, такие как
+
,
- и
*
, возвращают но- вый экземпляр класса, помеченный ключевым словом const
. Например:
40 const MyClass MyClass::operator+(const MyClass &other) const {
MyClass result = *this; // Make a copy of myself result.value += other.value; // Use += to add other to the copy. return result; // All done!
}
Использование ключевого слова const необходимо для того, что бы было не возможно написать следующий код:
MyClass a, b, c;
(a + b) = c;
В случае отсутствия ключевого слова const, данный код будет успешно скомпилирован.
5.4 Перегрузка операции присваивания Операция присваивания определена в любом классе по умолчанию как поэлементное копирование. Эта
операция вызывается каждый раз, когда од- ному существующему объекту присваивается значение другого. Если класс содержит поля ссылок на динамически выделяемую память, необходимо определить собственную операцию присваивания. Чтобы сохранить семан- тику операции, операция-функция должна возвращать ссылку на объект, для которого она вызвана, и принимать в качестве параметра единственный аргумент – ссылку на присваиваемый объект: monster& operator = (const monster &M)
{ if (&M == this)
// Проверка на самоприсваивание return *this; if (name) delete [] name; if (M.name) { name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
} else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; return *this;
}
Операцию присваивания можно определять только в теле класса. Она не наследуется. Можно заметить, что операция присваивания возвращает ссылку, что позволяет совершать «цепочки присваивания»: int a, b, c, d; a = b = c = d = 23;
В данной цепочке присваивания первой выполняется операция d = 23
, возвращающая ссылку на переменную d
, значение которой, в свою очередь, присваивается переменной с
и т.д.
41
5.5 Перегрузка операции приведения типа
Можно определить функции-операции, которые будут осуществлять преобразование класса к другому типу. Формат: operator имя_нового_типа ();
Тип возвращаемого значения и параметры указывать не требуется.
Можно определять виртуальные функции преобразования типа.
Пример: monster::operator int(){ return health; } monster Vasia; cout << int(Vasia);
5.6 Особенности работы операторов new и delete
Переменная объектного типа в динамической памяти создаётся в два этапа:
1. Выделяется память с помощью оператора new
2. Вызывается конструктор класса.
Удаляется такая переменная тоже в два этапа:
1. Вызывается деструктор класса.
2. Освобождается память с помощью оператора delete
5.7 Перегрузка операторов new и delete для отдельных классов
Операторы new и delete можно перегрузить. Для этого есть несколько причин:
можно увеличить производительность за счёт кеширования: при уда- лении объекта не освобождать память, а сохранять указатели на свободные блоки, используя их для вновь конструируемых объектов.
можно выделять память сразу под несколько объектов.
можно реализовать собственный «сборщик мусора» (
garbage collector
).
можно вести лог выделения/освобождения памяти.
Операторы new и delete имеют следующие сигнатуры: void *operator new(size_t size); void operator delete(void *p);
Оператор new принимает размер памяти, которую необходимо выделить, и возвращает указатель на выделенную память.
42
Оператор delete
принимает указатель на память, которую нужно осво- бодить. class A { public: void *operator new(size_t size); void operator delete(void *p);
}; void *A::operator new(size_t size) { printf("Allocated %d bytes\n", size); return malloc(size);
} void A::operator delete(void *p) { free(p);
}
Вместо функций malloc и free можно использовать глобальные опера- торы
::new и
::delete
Рекомендуется не производить в операторе new
(особенно в глобальном) какие-либо операции с объектами, которые могут вызвать оператор new
Например, для вывода текста используется функция printf
, а не объект std::cout
Операторы new и delete
, объявленные внутри класса, функционируют подобно статическим функциям и вызываются для данного класса и его наследников, для которых эти операторы не переопределены.
5.8 Переопределение глобальных операторов new и delete В некоторых случаях может потребоваться перегрузить глобальные опе- раторы new и delete
. Они находятся не в пространстве имен std
, а в гло- бальном пространстве имён.
Глобальные операторы new и delete вызываются для примитивных ти- пов и для классов, в которых они не переопределены. Они имеют такие же сигнатуры, что и рассмотренные выше операторы new и delete
// Для примитивных типов вызываются глобальные ::new и ::delete int *i = new int; delete i;
// Для класса A вызываются переопределённые A::new и A::delete
A *a = new A; delete a;
43
// Для класса C операторы new и delete не переопределены,
// поэтому вызываются глобальные ::new и ::delete
C *c = new C; delete c;
44
6. НАСЛЕДОВАНИЕ В ООП При большом количестве никак не связанных классов управлять ими ста- новится невозможным. Наследование позволяет справиться с этой пробле- мой путем упорядочивания и ранжирования классов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Механизм наследования классов позволяет строить иерархии, в которых производные классы получают элементы родительских, или базовых, клас- сов и могут дополнять их или изменять их свойства.
Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее общие черты для всех нижележащих классов. По мере продвиже- ния вниз по иерархии классы приобретают все больше конкретных черт.
Множественное наследование позволяет одному классу обладать свой- ствами двух и более родительских классов.
6.1 Виды наследования При описании класса в его
заголовке перечисляются все классы, являю- щиеся для него базовыми. Возможность обращения к элементам этих клас- сов регулируется с помощью модификаторов наследования private
, protected и public
Если базовых классов несколько, то они перечисляются через запятую.
Перед каждым может стоять свой модификатор наследования. По умолча- нию для классов установлен модификатор private
, а для структур – public
Если задан модификатор наследования public
, оно называется откры- тым. Использование модификатора protected делает наследование защи- щенным, а модификатора private
– закрытым. В зависимости от вида наследования классы ведут себя по-разному. Класс может наследовать от структуры, и наоборот.
Для любого элемента класса может также использоваться спецификатор protected
, который для одиночных классов, не входящих в иерархию, рав- носилен private
. Разница между ними проявляется при наследовании. Воз- можные сочетания модификаторов и спецификаторов доступа приведены в таблице ниже.
Как видно из таблицы ниже, private элементы базового класса в произ- водном классе недоступны вне зависимости от ключа. Обращение к ним мо- жет осуществляться только через методы базового класса.
45
Модификатор наследо- вания
Спецификатор базового класса
Доступ в производном классе private private нет protected private public private protected private нет protected protected public protected public private нет protected protected public public
Элементы protected при наследовании с ключом private становятся в производном классе private
, в остальных случаях права доступа к ним не изменяются.
Доступ к элементам public при наследовании становится соответству- ющим ключу доступа.
Если базовый класс наследуется с ключом private
, можно выборочно сделать некоторые его элементы доступными в производном классе, объ- явив их в секции public производного класса с помощью операции доступа к области видимости: class Base {... public: void f();
}; class Derived : private Base {... public: using Base::f;
};
6.2 Простое наследование
Простым называется наследование, при котором производный класс имеет одного родителя. Для различных элементов класса существуют раз- ные правила наследования. Рассмотрим наследование классов на примере.
Создадим производный от класса monster класс daemon
, добавив «де- мону» способность думать: enum color {red, green, blue};
// ------------- Класс monster ------------- class monster
{
// --------- Скрытые поля класса: ------------ int health, ammo; color skin; char *name; public:
// ------------- Конструкторы: monster(int he = 100, int am = 10); monster(color sk);
46 monster(char * nam); monster(monster &M);
// ------------- Деструктор:
monster() {delete [] name;}
// ------------- Операции: monster& operator ++(){++health; return *this;} monster operator ++(int)
{monster M(*this); health++; return M;} operator int(){return health;} bool operator >(monster &M)
{ if( health > M.get_health()) return true; return false;
} monster& operator = (monster &M)
{ if (&M == this) return *this; if (name) delete [] name; if (M.name) { name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
} else name = 0; health = M.health; ammo = M.ammo; skin = M.skin; return *this;
}
// ------------- Методы доступа к полям: int get_health() const { return health; } int get_ammo() const { return ammo; }
// ------------- Методы, изменяющие значения полей: void set_health(int he){ health = he;} void draw(int x, int y, int scale, int position);
};
// ------------- Реализация класса monster ------------- monster::monster(int he, int am): health (he), ammo (am), skin (red), name (0){} monster::monster(monster &M)
{ if (M.name)
{ name = new char [strlen(M.name) + 1]; strcpy(name, M.name);
} else name = 0; health = M.health; ammo = M.ammo; skin = M.skin;
} monster::monster(color sk)
{ switch (sk)
{ case red: health = 100; ammo = 10; skin = red; name = 0; break; case green: health = 100;ammo = 20;skin = green; name = 0; break; case blue: health = 100; ammo = 40; skin = blue; name = 0;break;
}
} monster::monster(char * nam)
{ name = new char [strlen(nam)+1]; strcpy(name, nam); health = 100; ammo = 10; skin = red;
}
47 void monster::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка monster */ }
// ------------- Класс daemon ------------- class daemon : public monster
{ int brain; public:
// ------------- Конструкторы: daemon(int br = 10){brain = br;}; daemon(color sk) : monster (sk) {brain = 10;} daemon(char * nam) : monster (nam) {brain = 10;} daemon(daemon &M) : monster (M) {brain = M.brain;}
// ------------- Операции: daemon& operator = (daemon &M)
{ if (&M == this) return *this; brain = M.brain; monster::operator = (M); return *this;
}
// ------------- Методы, изменяющие значения полей: void draw(int x, int y, int scale, int position); void think();
};
// ------------- Реализация класса daemon ------------- void daemon::draw(int x, int y, int scale, int position)
{ /* ... Отрисовка daemon */ } void daemon:: think(){ /* ... */ }
В классе daemon введено поле brain и метод think
, определены соб- ственные конструкторы и операция присваивания, а также переопределен метод отрисовки draw
. Все поля класса monster
, операции (кроме присваи- вания) и методы get_health
, get_ammo и set_health наследуются в классе daemon
, а деструктор формируется по умолчанию.