1.3.
Дружественные функции
В файле shape.cpp расположена функция tofit. bool tofit(circle & a, rectangle b){ if (b.width() >= 2 * a.r && b.height() >= 2 * a.r){ a.x = (b.x1 + b.x2) / 2; a.y = (b.y1 + b.y2) / 2; return true;
} return false;
}
В функции tofit используются не только функции этих классов, но и их поля. При этом поля описаны как private, т.е. защищены от внешне- го доступа. Чтобы разрешить внешней функции доступ к защищённым по- лям объектов, необходимо её сделать дружественной для этих классов. Для этого описание функции со спецификатором friend помещается в интер- фейс каждого из классов: friend bool tofit(circle &a, rectangle b);
В соответствии с принципом инкапсуляции работа с защищёнными членами класса должна быть организована через открытые функции. В рас- сматриваемом примере можно было бы организовать геттеры и сеттеры для доступа к закрытым членам классов. Если больше ни для каких других це- лей он не нужны, то вместо них можно использовать механизм дружест- венности. Важно, что при этом сокращается количество вызовов функций.
Объявление функции дружественной классу разрешает доступ к за- щищённым полям класса только для этой функции.
18
Дружественными по отношению к рассматриваемому классу могут быть не только внешние функции, но и функции другого класса, или даже другой класс (все его функции).
На функции, объявленные дружественными, не распространяются действия спецификаторов (public, private, protected).
Объявление функций дружественными рекомендуется располагать либо в самом начале, либо в самом конце описания класса.
Параметрами функции tofit являются объекты обоих классов. По- скольку объявление дружественности для tofit расположено в интерфей- сах обоих классов, возникает проблема очерёдности их описания. Решени- ем является использование предварительного описания одного из классов. class rectangle;
Пример 3. Используя классы, описанные в примере 2, создать объект
«прямоугольник» и массив объектов класса «круг». Посчитать количество кругов, которые можно поместить внутри прямоугольника. Для таких кру- гов совместить их центры с центром прямоугольника.
#include "shape.h"
#include
#include //содержит srand() и rand()
#include
//содержит time() using namespace std; int main(){ setlocale(0, "Russian"); rectangle b(10, 10, 40, 40); circle* c[5]; srand((unsigned)time(0)); for (int i = 0; i < 5; ++i) { c[i] = new circle(rand()%100, rand()%100, rand()%10 + 10); c[i]->print();
} int count = 0; for (int i = 0; i < 5; ++i) if (tofit(*c[i], b))
19 count++; cout << "количество перемещённых = " << count << endl; for (int i = 0; i < 5; ++i) c[i]->print(); for (int i = 0; i < 5; ++i) delete c[i];
}
В условии задачи требуется создать массив объектов класса circle.
Если бы описание массива выглядело таким образом circle c[5]; то все объекты были бы созданы с помощью конструктора по умолчанию, т.е. с одинаковыми значениями переменных-членов класса. Чтобы иметь возможность для каждого элемента массива вызывать конструктор с пара- метрами, необходимо разместить их в динамической памяти, т.е. при объ- явлении использовать массив указателей. circle* c[5];
Затем для каждого элемента массива создать объект класса circle. for (int i = 0; i < 5; ++i) { c[i] = new circle(rand()%100, rand()%100, rand()%10 + 10); c[i]->print();
}
В конце программы динамическая память освобождается. for (int i = 0; i < 5; ++i) delete c[i];
Пример 4. Реализовать класс – динамический массив целых чисел.
Перегрузить операции: + для
двух массивов одинакового размера; + для массива и целого числа; += для двух массивов одинакового размера; при- сваивания; обращение к элементу по индексу; вывода в поток.
//classArray.h
#ifndef CLASSARRAY_H
#define CLASSARRAY_H
#include
using namespace std; class Array { private: int n;
20 int *A; public:
Array();
//конструктор по умолчанию
Array(int _n, int x = 0);
//конструктор с параметром по умолчанию
Array(const Array &B);
//конструктор копии int length() const; //функция для нахождения размера массива void resize(int nsize);
//изменение размера массива
Array operator + (const Array &B);
Array &operator += (const Array &B);
Array operator + (const int x);
Array &operator = (const Array &B); int& operator [] (int i);
Array(); friend ostream & operator << (ostream &out, const Array &B);
};
#endif
//classArray.cpp
#include "classArray.h"
Array::Array() { n = 10;
A = new int[n]; for (int i = 0; i A[i] = 0;
}
Array::Array(int _n, int x) { n = _n;
A = new int[n]; for (int i = 0; i A[i] = x;
}
Array::Array(const Array &B) { n = B.n;
A = new int[n]; for (int i = 0; i A[i] = B.A[i];
} int Array::length() const{ return n;
} void Array::resize(int nsize) { int * ndata = new int[nsize]; int sz = (n < nsize) ? n : nsize;
21 for (int i=0;i A = ndata; n = nsize;
}
Array Array::operator + (const Array &B) { if (n != B.n) throw (1);
Array C(n); //можно: Array C(n,0); for (int i = 0; i C.A[i] = A[i] + B.A[i]; return C;
}
Array &Array::operator += (const Array &B) { if (n != B.n) throw (1); for (int i = 0; i A[i] = A[i] + B.A[i]; return *this;
}
Array Array::operator + (const int x) {
Array C(n); //можно: Array C(n,0); for (int i = 0; i C.A[i] = A[i] + x; return C;
}
Array &Array::operator = (const Array &B) { if (this != &B){ delete[] A; n = B.n;
A = new int[n]; for (int i = 0; i A[i] = B.A[i];
} return *this;
} int& Array::operator [] (int i) { return A[i];
}
Array::Array(){ delete[] A;
}
22 ostream & operator << (ostream & out, const Array &B) { for (int i = 0; i}
//main.cpp
#include "classArray.h" int main(){ setlocale(0, "Russian");
Array Q(10, 5);
Array P(10);
Array W, E;
Array R(Q); cout << "Q " << Q; cout << "P " << P; cout << "W " << W; cout << "E " << E; cout << "R " << R; cout << "срединные элементы массива Q "
<< Q[Q.length() / 2 - 1] << ' '
<< Q[Q.length() / 2] << endl; for (int i = 0; i < Q.length(); ++i)
Q[i] = Q.length() - 1 - i; cout << "изменённый Q " << Q; cout << "срединные элементы массива Q "
<< Q[Q.length() / 2 - 1] << ' '
<< Q[Q.length() / 2] << endl; for (int i = 0; i < E.length(); ++i)
E[i] = i; cout << "изменённый E " << E;
W = Q + 15; cout << "изменённый W " << W;
P = W + R; cout << "изменённый P " << P;
E += P; cout << "изменённый E " << E; cout << "срединные элементы массива P "
<< P[P.length() / 2 - 1] << ' '
<< P[P.length() / 2] << endl;
Array Z(15); try {
Z += Q; cout << "изменённый Z " << Z;
} catch (int) {
23 cout << "Массивы имеют разную длину" << endl;
} return 0;
}
Когда классы имеют сложную структуру, удобно использовать UML диаграммы классов, которые наглядно показывают их внутреннюю органи- зацию. В частности, на диаграммах представляются члены-данные (поля) и члены-функции (методы) с указанием в виде пиктограмм областей видимо- сти. UML диаграмма для класса Array представлен на рисунке 1.1.
Рис.1.1. UML диаграмма класса Array
На диаграмме не отображается перегруженная операция вывода в по- ток, реализованная с помощью дружественной функции, поскольку она не является членом класса.
Обычно разработку классов начинают с построения UML диаграмм.
Помимо этого, UML диаграммы можно использовать при описании требо- ваний к задаче.
Для класса Array размер динамического массива хранится в приват- ной переменной-члене класса n. Открытая функция-член класса length()
24 предназначена для получения длины динамического массива. Функция length() объявлена константной, так как она не изменяет вызывавший её объект. int Array::length() const{ return n;
}
Динамический массив подразумевает, что у него может меняться размер в ходе выполнения программы. Для это в классе предусмотрена функция resize(int). void Array::resize(int nsize) { int * ndata = new int[nsize]; int sz = (n < nsize) ? n : nsize; for (int i=0;i A = ndata; n = nsize;
}
Для создания объектов класса Array используются разные определения. Каждому виду определения соответствует свой конструктор.
Array Q(10,5); //конструктор с параметром по умолчанию.
//Второй параметр задан явно
Array P(10); //конструктор с параметром по умолчанию.
//Второй параметр задан неявно
Array W,E; //конструктор по умолчанию
Array R(Q); //конструктор копии
Конструктор копии, или копирующий конструктор, создаёт копию объекта, передаваемого в конструктор в качестве параметра. Кроме явного вызова конструктора копии при создании объекта, он неявно вызывается каждый раз, когда объект передаётся в функцию в качестве параметра по значению или функция возвращает объект.
Поясним эти три случая, когда вызывается конструктор копии.
1. Явное создание нового объекта как копии существующего:
Array myv1 (Q);
Array myv2 = myv1;
25 2. Вызов функции с передачей параметра по значению: void f(Array arr) {/* … */}
3. Возврат объекта из функции по значению:
Array g() {/* … */}
Копирующие конструкторы настолько важны, что компилятор авто- матически генерирует копирующий конструктор, если этого не делает про- граммист. Такой автоматически создаваемый конструктор копии является конструктором с побитовым копированием переменных-членов класса. Та- кое копирование называется поверхностным.
В классе Array для размещения массива используется динамическая память, адрес которой хранится в переменной A. В случае использования конструктора копии, создаваемого автоматически, вместо копии массива будет создана копия ссылки на него, т. е. ссылки двух разных объектов бу- дут указывать на одно и то же место в памяти. Для классов, размещающих данные в динамической памяти, необходим конструктор копии.
Для классов, использующих размещение данных в динамической памя- ти, отсутствие конструктора копии является грубой ошибкой.
Реализация конструктора копии для класса Array:
Array::Array (const Array &B){ n=B.n;
A=new int[n]; for (int i=0; i A[i]=B.A[i];
}
Завершая обсуждение конструктора копии, рассмотрим следующую ситуацию. class Array { public:
};
26
Array g() { return Array();
} int main()
{
Array myv1 = g();
}
Возникает вопрос – сколько раз будет вызван конструктор копии?
Здесь присутствуют случаи 1 и 3
вызова конструктора копии, а значит должны создаваться две копии. Чтобы проверить, так ли это, можно доба- вить в конструктор копии вывод сообщения о выполнении конструктора.
На самом деле, запуск данного примера покажет, что во время вы- полнения программы конструктор копии не будет вызван ни разу. Это объ- ясняется работой оптимизирующего компилятора. Заметим, что такая оп- тимизация может существенно повлиять на поведение программы в случае, когда конструктор копии содержит побочные эффекты (вывод сообщения).
Эта оптимизация носит название
Return Value Optimization (RVO) и выпол- няется по умолчанию подавляющим большинством современных компиля- торов. Требование выполнения RVO по умолчанию явно оговорено в стан- дарте языка.
Если специальными ключами компиляции запретить RVO, то вызов конструктора копии будет выполнен ровно два, как и ожидалось. Однако в реальных программах просто стараются не помещать в конструктор копии дополнительный код для реализации побочного эффекта.
Если в классе используется выделение памяти, то её необходимо ос- вободить средствами этого же класса. Это должен делать деструктор.
Array::Array( ){ delete[] A;
}
Напомним, что деструктор вызывается компилятором автоматически для каждого созданного объекта.
27
В случае использования динамической памяти в реализации класса – настоятельно рекомендуется реализовать конструкторы и деструктор. Сре- ди конструкторов обязательно должен присутствовать конструктор копии.
Остальные функции-члены класса Array реализуют перегрузку опе- раций.
1.4. Перегрузка операций Перегружать операции можно только для определённых пользовате- лем типов, т. е. для классов. Операции для стандартных типов перегружать нельзя. Перегруженная операция должна иметь хотя бы один операнд, тип которого определён пользователем. Для определения перегруженных опе- раций используется специальная функция с именем operator@, где @ – идентификатор перегружаемой операции.
В примере 1 для класса circle была перегружена операция сравне- ния на равенство (==): bool circle::operator==(circle a){ return equal(a);
}
Ключевое слово operator служит для использования операций в функциональном стиле.
Вызов перегруженной операции можно выполнять двумя способами:
используя функциональный стиль a.operator==(b), например, cout<
используя синтаксис операции a==b, например, cout<<(a==b)<
Попробовать убрать скобки в примере 1 в операторе вывода в поток cout<<(a==b)<Объяснить, почему они необходимы.
28
Хотя C++ позволяет перегружать почти все операции, доступные в
C++, возможности перегрузки ограничены. В частности, отсутствует воз- можность изменения приоритета или количества аргументов у операций.
Кроме того рекомендуется не изменять семантику операций.
Количество аргументов в списке перегруженной операции зависит от двух факторов:
категории операции – унарная или бинарная;
способа определения операции – в виде глобальной функции (один аргумент для унарной, два – для бинарных операций) или функции класса (для
унарных операций аргументы отсутствуют, для бинарных операций – один аргумент).
При определении функции-члена класса объект, для которого она бу- дет вызываться, всегда будет её левым операндом.
Рассмотрим реализацию перегрузки бинарной операций + для класса
Array в примере 4. Эта операция может быть реализована функцией- членом класса с одним аргументом. Напомним, что для всех функций- членов класса первым параметром всегда неявно передаётся указатель на объект, для которого вызывается функция. Именно поэтому у бинарной операции всего один аргумент. Операция сложения перегружается для двух списков параметров.
Если параметром является целое число, то операция предназначена для увеличения всех элементов массива на заданное значение.
Array Array::operator + (const int x) {
Array C(n); //можно: Array C(n,0); for (int i = 0; i
C.A[i] = A[i] + x; return C;
}
29
Данная перегруженная операция сложения является некоммутатив- ной, поскольку её левый операнд — это объект класса Array, а правый — целое число.
Операция сложения двух объектов класса Array выглядит следую- щим образом:
Array Array::operator + (const Array &B){ if (n != B.n) throw (1);
Array C(n); //можно: Array C(n,0); for (int i=0; i C.A[i]=A[i]+B.A[i]; return C;
}
Хотя предполагается, что эта операция будет использована только для массивов одинаковой размерности, функция начинается с проверки корректности вызова и в случае несоответствия выбрасывается исключе- ние. Чтобы отслеживать эту ошибку, операцию следует использовать внут- ри блока try catch.
Array Z(15); try {
Z += Q; cout << "изменённый Z " << Z;
} catch (int) { cout << "Массивы имеют разную длину" << endl;
}
Рассмотрим перегрузку операции присваивания. Она занимает важ- ное место при реализации классов, использующих динамическую память.
Присваивание, так же, как и конструктор копии, по умолчанию реализуется побайтным копированием, которое подразумевает только копирование зна- чений полей объекта. Поэтому при использовании динамической памяти в классе произойдет копирование ссылки на нее, а сама область динамиче- ской памяти станет разделяемой между двумя объектами. Такое разделение доступа к памяти в С++ при работе с указателями не является корректным.
30
В частности, это приведёт к ошибке двойного освобождения памяти.
{
Array myv1;
Array myv2; myv2 = myv1;
}
//вызываются деструктор для объекта myv1 и деструктор для myv2
Здесь произойдёт ещё и утечка памяти, так как старое значение ука- зателя A в объекте myv2 потеряется.
В случае использования динамической памяти в реализации класса на- стоятельно рекомендуется перегружать операцию присваивания.
Функция operator= должна быть обязательно функцией класса.
В определении функции operator= необходимо скопировать всю необходимую информацию из правостороннего объекта в левосторонний.
Array &Array::operator = (const Array &B) { if (this != &B) { delete[] A; n=B.n;
A=new int[n]; for (int i=0; i
A[i]=B.A[i];
} return *this;
}
Сначала следует проверить, не происходит ли самоприсваивание объектов (A=A). Это позволяет избежать выполнения бесполезных опера- ций. if (this != &B) {
}
Поскольку размерности полей, размещённых в динамической памяти, у левостороннего и правостороннего операторов могут не совпадать, реко- мендуется очистить динамическую память левостороннего операнда и за- ново выделить память нужного размера.
31 delete[] A; n=B.n;
A=new int[n];
Игнорирование проверки самоприсваивания в случае использования динамической памяти в определении класса может привести к потере данных.
Напомним, что каждая операция в С++ возвращает значение.
Операция присваивания возвращает значение, совпадающее со значением левого операнда после выполнения операции. Поскольку рекомендуется не изменять семантику при перегрузке операции присваивания необходимо вернуть знеачение левого операнда. Левый операнд – это объект, на который указывает неявный параметр this. return *this;
В данной реализации операции могут появиться проблемы при воз- никновении исключения в операции new (такое исключение это не такая уж редкая ситуация). Поскольку к этому моменту уже выполнена операция delete[] A, объект после исключения в new останется в «полуразру- шенном» состоянии. В C++ выделяют три уровня гарантий безопасности кода при возникновении исключений:
базовый – при возникновении исключения не возникает утечек ресур- сов, однако объекты могут находиться в непредсказуемом состоянии;
строгий – если во время операции произошло исключение, то объект будет находиться в том же состоянии, что до начала операции;
без исключений – в данном коде не может возникнуть исключений.
Приведённая реализация operator= даёт лишь базовую гарантию.
Достаточно несложно изменить её на строгую. Заведём вспомогательную
32 переменную newdata для результата выделения памяти, а операцию удаления A перенесём в конец функции.
Array & Array::operator = (const Array &B) { if (this != &B) { n=B.n; int
*
newdata
=
new int[n]; for (int i=0; i A = newdata;
} return *this;
}
Заметим, что обе версии реализации operator= выполняют дейст- вия, которые используются в других функциях, а именно в деструкторе и в конструкторе копии.
Идиома copy-and-swap опирается на это наблюдение и предполагает реализацию операции копирующего присваивания с использованием кон- структора копий. При этом требуется вначале создать вспомогательную функцию-член swap(Array & other), для обмена содержимого теку- щего объекта с объектом other. void Array::swap(Array & other) { swap(n, other.n); swap(A, other.A);
}
Array& Array::operator=(Array other){//вызов конструктора копии this -> swap(other); return *this;
}
Идиома copy-and-swap позволяет разрабатывать устойчивые к исключе- ниям операторы присваивания и сокращает количество кода в них ценой определения полезной вспомогательной функции swap.
В случае, если для класса перегружены операции + и =, то реализация перегруженной операции += может быть выполнена с их использованием.
33
Например, реализация перегруженной операции += для класса
Array из примера 4
Array &Array::operator += (const Array &B){ for (int i=0; i
A[i]=A[i]+B.A[i]; return *this;
} может быть изменена следующим образом:
Array &Array::operator += (const Array &B){
*this=(*this)+B; return *this;
}
Выполнить эти изменения в примере. Проверить работоспособность программы.
Перегрузка операции индексирования также обладает некоторыми особенностями. Её нужно реализовывать только как функцию-член класса.
Важно, чтобы перегруженная операция доступа к элементу массива по ин- дексу [ ] возвращала ссылку на элемент массива. Это обусловлено требова- ниями к соблюдению семантики. int& Array::operator [] (int i){ return A[i];
}
Перегруженную операцию доступа к элементу массива по индексу [ ] можно использовать как для получения значения элемента массива, так и для его изменения. for (int i = 0; i < Q.length(); ++i)
Q[i] = Q.length() - 1 - i; cout << "изменённый Q " << Q; cout << "срединные элементы массива Q "
<< Q[Q.length() / 2 - 1] << ' '
<< Q[Q.length() / 2] << endl; for (int i = 0; i < E.length(); ++i)
E[i] = i;
Возможно, операцию индексирования потребуется использовать в функциях, в которые объект класса Array передаётся как константный па-
34 раметр по ссылке. Например, функция printAndSum, которая выведет на экран содержимое нашего вектора и вычислит сумму его элементов. int printAndSum (const Array &v) { int sum=0; for (int i = 0; i < v.length(); i++){ cout << v[i] << ' '; sum+=v[i];
} return sum;
}
При компиляции этой функции возникнет ошибка
«
IntelliSense: отсутствует оператор "[]", соответствующий этим операндам: const Array[int]
».
Для корректной компиляции в данном случае необходимо определить вторую функцию перегрузки операции индексирования. int Array::operator [] (int i) const { return A[i];
}
Несмотря на то, что списки параметров совпадают у обеих операций индексации, перегрузка возможна, поскольку модификатор const включа- ется в сигнатуру функции.
Обратите внимание, что аналогичная ошибка возникала бы, если бы функция length не являлась константной. Это связано с тем, что для кон- стантных объектов можно вызывать только константные функции.
Иногда бывает необходимо, чтобы левосторонний операнд был объ- ектом другого класса или примитивного типа. Например, для часто пере- гружаемых операций потокового ввода-вывода левосторонним операндом должен быть объект-поток. Следовательно, перегрузку такой операции нужно организовывать в виде внешней функции. А внешним функциям за- прещен доступ напрямую к полям класса, объявленным как private.
Способ решения проблемы – объявить такую операцию дружественной для класса.
35
В примере 4 для класса Array реализована перегрузка операции << вывода в поток: ostream & operator << (ostream & out, const Array &B) { for (int i=0; i}
После выполнения всех действий с потоком ввода или вывода опера- ция возвращает ссылку на поток, что позволяет использовать результат в более сложных выражениях.
Дружественность функции operator<< по отношению к классу
Array дает право функции напрямую обращаться к закрытым членам класса.
Реализовать для класса Array операцию ввода из потока.
Всякий раз, когда нужно перегрузить бинарную операцию для опе- рандов двух разных типов с сохранением коммутативности, часто исполь- зуют дружественные функции.
Перегрузить операцию сложения, у которой левой операнд – целое число, а правый – объект класса Array.
Большинство операций можно перегружать и как внутренние функ- ции, и как внешние, используя дружественность. Однако некоторые опера- ции можно перегружать только одним из способов. Перегрузку только в виде внешней функции требуют все бинарные операции, у которых левый операнд является объектом другого класса или примитивного типа. Пере- грузку только в виде внутренней функции требуют следующие операции: присваивания =, индексирования [], преобразования типов type, вызова функции () и доступ к элементу через указатель >.