Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

Подписываем
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Предоплата всего
Подписываем
Вопрос 15
В этой главе описывается аппарат, предоставляемый в C++ для перегрузки операций. Программист может определять смысл операций при их применении к объектам определенного класса. Кроме арифметических, можно определять еще и логические операции, операции сравнения, вызова () и индексирования [], а также можно переопределять присваивание и инициализацию. Можно определить явное и неявное преобразование между определяемыми пользователем и основными типами. Показано, как определить класс, объект которого не может быть никак иначе скопирован или уничтожен кроме как специальными определенными пользователем функциями.
6.1 Введение
Часто программы работают с объектами, которые являются конкретными представлениями абстрактных понятий. Например, тип данных int в C++ вместе с операциями +, -, *, / и т.д. предоставляет реализацию (ограниченную) математического понятия целых чисел. Такие понятия обычно включают в себя множество операций, которые кратко, удобно и привычно представляют основные действия над объектами. К сожалению, язык программирования может непосредственно поддерживать лишь очень малое число таких понятий. Например, такие понятия, как комплексная арифметика, матричная алгебра, логические сигналы и строки не получили прямой поддержки в C++. Классы дают средство спецификации в C++ представления неэлементарных объектов вместе с множеством действий, которые могут над этими объектами выполняться. Иногда определение того, как действуют операции на объекты классов, позволяет программисту обеспечить более общепринятую и удобную запись для манипуляции объектами классов, чем та, которую можно достичь используя лишь основную функциональную запись. Например:
class complex {
double re, im;
public:
complex(double r, double i) { re=r; im=i; }
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};
определяет простую реализацию понятия комплексного числа, в которой число представляется парой чисел с плавающей точкой двойной точности, работа с которыми осуществляется посредством операций + и * (и только). Программист задает смысл операций + и * с помощью определения функций с именами operator+ и operator*. Если, например, даны b и c типа complex, то b+c означает (по определению) operator+(b,c). Теперь есть возможность приблизить общепринятую интерпретацию комплексных выражений. Например:
void f()
{
complex a = complex(1, 3.1);
complex b = complex(1.2, 2);
complex c = b;
a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}
Перегрузка операций была реализована в языке прежде всего для того, чтобы вы могли интегрировать разработанный вами арифметический тип в существующую арифметическую систему языка С. Этот механизм никогда не предназначался в качестве средства расширения этой системы. Следовательно, перегрузку операций лучше применять, лишь используя классы для реализации арифметического типа.
Тем не менее, также разумно использовать перегруженные операции и там, где аналогии с С незаметны. Например, большинство классов будет перегружать присваивание. Перегрузка operator==() и operator!=() также разумна в большинстве классов.
Менее ясным (и более противоречивым) примером является класс "итератор". Итератор является средством просмотра каждого члена структуры данных, и он используется почти точно так же, как если бы он был указателем на массив. Например, вы можете в С итерировать массив, просматривая каждый элемент, следующим образом:
string array[ size ];
string *p = array;
for( int i = size; --i >= 0 ; )
visit( *p++ ); // функции visit() передается строка.
Аналог в С++ может выглядеть вот так (keys является деревом, чьи узлы имеют строковые ключи; здесь могут быть любые другие структуры данных):
tree<string> keys; // двоичное дерево с узлами, имеющими строковые ключи
iterator p = keys;
// ...
for( int i = keys.size(); --i >= 0 ; )
visit( *p++ ); // функции visit() передается строка.
Другими словами, вы обращаетесь с деревом как с массивом, и можете итерировать его при помощи итератора, действующего как указатель на элемент. И так как iterator(p) ведет себя точно как указатель в С, то правило "без сюрпризов" не нарушается.
147. Перегрузив одну операцию, вы должны перегрузить все сходные с ней операции.
Это правило является продолжением предыдущего. После того, как вы сказали, что "итератор работает совсем подобно указателю", он на самом деле должен так работать. Пример в предыдущем правиле использовал лишь перегруженные * и ++, но моя настоящая реализация итератора делает аналогию полной, поддерживая все операции с указателями. Таблица 4 показывает различные возможности (t является деревом, а ti - итератором для дерева). Обе операции *++p и *p++ должны работать и т.д. В предыдущем примере я бы должен был также перегрузить в классе tree операции operator[] и (унарная)operator*() для того, чтобы аналогия дерева с массивом выдерживалась везде. Вы уловили эту мысль.
Таблица 4. Перегрузка операторов в итераторе.
Операция Описание
ti = t; Возврат к началу последовательности
--ti; Возврат к предыдущему элементу
ti += i; Переместить вперед на i элементов
ti -= i; Переместить назад на i элементов
ti + i;
ti - i; Присваивает итератору другой временной переменной значение с указанным смещением от ti
ti[i]; Элемент со смещением i от текущей позиции
ti[-i]; Элемент со смещением -i от текущей позиции
t2 = ti; Скопировать позицию из одного итератора в другой
t2 - ti; Расстояние между двумя элементами, адресуемыми различными итераторами
ti->msg(); Послать сообщение этому элементу
(*ti).msg(); Послать сообщение этому элементу
Одна из проблем здесь связана с операциями operator==() и operator!=(), которые при первом взгляде кажутся имеющими смысл в ситуациях, где другие операции сравнения бессмысленны. Например, вы можете использовать == для проверки двух окружностей на равенство, но означает ли равенство "одинаковые координаты и одинаковый радиус", или просто "одинаковый радиус"? Перегрузка других операций сравнения типа < или <= еще более сомнительна, потому что их значение не совсем очевидно. Лучше полностью избегать перегрузки операций, если есть какая-либо неясность в их значении.
148. Перегруженные операции должны работать точно так же, как они работают в С.
Главной новой проблемой здесь являются адресные типы lvalue и rvalue. Выражения типа lvalue легко описываются в терминах С++: они являются просто ссылками. Компилятор С, вычисляя выражение, выполняет операции по одной за раз в порядке, определяемом правилами сочетательности и старшинства операций. Каждый этап в вычислениях использует временную переменную, полученную при предыдущей операции. Некоторые операции генерируют "rvalue" - действительные объекты, на самом деле содержащие значение. Другие операции создают "lvalue" - ссылки на объекты. (Кстати, "l" и "r" используются потому, что в выражении l=r слева от = генерируется тип lvalue. Справа образуется тип rvalue).
• Операции присваивания (=, +=, -= и т.д.) и операции автоинкремента и автодекремента (++, --) требуют операндов типа lvalue для адресата - части, которая изменяется. Представьте ++ как эквивалент для +=1, чтобы понять, почему эта операция в той же категории, что и присваивание.
В перегруженных операциях функций-членов указатель this на самом деле является lvalue, поэтому здесь не о чем беспокоиться. На глобальном уровне левый операнд перегруженной бинарной операции присваивания (и единственный операнд перегруженной унарной операции присваивания) должен быть ссылкой.
• Все другие операции могут иметь операнды как типа lvalue, так и rvalue.
Используйте ссылку на объект типа const для всех операндов. (Вы могли бы передавать операторы по значению, но обычно это менее эффективно).
• Имена переменных составного типа (массивов) создают типы rvalue - временные переменные типа указателя на первый элемент, после инициализации на него и указывающие. Заметьте, что неверно представление о том, что вы не можете инкрементировать имя массива из-за того, что оно является константой. Вы не можете инкрементировать имя массива, потому что оно имеет тип rvalue, а все операции инкремента требуют операндов типа lvalue.
• Имена переменных несоставного типа дают lvalue.
• Операции *, -> и [] генерируют lvalue, когда относятся к несоставной переменной, иначе они работают подобно именам составных переменных. Если y не является массивом, то x->y создает тип lvalue, который ссылается на этого поле данных. Если y - массив, то x->y генерирует тип rvalue, который ссылается на первую ячейку этого массива.
В С++ перегруженные * и [] должны возвращать ссылки на указанный объект. Операция operator-> таинственна. Правила по существу заставляют вас использовать ее таким же образом, как вы делали бы это в С. Операция -> рассматривается как унарная с операндом слева от нее. Перегруженная функция должна возвращать указатель на что-нибудь, имеющее поля -, структуру класс или объединение. Компилятор будет затем использовать такое поле для получения lvalue или rvalue. Вы не можете перегрузить .(точку).
• Все другие операнды генерируют тип rvalue.
Эквивалентные перегруженные операции должны возвращать объекты, а не ссылки или указатели.
В языке C# как и в C++ можно при желании перегружать операции для работы с объектами своих классов. Перегрузка операций позволяет получать более естественный и лучше читаемый текст программы. Перегрузка операций, как и перегрузка методов, является одной из форм полиморфизма.
Перегружать можно одноместные и двухместные операции.
При перегрузке операций в C# существует ряд ограничений:
<= и >=
< и >
== и !=
Теперь рассмотрим ряд примеров по перегрузке операций. Для большей конкретности создадим какой-нибудь класс, например, класс, моделирующий обыкновенные дроби. Вот как может выглядеть начало этого класса:
public class Drobi
{
int a; // Числитель
int b; // Знаменатель
// Конструкторы и другие методы
...............................
}
В языке C# можно перегрузить следующие одноместные операции:
+ (унарный плюс) - (унарный минус)
! ~ ++ -- true false
Формально перегрузка операции записывается таким образом:
public static Тип_результата operator Знак_операции (Формальный_Параметр)
{
// тело метода, реализующего перегрузку операции
}
Как видим, метод, перегружающий операцию, всегда объявляется открытым (public) и статичным (static). Признаком того, что делается именно перегрузка операции, служит ключевое слово operator, за которым должен располагаться знак перегружаемой операции.
В качестве формального параметра при перегрузке одноместной операции может выступать только объект того же типа (класса), что и класс, для которого мы делаем перегрузку операции. Формальный параметр передаётся только по значению.
Приведём пару примеров перегрузки одноместных операций для нашего класса Drobi.
Пример 1. Перегрузка операции - (смена знака):
public static Drobi operator - (Drobi x)
{
Drobi t = new Drobi();
t.a = - x.a;
t.b = x.b;
return t;
}
Здесь в методе создаётся новый объект t типа Drobi и затем вычисляются значения полей этого объекта. Метод возвращает результат переменную t типа Drobi, исходный объект остаётся неизменным. Пример использования операции:
Drobi x = new Drobi(2,5);
Drobi y = new Drobi();
y = -x;
После этих действий переменная y будет иметь значение -2/5.
Пример 2. Перегрузка операции ++ (автоувеличение):
public static Drobi operator ++ (Drobi x)
{
Drobi t = new Drobi();
t.a = x.a + x.b;
t.b = x.b;
return t;
}
Пример использования операции автоувеличения:
y++;
Если до этого y было равно -2/5, то теперь y примет значение 3/5.
Замечание. Операции автоувеличения (++) и автоуменьшения (--) имеют две формы: префиксную (например, ++i) и постфиксную (например, i++). Но при перегрузке этих операций (инкремент и декремент) обе формы вызывают один и тот же метод. Поэтому перегружаем метод один раз, а затем используем в любой из форм. Хотя, если посмотреть на нашу реализацию перегрузки операции автоувеличения, то по характеру действий в методе нетрудно понять, что это метод для префиксной формы записи.
В языке C# можно перегрузить следующие двухместные операции:
+ - * / % & | ^ << >> == != < > >= <=
Формально перегрузка двухместной операции записывается таким образом:
public static Тип_результата operator Знак_операции (Формальный_Параметр1, Формальный_Параметр2)
{
// тело метода, реализующего перегрузку операции
}
Любой метод, реализующий перегрузку двухместной операции, также является открытым и статичным. При этом хотя бы один из двух формальных параметров должен иметь тип класса, для которого делается эта перегрузка.
Пример 3. Перегрузка операции сложения +:
public static Drobi operator + (Drobi x, Drobi y)
{
int t1 = x.a * y.b + x.b * y.a;
int t2 = x.b * y.b;
socrat(ref t1, ref t2);
return new Drobi(t1, t2);
}
Переменные целого типа t1 и t2 это значения числителя и знаменателя дроби, которая является суммой исходных дробей x и y.
Так как при выполнении ряда операций, например, сложения, могут получаться дроби, в которых возможно сокращение, вызываем закрытый статический метод socrat() (пример реализации можно посмотреть в полном тексте программы). Этот метод по сокращению дроби приходится делать статическим, потому-что из статического метода (а метод, перегружающий сложение, обязан быть статическим!) можно вызвать тоже только статический метод.
В конце метода создаём безымянный объект типа Drobi, который и будет результатом работы метода.
Пример использования операции сложения (здесь x и y объекты класса Drobi):
Drobi z = new Drobi();
z = x + y;
Хотя операции сравнения и являются двухместными, (а перегрузку двухместных операций мы уже рассмотрели!) но для операций сравнения имеется ряд особенностей, в которых необходимо разобраться.
Во-первых, операции сравнения необходимо реализовывать парами (см. выше), а во-вторых при этом надо перегрузить методы Equals() и GetHashCode().
Метод Equals() позволяет сравнивать два объекта, а метод GetHashCode() возвращает так называемый хеш-код объекта, который однозначно идентифицирует каждый объект класса и используется для быстрого поиска объектов по ключу.
Методы Equals() и GetHashCode() взаимосвязаны, т.е. если метод Equals() определяет два объекта одинаковыми, то метод GetHashCode() для обоих объектов будет выдавать одинаковый хеш-код.
Пример 4. Перегрузка операций > (больше) и < (меньше) и возможная реализация методов Equals() и GetHashCode():
public static bool operator > (Drobi x, Drobi y)
{
if(x.a * y.b > x.b * y.a)
return true;
else
return false;
}
public static bool operator < (Drobi x, Drobi y)
{
if(x.a * y.b < x.b * y.a)
return true;
else
return false;
}
public override bool Equals(object o)
{
if(o.GetType() != GetType())
return false;
Drobi t = (Drobi) o;
return (a == t.a && b == t.b);
}
public override int GetHashCode()
{
return 0;
}
Пример использования перегрузки операций сравнения (здесь x и y объекты класса Drobi):
if(x > y)
Console.WriteLine("Да, x > y");
else
Console.WriteLine("Нет, x не больше y");