Главная страница
Навигация по странице:

  • 1.4. Перегрузка операций

  • Пособие по ооп. С++ ООП УчебноеПособие. Объектноориентированное


    Скачать 1.49 Mb.
    НазваниеОбъектноориентированное
    АнкорПособие по ооп
    Дата23.11.2021
    Размер1.49 Mb.
    Формат файлаpdf
    Имя файлаС++ ООП УчебноеПособие.pdf
    ТипУчебное пособие
    #279608
    страница2 из 7
    1   2   3   4   5   6   7

    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, вызова функции () и доступ к элементу через указатель >.

    36
    1   2   3   4   5   6   7


    написать администратору сайта