Будь умным!


У вас вопросы?
У нас ответы:) SamZan.net

Программист может определять смысл операций при их применении к объектам определенного класса.

Работа добавлена на сайт samzan.net: 2016-03-13

Поможем написать учебную работу

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

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

от 25%

Подписываем

договор

Выберите тип работы:

Скидка 25% при заказе до 21.5.2024

Вопрос 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# существует ряд ограничений:

  1.  нельзя придумать свои знаки операций;
  2.  нельзя изменить приоритет операции, например, если для чисел приоритет операции умножения (*) выше, чем сложения (+), то и в классе пользователя при перегрузке этих операций сохраняется то же старшинство действий;
  3.  метод, реализующий перегрузку какой-либо операции, должен быть статическим и открытым;
  4.  параметры можно передавать в метод для реализации операции только по значению (ref и out запрещены);
  5.  нельзя перегружать ни какие формы операции присваивания (=+= и т.д.);
  6.  операции сравнения необходимо реализовывать парами (симметричными по смыслу):

<=  и  >=

<   и  >

==  и  !=

  1.  если перегружаются операции сравнения, то необходимо также перегрузить два метода, наследуемые из класса objectEquals() и GetHashCode().

Теперь рассмотрим ряд примеров по перегрузке операций. Для большей конкретности создадим какой-нибудь класс, например, класс, моделирующий обыкновенные дроби. Вот как может выглядеть начало этого класса:

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");





1.  2013г
2. Тема Статистика уровня жизни 1
3. 30.10.1934 выдающийся русский советский педагог.html
4. Учебная история болезни
5. Плоттеры нового поколения.html
6. Тема 4 Культура общения в коллективе Умение общаться с людьми такой же покупаемый за деньги товар как
7. Лабораторная работа 1 rсм
8. либо по телефону либо участвуя в совещаниях и заседаниях мы не отдавая себе отчёта частенько выводим на ли
9. Тема- Тема- Художественные направления течения и школы в литературе ХХ века
10. Лабораторная работа 2 ldquo;Демонстрация уравнения Бернуллиrdquo; Студент- Кузин А
11. за этих своих свойств отец долго не уживался на одном месте
12. тематично подаються дробоволінійними функціями
13. Исследование НДС фрагмента плиты перекрытия в здании детского сада на 120 мест
14. а й неортодоксальні локаятачарвака джайнізм буддизм філософські вчення
15. Лекция 10 ПИЩЕВАЯ АДДИКЦИЯ В экономически развитых странах проблема избыточного веса и ожирение в посл
16. на тему- АУДИТ РАСЧЕТОВ С ПОДОТЧЕТНЫМИ ЛИЦАМИ
17. тематизированные знания и именно в ней должны даваться знания о своем крае прививаться соответственное отно
18. Социально-психологические аспекты управления трудовым коллективом
19. КАЛИЙ Химические и физические свойства
20. Вероятностные или статистические законы