Читайте также:
|
|
Хотя один объект может быть присвоен другому объекту того же класса одним оператором присваивания, как мы упоминали выше, и этот оператор может создать только логическую копию (т.е., почленную копию). В логической копии член одного объекта просто принимает значение соответствующего члена другого объекта. Если, например, таким членом окажется указатель, то по-членное копирование приведет к тому, что у двух объектов будет указатель, указывающий на один и тот же объект. При таком по-членном копировании имеются две проблемы: Первая, так как указатели в двух объектах “разделяют” (share-совместно используют) одну и ту же переменную (имеется в виду объект, на который указывает указатель…), то изменение этой переменной изменяет оба объекта. Другими словами, объекты теряют независимость. Вторая проблема, если разделяемая переменная находится в куче (т.е. в динамически выделяемой памяти), то не известно, кто несет ответственность за освобождение памяти. Эта память либо не освободится, либо будет освобождена дважды, что не допустимо. Другая проблема возникает, когда указатель, член класса, указывает на область памяти в куче и, если этот указатель просто примет значение другого указателя, то “первоначальная” память не будет освобождена. Поэтому, когда в классе присутствует указатель на память в куче, нам, обычно, необходимо иметь перегруженный оператор присваивания для создания физической копии объекта.
Для перегрузки в классе оператора присваивания, обычно, нужно учитывать четыре фактора: Во-первых, мы должны проверить будет ли объект присваиваться сам себе. (Такое возможно, поскольку к переменной можно иметь доступ через разные имена). Если это – присваивание самому себе, то мы, просто, ничего не делаем с объектом. Если это не присваивание самому себе, то мы должны освободить память в куче, на которую указатели – члены объекта (левого) в текущее время указывают. Затем, мы копируем правый операнд в левый операнд. Наконец, мы возвращаем *this для того, чтобы обеспечить “цепочку” (“сцепление”) присваиваний. Возвращаемый тип делаем ссылкой на константный объект, так как мы хоти вернуть оригинальный (подлинник) объект (ссылку, &), но не хотим, чтобы он использовался как l-value (поэтому – модификатор const). Если возвращаемое значение – не константный объект, то мы могли бы писать выражения вида (x = y) = z. Это, в действительности, означало бы, что мы присваиваем значение z возвращаемому значению оператора (x = y). Другими словами, мы прсваиваем x = y, и затем присваиваем значение z объекту x опять, что не имеет особого смысла.
Функция перегрузки оператора присваивания должна быть методом класса.
Пример 11-2.2 Этот пример определяет Вторую версию класса Vector, который имеет перегруженные операции =, [ ] и <<.
#include <iostream.h>
class Vector
{
int *rep;
int size;
public:
Vector(int s, int an_array[ ]); // a constructor
~ Vector() {delete [ ] rep;} // a destructor
int get_size() const {return size;} // an accessor
const Vector& operator=(const Vector& x);
int& operator[ ](int index) {return rep[index];}
const int& operator[ ](int index) const {return rep[index];}
};
// Конструктор с инициализацией элементов из rep параметром an_array
Vector:: Vector(int s, int an_array[ ]):size(s), rep(new int[s])
{
for (int i = 0; i < size; ++i)
{ rep[i] = an_array[i]; }
}
// Заметим, что инициализатор использует new int[s] для инициализации rep,
// а не new int[ size ],
// так как инициализаторы не могут быть оценены при этом пордке их спецификации.
const Vector& Vector::operator=(const Vector& x)
{
size = x.size;
if (rep!= x.rep)
{
delete [ ] rep; // clean up the old one.
rep = new int[size];
for (int i = 0; i < size; ++i)
{ rep[i] = x.rep[i]; }
}
return *this;
}
ostream& operator<<(ostream& out, const Vector& x)
{
int s = x.get_size();
for (int i = 0; i < s; ++i)
{
out << x[i];
(i < s - 1)? (out << " "): (out << ".");
}
out << endl;
return out;
}
Обратим внимание, что в нашем классе вектор представлен динамическим массивом. Оператор присваивания, конструктор копирования, и деструктор – необходимы. В первой версии класса Vector, нам не нужны были эти функции, так как там физическая копия создавалась компилятором, если присвоение или копирование имело место.
Операции +=. -+, *=, и /= перегружаются таким же способом. Мы можем использовать эти операции как перегруженные арифметические операции. Например, можно реализовать операцию += в классе Complex следующим образом:
const Complex& Complex::operator+= (const Complex& c)
{
re += c.re;
im += c.im;
return *this;
}
Тогда операция + могла бы быть перегружена как:
const Complex operaotr+ (const Complex& c1, const Complex& c2)
{
Complex temp(c1);
temp += c2;
return temp;
}
Или, ее реализация в виде член- функции:
const Complex Complex::operator+(const Complex& c) const
{
Complex temp(*this);
temp += c;
return temp;
}
Перегрузка ++ и --
Перегрузка операций ++ и -- - это специальный случай, так как они могут использоваться как в префиксной, так и постфиксной форме. Перегружающие функции для этих операций, вообще говоря, не нуждаются в параметре. Но для того, чтобы отличать префиксную версию от постфиксной, вводится искусственный (dummy) параметр (обычно типа int) в постфиксную версию.
Пример 11-3.1
class Date { public: Date& operator++(); //prefix Date& operator--(); //prefix Date& operator++(int unused); // postfix Date& operator--(int unused); // postfix }; void f() { Date d, d1; d 1 = ++ d ;// prefix : сначала увеличиваем d , а затем присваиваем переменной d 1 d 1 = d ++; // postfix ; сначала присваиваем, а потом увеличиваем d}
Пример 11-3.4. Класс Fraction - “Дробь”
class Fraction
{
int den; // знаменатель
int num; // числитель;
public:
Fraction(int d = 1, int n = 0):den(d),num(n){ } // конструктор
Fraction& operator ++() // the prefix version
{
num += den;
return *this;
}
const Fraction operator ++(int) // the postfix version
{
Fraction old (*this); // Локальная переменная: сначала инициализир. Затем инкремент.
++(*this);
return old;
}
};
Тогда мы можем писать
Fraction x(2, 3), y(3, 4);
++x; // вызов operator++()
y++; // вызов operator++(int)
Объяснение.
(i) Префиксная версия возвращает по ссылке, а постфиксная – по значению, так как old есть локальная переменная. Возвращаемое значение из постфиксной версии есть константа, так как следует запретить такие выражения как y++++, поскольку y++++, в действительности, не увеличивает y на 2 (в нашем случае, компилятор сообщит об ошибке). Вторая операция ++ увеличивала бы на 1 возвращенный объект old (временную переменную).
(ii) Постфиксная версия реализована с помощью префиксной версии. Это делает эти две версии “совместимыми”, т.е., для переменной, являющейся частью некоторого выражения, префиксная и постфиксная версии изменяют переменную в точности одним и тем же способом. Если Вы хотите изменить поведение операции ++, Вам нужно изменить только префиксную версию. Постфиксная версия автоматически изменит свое поведение.
(iii) Так как постфиксная версия реализована посредством префиксной, то там, где они по действию эквивалентны, будет более эффективным применять префиксную вместо постфиксной.
(iv) Постфиксная версия использует искусственный параметр, который, на самом деле, не имеет для операции никакого значения. Для того, чтобы компилятор не “беспокоился”, этот идентификатор опускается.
Перегрузка операции -- выполняется похожим образом.
Перегрузка операции ==
Во многих случаях, у нас возникает необходимость в сравнении на равенство двух объектов. Например, при поиске в массиве объектов бывает нужно найти конкретный элемент этого массива. В таком случае нам нужно определить, что означает “равенство” двух объектов.
Следующая функция определяется для сравнения двух объектов класса Vector. В данном случае эта функция не является ни член-функцией, ни дружественной.
Пример 11-5.1
bool operator==(const Vector& a, const Vector& b)
{
bool yes = true;
if (a.get_size()!= b.get_size())
{ yes = false; }
else
{
int index = 0;
int s = a.get_size();
while (index < s && a[index] == b[index])
{ ++index; }
if (index < s)
{ yes = false; }
}
return yes;
}
Подобным же способом мы можем перегрузить операции сравнения >, <, <= и >=.
àЗамечание. Ранее в лекции 10 мы упомянули о так называемых функторах (объектах, имеющих метод, перегружающий операцию вызова функции). Поскольку изучение STL выходит за рамки данного курса (а именно в контексте STL часто говорят об использовании функциональных объектов или функторов), то мы не будем обсуждать эту тему подробно, а приведем простейшие примеры, показывающие, как выглядит перегрузка операции ().
Итак, функтор – любой объект, который может использоваться с применением семантики вызова функции. Сюда включаются применение обычных имен функций, указателей на функции и объекты классов, для которых перегружена операция (), т.е., определена функция-оператор “странного” вида operator ()() . Например:
class Linear{private: double slope; double y0;public: Linear(double _sl = 1, double _y = 0): slope(_sl), y0(_y) {} double operator()(double x) {return y0 + slope * x; }};Перегруженная операция () позволяет, тогда, использовать объекты типа Linear как функции:
Linear f1;Linear f2(2.5, 10.0);double y1 = f1(12.5); // справа имеем вызов f1.operator()(12.5)double y2 = f2(0.4);
И еще один простой пример:
#include <string>
using namespace std;
class FunctionObject
{
public:
int operator() (int inParam); // операция вызова функции
void operator() (string& str) {}
int aMethod(int inParam); // обычный метод
};
//Реализация пергруженной операции вызова функции
int FunctionObject::operator() (int inParam)
{
return (inParam * inParam);
}
// Реализация обычного метода
int FunctionObject::aMethod(int inParam)
{
return (inParam * inParam);
}
int main(int argc, char** argv)
{
int x = 3, xSquared, xSquaredAgain;
FunctionObject square;
xSquared = square(x); // Вызов операции вызова функции
xSquaredAgain = square.aMethod(x); // вызов обычного метода для объекта square
return (0);
}
à
Дата добавления: 2015-11-16; просмотров: 52 | Нарушение авторских прав
<== предыдущая страница | | | следующая страница ==> |
Перегрузка операции индексации | | | Общий механизм обработки исключений. |