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


Содержание

Основы перегрузки операторов

C# — Руководство по C# — Основы перегрузки операторов

В C#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму:

Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

По сути, функциональность операции + уникальным образом базируются на представленных типах данных (строках или целых в данном случае). Когда операция + применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк.

Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

Операция C# Возможность перегрузки
+, -, !, ++, —, true, false Этот набор унарных операций может быть перегружен
+, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
==, !=, = Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки «подобных» операций (т.е. , =, == и !=)
[] Операция [] не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
() Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово operator, определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса. Существуют две формы операторных методов (operator): одна — для унарных операторов, другая — для бинарных. Ниже приведена общая форма для каждой разновидности этих методов:

Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2. Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа — public и static.

Перегрузка бинарных операторов

Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

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

В C++ можно перегружать не только функции, но и операции. Таким способом гораздо удобнее вносить какие-либо действия привычным методом записи. Например, можно организовать перемножение матриц с помощью * или сложение комплексных чисел с помощью +.

Для перегрузки операций существует специальное ключевое слово оператор. Рассмотрим конкретный пример

В данном примере мы перегружаем оператор +, заставляя его складывать 2 переменных класса MyClass заданным нами образом. В итоге, если в классе встретится конструкция a+b, где a и b – экземпляры MyClass, то операция будет совершаться, как показано выше в примере. Во всех остальных случаях оператор + будет выполняться обычным образом.

На такую перегрузку действует ряд ограничений :

  1. Перегружать можно только уже используемые операторы. Нельзя ввести новые.
  2. Нельзя перегрузить операторы для уже существующих типов данных, так, например, нельзя перегрузить сложение целых чисел.
  3. Нельзя изменять синтаксис операций – из унарной делать бинарную, менять место оператора в выражении.
  4. Ряд символов перегрузить нельзя: . , .* , ?: , :: , sizeof и #.

Похожие записи:

Перегрузка операций: 2 комментария

А что насчет перегрузки операторов ввода/вывода? Я сейчас изучаю раздел о перегрузке сin и cout. Не очень понятно, как это работает, если использовать в перегруженной операции объекты ostream и istream (передача параметров — по ссылке).

Для этого используют друзей. В классе пишут:
friend ostream& operator >(istream& cin, MyClass& MyClass);
и пишут вне классов глобально:

Перегрузка операций. Перегрузка методов – это один из способов, которым достигается полиморфизм в языке С#

Читайте также:

  1. C.2 Примеры операций
  2. IV. По характеру операций
  3. VIII. Счет операций с капиталом
  4. А кассовых операций
  5. Автоматизированный расчет стоимости транзитных операций.
  6. Алгоритмы выполнения реляционных операций
  7. Аудит и ревизия денежных средств (ДС) кассовых и банковских операций.
  8. Аудит кассовых операций
  9. Аудит кассовых операций
  10. Аудит кассовых операций.
  11. Аудит кассовых операций.
  12. Аудит операций по валютным счетам

Перегрузка методов – это один из способов, которым достигается полиморфизм в языке С#. Две и более функции могут иметь одно и то же имя, а отличаться набором аргументов в интерфейсе (описании).

Перегрузка методов

При определении методов какого-нибудь класса в программах необходимо указать тип возвращаемого методом значения, а также количество параметров и тип каждого из них.

Например, программистом была разработана функция с именем sum(), которая суммировала два целых значения. Если требуется использовать подобную функцию для сложения трех целых значений, следует создать функцию с другим именем. Аналогично если требуется использовать подобную функцию для сложения значений типа float, то необходимо еще одна функция с еще одним именем.

Чтобы избежать дублирования функций, язык C# позволяет определять несколько функций с одним и тем же именем. В процессе компиляции C# принимает во внимание количество аргументов, используемых каждой функцией, и затем вызывает именно требуемую функцию. Предоставление компилятору выбора среди нескольких функций называется перегрузкой. Перегрузка является одним из способов реализации полиморфизма.

Перегрузка методов позволяет использовать одно и то же имя для нескольких функций с разным количеством или разным типом параметров.

Полиморфизм – позволяет использовать один и тот же интерфейс при реализации целого круга различных действий.

Наиболее распространенным видом перегрузки методов является перегрузка конструкторов в классе.

Как известно, в языке С# тип переменной определяет набор значений, которые она может хранить, а также набор операций, которые можно выполнять над этой переменной. Например, над значением переменной типа int программа может выполнять сложение, вычитание, умножение и деление. С другой стороны, использование оператора “плюс” для сложения двух экземпляров реализованного программистом класса лишено смысла.

Когда в программе определяется класс, то по существу определяется новый тип данных. Тогда язык C# позволяет определить операции, соответствующие этому новому типу данных.

Перегрузка операций состоит в изменении смысла операции при использовании его с определенным классом.

Например, пусть имеется:

myclass a,bc;…//a,b,c-экземпляры класса myclass

c=a+b; //перегруженная операция сложения для класса myclass

Перегрузка операций обычно применяется для классов, описывающих математические или физические понятия, то есть таких классов, для которых требуется выполнить соответствующие операции.

Общий синтаксис объявления перегруженной операции:

[атрибуты] спецификаторы operator тело операции,

operator – ключевое слово, определяющее перегруженную операцию

тело операции-действия, которые выполняются при использовании операции в выражении

Перегружать можно только стандартные операции.

Алгоритм перегрузки операции:

1. Определить класс, которому данная операция будет назначена.

2. Для перегрузки операций используется ключевое слово operator.

3. Переопределяя операцию, необходимо указать метод, который C# вызывает каждый раз, когда класс использует перегруженную операцию. Этот метод, в свою очередь, выполняет соответствующую операцию.

Правила перегрузки операции:

1. Операция должна быть объявлена как public static

2. Параметры в операцию должны передаваться по значению (не ref, не out)


3. Двух одинаковых перегруженных операций в классе не должно быть

Если программа перегружает операцию для определенного класса, то смысл этой операции изменяется только для указанного класса, оставшаяся часть программы будет продолжать использовать эту операцию для выполнения ее стандартных действий.

| следующая лекция ==>
Диа- и парамагнетики. Ферромагнетики | Перегрузка бинарных операций

Дата добавления: 2014-01-20 ; Просмотров: 281 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

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

Задания для лабораторной работы № 7

// overloaded ‘+’ operator adds two Distances

using namespace std;

class Distance //English Distance class

public: //constructor (no args)

Distance() : feet(0), inches(0.0)

< >//constructor (two args)

Distance(int ft, float in) : feet(ft), inches(in)

void getdist() //get length from user

void showdist() const //display distance

i -= 12.0; //by 12.0 and

f++; //increase feet by 1

> //return a temporary Distance

return Distance(f,i); //initialized to sum

Distance dist1, dist3, dist4; //define distances

dist1.getdist(); //get dist1 from user

Distance dist2(11, 6.25); //define, initialize dist2

dist3 = dist1 + dist2; //single ‘+’ operator

dist4 = dist1 + dist2 + dist3; //multiple ‘+’ operators

//display all lengths

using namespace std;

class Distance //English Distance class

public: //constructor (no args)

Distance() : feet(0), inches(0.0)

< >//constructor (two args)

Distance(int ft, float in) : feet(ft), inches(in)

void getdist() //get length from user

void showdist() //display distance

i -= 12.0; //by 12.0 and

f++; //increase feet by 1

> //return a temporary Distance

return Distance(f,i); //initialized to sum

//subtract d2 from this dist

Distance Distance::operator — (Distance d2) //return the diff

int f = feet — d2.feet; //subtract the feet

float i = inches — d2.inches; //subtract the inches

return Distance(f,i); //initialized to difference

Distance dist1, dist3; //define distances

dist1.getdist(); //get dist1 from user

Distance dist2(3, 6.25); //define, initialize dist2

dist3 = dist1 — dist2; //subtract

//display all lengths

using namespace std;

#include //for strcpy(), strcat()

#include //for exit()


class String //user-defined string type

enum < SZ=80 >; //size of String objects

char str[SZ]; //holds a string

String() //constructor, no args

String( char s[] ) //constructor, one arg

void display() const //display the String

#include //for strcpy(), strlen()

using namespace std;

class String //user-defined string type

enum < SZ = 80 >; //size of String objects

char str[SZ]; //holds a C-string

String() //no-arg constructor

String( char s[] ) //1-arg constructor

void display() //display the String

using namespace std;

int hrs, mins, secs;

time() : hrs(0), mins(0), secs(0) //no-arg constructor

time(int h, int m, int s) : hrs(h), mins(m), secs(s)

void display() //format 11:59:59

if( m > 59 ) //if mins overflow,

return time(h, m, s); //return temp value

time time1(5, 59, 59); //create and initialze

time time2(4, 30, 30); // two times

time time3; //create another time

time3 = time1 + time2; //add two times

using namespace std;

Int() : i(0) //no-arg constructor

Int(int ii) : i(ii) //1-arg constructor

void putInt() //display Int

operator int() //conversion operator

Int operator + (Int i2) //addition

Int operator — (Int i2) //subtraction

Int operator * (Int i2) //multiplication

Int operator / (Int i2) //division

Int checkit(long double answer) //check results

if( answer > 2147483647.0L || answer

using namespace std;

const int LIMIT = 100; //array size

int& operator [](int n) //note: return by reference

Не нашли то, что искали? Воспользуйтесь поиском:

Лучшие изречения: Только сон приблежает студента к концу лекции. А чужой храп его отдаляет. 8808 — | 7523 — или читать все.

188.64.174.135 © studopedia.ru Не является автором материалов, которые размещены. Но предоставляет возможность бесплатного использования. Есть нарушение авторского права? Напишите нам | Обратная связь.

Отключите adBlock!
и обновите страницу (F5)

очень нужно

§20 ООП. Перегрузка операций. Друзья класса. Указатель this. Деструктор

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

В 10 классе мы с вами познакомились с понятием перегрузка функции. Суть его в том, что функция, в зависимости от сигнатуры (количество и типы аргументов), может вести себя по разному. Такое явление мы назвали функциональным полиморфизмом. В C++ полиморфизм присущ и операциям. Например, в зависимости от количества и типов аргументов, операции «*» , «-» , «&» , «>>» будут выполнять совершенные разные действия. С++ позволяет использовать перегрузку операций и для абстрактных типов. Это не должно являться для вас большой неожиданностью, так как мы не могли пройти мимо понятия функтора, а для его создания использовалась перегрузка операции – вызова функции () . Для реализации перегрузки операции в классе разработчика используется специальный метод, заголовок которого имеет следующий синтаксис:

где символ операции – это обозначение любой перегружаемой операции.
Для чего это нужно? Все очень просто. Используя хорошо знакомые нам операции мы можем придать нашему коду более естественный и понятный вид. В тоже время скрыть (инкапсулировать) реализацию сложного алгоритма внутри класса. Как вы знаете в C++ такая операция невозможна:

Получаем ошибку: no match for «operator*». . Однако, мы можем описать класс в котором такая операция будет выполняться. Для этого применяется перегруженная операция «*» . Обратите внимание, что порядок операндов здесь будет важен. Так будет правильно: myStr * 5 , а так – будет ошибка: 5 * myStr (о передаче первого операнда неявно см. ниже).

На перегрузку операций накладываются некоторые ограничения. Так нельзя перегружать следующие операции: «sizeof» , «.» , «.*» , «::» , «:?» (тернарная), операции приведения типа. Не допускается изменение приоритетов операций. Можно использовать лишь те символы операций, которые известны в C++. Нельзя нарушать правила синтаксиса исходной операции.
Поскольку операции подразделяются на две категории унарные и бинарные рассмотрим примеры использования тех и других.

Бинарные операции

Бинарная операция требует наличия двух операндов. Синтаксис метода перегрузки бинарной операции имеет вид:

Илон Маск рекомендует:  Что такое код extfloodfill


В программе ext_20.1 нетрудно заметить, что метод перегруженной операции имеет только один параметр. Все верно! Дело в том, что первый операнд передается неявно как вызывающий объект. Часто перегруженные операции не являются членами класса, а являются дружественными функциями (друзья класса). В этом случае, количество операндов должно точно соответствовать типу операции (для бинарной их должно быть два). Для того, чтобы функцию определить как друг класса необходимо использовать спецификатор доступа friend .
Создадим класс в котором перегружаются операции , >> (вставки и извлечения). Справедливости ради заметим, что эти потоковые функции также являются перегруженными операциями побитового сдвига. Итак, нам нужны эти операции для тех же целей, что и для обычных переменных — для ввода и вывода. Адаптируем эти операции для ввода и вывода массива vector .

Почему перегрузки функций ввода/вывода необходимо объявлять как друзья класса? Причина заключается в том, что функции библиотеки iostream (как и любой другой библиотеки STD) не могут быть членами классов создаваемых разработчиком. В тоже время, нельзя добавлять свои методы в библиотечные классы. Но мы можем создавать объекты библиотечных классов в методах своих классов, сделав их дружественными.
В программе 29.2 первым параметром выступает объект класса iostream . Второй аргумент — это объект нашего класса Boo . В циклах формируются соответствующие потоки, которые по окончанию работы циклов, передаются как ссылки. В основной программе эти ссылки становятся аргументами функций стандартных потоковых объектов.

Указатель this

В нестатических функциях-членах класса (а также в struct или union ) доступен указатель this . Он указывает на объект, для которого вызывается функция-член. Поскольку this — указатель, то при обращении к полям класса применяется операция «стрелка» — » -> «.

В программе ext_20.3 приводятся три равносильных варианта одной и той же инструкции. Становится очевидным, что указатель this используется здесь неявно. Также неявно передается this когда один метод класса вызывает другой. (Неявно передавался указатель this и в программе ext_20.1, мы обратили на это внимание). В каких случаях этот указатель можно применить с пользой? Пример такой программы мы составим ниже.

Унарные операции

Унарные операции имеют только один операнд. Синтаксис перегрузки унарной операции имеет следующий вид:

Реализуем перегрузки двух операций постфиксного инкремента operator++ и разыменования operator* . Реализация перегрузки постфиксного и префиксного инкремента различается по существу самой операции. В первом случае возвращается сохраненное состояние объекта ( *this ). Во втором сам объект *this . (Нужно сказать, что и в первом, и во втором случаях можно обойтись и без указателя this ; в комментариях мы это показали). Что конкретно будет инкрементироваться — зависит от поставленной задачи. В нашей новой программе мы создадим свой «умный указатель» и будем инкрементировать этот указатель, перемещаясь по массиву. Поскольку для вывода элементов (а не адресов) нам нужна операция разыменования, то мы перегрузим и её.

Конечно, этот указатель можно с большой натяжкой назвать «умным». Ведь кроме скрытой реализации двух перегрузок в нем больше ничего нет. Но функционал можно и нарастить. Так, например, можно добавить исключения проверяющие выход за границы массива и проверку на валидность элементов.
Реализация префиксного инкремента (или декремента) еще проще:

Деструкторы

Деструктор — это специальный метод класса предназначенный для удаления объекта. Как конструктор автоматически вызывается при создании объекта, так и деструктор автоматически вызывается для его уничтожения. Как конструктор, так и деструктор имеет форму по умолчанию. Имя деструктора тоже совпадает с именем класса, но предваряется символом «

Деструктор применяется явно тогда, когда необходимо освободить ресурсы с помощью инструкции delete , выделенные с помощью инструкции new . Для классов контейнеров существуют встроенные деструкторы, так что их вызывать намеренно не обязательно (но не запрещено). Если есть необходимость удалить какой-либо объект контейнера до момента его автоматического удаления (когда он выйдет из области видимости), то деструктор — это самое подходящее место для этой операции. Если вы используете динамические небезопасные встроенные массивы, то их удалять нужно вручную в обязательном порядке. Из этого следует, что лучшим решением будет использование встроенных типов, таких как string или vector . Приведем небольшой код, иллюстрирующий работу с деструктором, в заключение.

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

Для удобства чтения и написания кода разрешается перегрузка некоторых операций. Оператор перегрузки записывается с помощью ключевого слова operator . Разрешена перегрузка следующих операций:

;

  • оператор присваивания =;
  • оператор индексации [].
  • Перегрузка операций позволяет использовать операционную нотацию (запись в виде простых выражений) к сложным объектам — структурам и классам. Запись выражений с использованием перегруженных операций упрощает восприятие исходного кода, так как более сложная реализация сокрыта.

    Для примера рассмотрим широко применяемые в математике комплексные числа, которые состоят из действительной и мнимой части. В языке MQL5 нет типа данных для представления комплексных чисел, но есть возможность создать новый тип данных в виде структуры или класса. Объявим структуру complex и определим в ней четыре метода, реализующие четыре арифметические операции:

    Теперь мы можем объявлять в своем коде переменные, представляющие комплексные числа, и работать с ними.

    void OnStart ()
    <
    //— объявим и инициализируем переменные комплексного типа
    complex a(2,4),b(-4,-2);
    PrintFormat ( «a=%.2f+i*%.2f, b=%.2f+i*%.2f» ,a.re,a.im,b.re,b.im);
    //— сложим два числа
    complex z;
    z=a.Add(a,b);
    PrintFormat ( «a+b=%.2f+i*%.2f» ,z.re,z.im);
    //— умножим два числа
    z=a.Mul(a,b);
    PrintFormat ( «a*b=%.2f+i*%.2f» ,z.re,z.im);
    //— разделим два числа
    z=a.Div(a,b);
    PrintFormat ( «a/b=%.2f+i*%.2f» ,z.re,z.im);
    //—
    >

    Но было бы удобнее для привычных арифметических операций с комплексными числами использовать привычные операторы «+»,»-«,»*» и «/».

    Ключевое слово operator используется для того чтобы определить функцию-член, осуществляющую преобразование типа. Унарные и бинарные операции для переменных-объектов класса могут быть перегружены как нестатические функции-члены. Они неявно действуют на объект класса.

    Большинство бинарных операций можно перегружать как обычные функции, принимающие один или оба аргумента в виде переменной класса или в виде указателя на объект данного класса. Для нашего типа complex перегрузка в объявлении будет выглядеть так:

    //— операторы
    complex operator +( const complex &r) const < return (Add(this,r)); >
    complex operator -( const complex &r) const < return (Sub(this,r)); >
    complex operator *( const complex &r) const < return (Mul(this,r)); >
    complex operator /( const complex &r) const

    Полный пример скрипта:

    Большинство унарных операций для классов можно перегружать как обычные функции, принимающие единственный аргумент-объект класса или указатель на него. Добавим перегрузку унарных операций «-» и «!».

    Теперь мы можем проверять значение комплексного числа на ноль и получать отрицательное значение:

    //+——————————————————————+
    //| Script program start function |
    //+——————————————————————+
    void OnStart ()
    <
    //— объявим и инициализируем переменные комплексного типа
    complex a(2,4),b(-4,-2);
    PrintFormat ( «a=%.2f+i*%.2f, b=%.2f+i*%.2f» ,a.re,a.im,b.re,b.im);
    //— разделим два числа
    complex z=a/b;
    PrintFormat ( «a/b=%.2f+i*%.2f» ,z.re,z.im);
    //— комплексное число по умолчанию равно нулю (в конструкторе по умолчанию re==0 и im==0)
    complex zero;
    Print ( «!zero=» ,!zero);
    //— присвоим отрицательное значение
    zero=-z;
    PrintFormat ( «z=%.2f+i*%.2f, zero=%.2f+i*%.2f» ,z.re,z.im, zero.re,zero.im);
    PrintFormat ( «-zero=%.2f+i*%.2f» ,-zero.re,-zero.im);
    //— еще раз проверим на равенство нулю
    Print ( «!zero=» ,!zero);
    //—
    >

    Обратите внимание, что нам не пришлось в данном случае перегружать операцию присваивания «=», так как структуры простых типов можно копировать друг в друга напрямую. Таким образом, теперь мы можем писать код для расчетов с участием комплексных чисел в привычной манере.

    Перегрузка оператора индексирования позволяет получать значения массивов, заключенных в объект, более простым и привычным способом, и это также способствует лучшей читаемости и пониманию исходного кода программ. Например, нам необходимо обеспечить доступ к символу в строке по указанной позиции. Строка в языке MQL5 является отдельным типом string, который не является массивом символов, но с помощью перегруженной операции индексации в созданном классе CString мы можем обеспечить простую и прозрачную работу:

    Другой пример перегрузки операции индексирования — работа с матрицами. Матрица представляет собою двумерный динамический массив, размеры массивов заранее неопределены. Поэтому нельзя объявить массив вида array[][] без указания размера второго измерения и затем передавать этот массив в качестве параметра. Выходом может служить специальный класс CMatrix, который содержит в себе массив объектов класса CRow.

    //+——————————————————————+
    //| Script program start function |
    //+——————————————————————+
    void OnStart ()
    <
    //— операции сложения и умножения матриц
    CMatrix A(3),B(3),C();
    //— готовим массивы под строки
    double a1[3]=<1,2,3>, a2[3]=<2,3,1>, a3[3]=<3,1,2>;
    double b1[3]=<3,2,1>, b2[3]=<1,3,2>, b3[3]=<2,1,3>;
    //— заполняем матрицы
    A[0]=a1; A[1]=a2; A[2]=a3;
    B[0]=b1; B[1]=b2; B[2]=b3;
    //— выведем матрицы в журнал «Эксперты»
    Print ( «—- элементы матрицы A» );
    Print (A. String ());
    Print ( «—- элементы матрицы B» );
    Print (B. String ());

    //— сложение матриц
    Print ( «—- сложение матриц A и B» );
    C=A+B;
    //— вывод форматированного строкового представления
    Print (C. String ());

    //— умножение матриц
    Print ( «—- произведение матриц A и B» );
    C=A*B;
    Print (C. String ());

    //— а теперь покажем как получать значения в стиле динамических массивов matrix[i][j]
    Print ( «Выводим значения матрицы C поэлементно» );
    //— перебираем в цикле строки матрицы — объекты CRow
    for ( int i=0;i
    <
    string com= «| » ;
    //— формируем для значения строки из матрицы
    for ( int j=0;j
    <
    //— получим элемент матрицы по номерам строки и столбца
    double element=C[i][j]; // [i] — доступ к CRow в массиве m_rows[] ,
    // [j] — перегруженный оператор индексации в CRow
    com=com+ StringFormat ( «a(%d,%d)=%G ; » ,i,j,element);
    >
    com+= «|» ;
    //— выводим значения строки
    Print (com);
    >
    >
    //+——————————————————————+
    //| Класс «Строка» |
    //+——————————————————————+
    class CRow
    <
    private :
    double m_array[];
    public :
    //— конструкторы и деструктор
    CRow( void ) < ArrayResize (m_array,0); >
    CRow( const CRow &r) < this =r; >
    CRow( const double &array[]);

    Перегрузка Операций

    Здесь водятся Драконы! — старинная карта

    В этой главе описывается аппарат, предоставляемый в C++ для перегрузки операций. Программист может определять смысл операций при их применении к объектам определенного класса. Кроме арифметических, можно определять еще и логические операции, операции сравнения, вызова () и индексирования [], а также можно переопределять присваивание и инициализацию. Можно определить явное и неявное преобразование между определяемыми пользователем и основными типами. Показано, как определить класс, объект которого не может быть никак иначе скопирован или уничтожен кроме как специальными определенными пользователем функциями.

    6.1 Введение

    Часто программы работают с объектами, которые являются конкретными представлениями абстрактных понятий. Например, тип данных int в C++ вместе с операциями +, -, *, / и т.д. предоставляет реализацию (ограниченную) математического понятия целых чисел. Такие понятия обычно включают в себя множество операций, которые кратко, удобно и привычно представляют основные действия над объектами. К сожалению, язык программирования может непосредственно поддерживать лишь очень малое число таких понятий. Например, такие понятия, как комплексная арифметика, матричная алгебра, логические сигналы и строки не получили прямой поддержки в C++. Классы дают средство спецификации в C++ представления неэлементарных объектов вместе с множеством действий, которые могут над этими объектами выполняться. Иногда определение того, как действуют операции на объекты классов, позволяет программисту обеспечить более общепринятую и удобную запись для манипуляции объектами классов, чем та, которую можно достичь используя лишь основную функциональную запись. Например:

    определяет простую реализацию понятия комплексного числа, в которой число представляется парой чисел с плавающей точкой двойной точности, работа с которыми осуществляется посредством операций + и * (и только). Программист задает смысл операций + и * с помощью определения функций с именами operator+ и operator*. Если, например, даны b и c типа complex, то b+c означает (по определению) operator+(b,c). Теперь есть возможность приблизить общепринятую интерпретацию комплексных выражений. Например:

    Выполняются обычные правила приоритетов, поэтому второй оператор означает b=b+(c*a), а не b=(b+c)*a.

    6.2 Функции Операции

    Можно описывать функции, определяющие значения следующих операций:

    Последние четыре — это индексирование (#6.7), вызов функции (#6.8), выделение свободной памяти и освобождение свободной памяти (#3.2.6). Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную !. Невозможно определить новые лексические символы операций, но в тех случаях, когда множество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения могут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд определение операции **, означающей возведение в степень, может показаться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)? Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)?

    Имя функции операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator vo >

    При наличии предыдущего описания complex оба инициализатора являются синонимами.

    6.2.1 Бинарные и Унарные Операции

    Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры:

    Когда операции ++ и — перегружены, префиксное использование и постфиксное различить невозможно.

    6.2.2 Предопределенные Значения Операций


    Относительно смысла операций, определяемых пользователем, не делается никаких предположений. В частности, поскольку не предполагается, что перегруженное = реализует присваивание ее первому операнду, не делается никакой проверки, чтобы удостовериться, является ли этот операнд lvalue (#с.6).

    Значения некоторых встроенный операций определены как равносильные определенным комбинациям других операций над теми же аргументами. Например, если a является int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения для определенных пользователем операций не выполняются, если только не случилось так, что пользователь сам определил их таким образом. Например, определение operator+=() для типа complex не может быть выведено из определений complex::operator+() и complex::operator=().

    Илон Маск рекомендует:  Что такое код ftp_size

    По историческому совпадению операции = и & имеют предопределенный смысл для объектов классов. Никакого элегантного способа «не определить» эти две операции не существует. Их можно, однако, сделать недееспособными для класса X. Можно, например, описать X::operator&(), не задав ее определения. Если где-либо будет браться адрес объекта класса X, то компоновщик обнаружит отсутствие определения* 1 . Или, другой способ, можно определить X::operator&() так, чтобы вызывала ошибку во время выполнения.

    6.2.3 Операции и Определяемые Пользователем Типы

    Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (функциям, которые переопределяют операции new и delete, это делать необязательно). Это правило гарантирует, что пользователь не может изменить смысл никакого выражения, не включающего в себя определенного пользователем типа. В частности, невозможно определить функцию, которая действует исключительно на указатели.

    Функция операция, первым параметром которой предполагается основной тип, не может быть функцией членом. Рассмотрим, например, сложение комплексной переменной aa с целым 2: aa+2, при подходящим образом описанной функции члене, может быть проинтерпретировано как aa.operator+(2), но с 2+aa это не может быть сделано, потому что нет такого класса int, для которого можно было бы определить + так, чтобы это означало 2.operator+(aa). Даже если бы такой тип был, то для того, чтобы обработать и 2+aa и aa+2, понадобилось бы две различных функции члена. Так как компилятор не знает смысла +, определенного пользователем, то не может предполагать, что он коммутативен, и интерпретировать 2+aa как aa+2. С этим примером могут легко справиться функции друзья.

    Все функции операции по определению перегружены. Функция операция задает новый смысл операции в дополнение к встроенному определению, и может существовать несколько функций операций с одним и тем же именем, если в типах их параметров имеются отличия, различимые для компилятора, чтобы он мог различать их при обращении (см. #4.6.7).

    6.3 Определяемое Преобразование Типа

    Приведенная во введении реализация комплексных чисел слишком ограничена, чтобы она могла устроить кого-либо, поэтому ее нужно расширить. Это будет в основном повторением описанных выше методов. Например:

    Теперь, имея описание complex, мы можем написать:

    Но писать функцию для каждого сочетания complex и double, как это делалось выше для operator+(), невыносимо нудно. Кроме того, близкие к реальности средства комплексной арифметики должны предоставлять по меньшей мере дюжину таких функций; посмотрите, например, на тип complex.

    6.3.1 Конструкторы

    Альтернативу использованию нескольких функций (перегруженных) составляет описание конструктора, который по заданному double создает complex. Например:

    Конструктор, требующий только один параметр, необязательно вызывать явно:

    И z1, и z2 будут инициализированы вызовом complex(23).

    Конструктор — это предписание, как создавать значение данного типа. Когда требуется значение типа, и когда такое значение может быть создано конструктором, тогда, если такое значение дается для присваивания, вызывается конструктор. Например, класс complex можно было бы описать так:

    и действия, в которые будут входить переменные complex и целые константы, стали бы допустимы. Целая константа будет интерпретироваться как complex с нулевой мнимой частью. Например, a=b*2 означает:

    Определенное пользователем преобразование типа применяется неявно только тогда, когда оно является единственным.

    Объект, сконструированный с помощью явного или неявного вызова конструктора, является автоматическим и будет уничтожен при первой возможности, обычно сразу же после оператора, в котором он был создан.

    6.3.2 Операции Преобразования

    Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут оказаться нежелательными:

    [1] Не может быть неявного преобразования из определенного пользователем типа в основной тип (поскольку основные типы не являются классами);

    [2] Невозможно задать преобразование из нового типа в старый, не изменяя описание старого; и

    [3] Невозможно иметь конструктор с одним параметром, не имея при этом преобразования.

    Последнее не является серьезной проблемой, а с первыми двумя можно справиться, определив для исходного типа операцию преобразования. Функция член X::operator T(), где T — имя типа, определяет преобразование из X в T. Например, можно определить тип tiny (крошечный), который может иметь значение только в диапазоне 0. 63, но все равно может свободно сочетаться в целыми в арифметических операциях:

    Диапазон значения проверяется всегда, когда tiny инициализируется int, и всегда, когда ему присваивается int. Одно tiny может присваиваться другому без проверки диапазона. Чтобы разрешить выполнять над переменными tiny обычные целые операции, определяется tiny::operator int(), неявное преобразование из int в tiny. Всегда, когда в том месте, где требуется int, появляется tiny, используется соответствующее ему int. Например:

    Тип вектор из tiny может оказаться более полезным, поскольку он экономит пространство. Чтобы сделать этот тип более удобным в обращении, можно использовать операцию индексирования.

    Другое применение определяемых операций преобразования — это типы, которые предоставляют нестандартные представления чисел (арифметика по основанию 100, арифметика с фиксированной точкой, двоично-десятичное представление и т.п.). При этом обычно переопределяются такие операции, как + и *.

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

    Типы istream и ostream опираются на функцию преобразования, чтобы сделать возможными такие операторы, как while (cin>>x) coutx выше возвращает istream&. Это значение неявно преобразуется к значению, которое указывает состояние cin, а уже это значение может проверяться оператором while (см. #8.4.2). Однако определять преобразование из оного типа в другой так, что при этом теряется информация, обычно не стоит.

    6.3.3 Неоднозначности

    Присваивание объекту (или инициализация объекта) класса X является допустимым, если или присваиваемое значение является X, или существует единственное преобразование присваиваемого значения в тип X.

    В некоторых случаях значение нужного типа может сконструироваться с помощью нескольких применений конструкторов или операций преобразования. Это должно делаться явно; допустим только один уровень неявных преобразований, определенных пользователем. Иногда значение нужного типа может быть сконструировано более чем одним способом. Такие случаи являются недопустимыми. Например:

    Определенные пользователем преобразования рассматриваются только в том случае, если без них вызов разрешить нельзя. Например:

    Вызов мог бы быть проинтерпретирован или как h(double(1)), или как h(x(1)), и был бы недопустим по правилу единственности. Но первая интерпретация использует только стандартное преобразование и она будет выбрана по правилам, приведенным в #4.6.7. Правила преобразования не являются ни самыми простыми для реализации и документации, ни наиболее общими из тех, которые можно было бы разработать. Возьмем требование единственности преобразования. Более общий подход разрешил бы компилятору применять любое преобразование, которое он сможет найти; таким образом, не нужно было бы рассматривать все возможные преобразования перед тем, как объявить выражение допустимым. К сожалению, это означало бы, что смысл программы зависит от того, какое преобразование было найдено. В результате смысл программы неким образом зависел бы от порядка описания преобразования. Поскольку они часто находятся в разных исходных файлах (написанных разными людьми), смысл программы будет зависеть от порядка компоновки этих частей вместе. Есть другой вариант — запретить все неявные преобразования. Нет ничего проще, но такое правило приведет либо к неэлегантным пользовательским интерфейсам, либо к бурному росту перегруженных функций, как это было в предыдущем разделе с complex.

    Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Например, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность толкования. Если aa является x, то единственное, дающее в результате x, который требуется присваиванием, — это f(x(1)), а если aa — это y, то вместо этого будет использоваться f(y(1)). Самый общий подход справился бы и с g(«asdf»), поскольку единственной интерпретацией этого может быть g(z(x(«asdf»))). Сложность этого подхода в том, что он требует расширенного анализа всего выражения для того, чтобы определить интерпретацию каждой операции и вызова функции. Это приведет к замедлению компиляции, а также к вызывающим удивление интерпретациям и сообщениям об ошибках, если компилятор рассмотрит преобразования, определенные в библиотеках и т.п. При таком подходе компилятор будет принимать во внимание больше, чем, как можно ожидать, знает пишущий программу программист!

    6.4 Константы

    Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e3 являются константой типа double. Вместо них, однако, часто можно использовать константы основных типов, если их реализация обеспечивается с помощью функций членов. Общий аппарат для этого дают конструкторы, получающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам функций приведут две операции *, а операция + и конструктор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.

    6.5 Большие Объекты

    При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать ненужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:

    Ссылки позволяют использовать выражения, содержащие обычные арифметические операции над большими объектами, без ненужного копирования. Указатели применять нельзя, потому что невозможно для применения к указателю смысл операции переопределить невозможно. Операцию плюс можно определить так:

    Это является допустимым, но приводит к сложности с выделением памяти. Поскольку ссылка на результат будет передаваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто операция используется в выражении больше одного раза, результат не может быть и статической переменной. Как правило, его размещают в свободной памяти. Часто копирование возвращаемого значения оказывается дешевле (по времени выполнения, объему кода и объему данных) и проще программируется.

    6.6 Присваивание и Инициализация

    Рассмотрим очень простой класс строк string:

    Строка — это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:

    будет размещать два вектора символов, а присваивание s1=s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом определить присваивание объектов типа string:

    Это определение string гарантирует, и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приведет к появлению той же проблемы в новом облике:

    Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определенная пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было неразумно так делать: указатель p будет содержать неопределенное и совершенно случайное значение. Часто операция присваивания полагается на то, что ее аргументы инициализированы. Для такой инициализации, как здесь, это не так по определению. Следовательно, нужно определить похожую, но другую, функцию, чтобы обрабатывать инициализацию:

    Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&). Нельзя не подчеркнуть еще раз, что присваивание и инициализация — разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор, выполняющий нетривиальную работу вроде освобождения памяти, то скорее всего потребуется полный комплект функций, чтобы полностью избежать побитового копирования объектов:

    Есть еще два случая, когда объект копируется: как параметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого переменная — формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из функции, хотя это менее очевидно. В обоих случаях будет применен X(X&), если он определен:

    Ясно, что после вызова g() значение s обязано быть «asdf». Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&); на этот раз инициализируемой является временная переменная, которая затем присваивается s. Такие переменные, естественно, уничтожаются как положено с помощью string::


    string() при первой возможности.

    6.7 Индексирование

    Чтобы задать смысл индексов для объектов класса используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перепишем пример из #2.3.10, где при написании небольшой программы для подсчета числа вхождений слов в файле применялся ассоциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:

    В assoc хранится вектор пар pair длины max. Индекс первого неиспользованного элемента вектора находится в free. Конструктор выглядит так:

    Поскольку представление assoc скрыто, нам нужен способ его печати. В следующем разделе будет показано, как определить подходящий итератор, а здесь мы используем простую функцию печати:

    6.8 Вызов Функции

    Вызов функции, то есть запись выражение(список_выражений), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие операции. Список параметров функции operator() вычисляется и проверяется в соответствие с обычными правилами передачи параметров. Перегружающая функция может оказаться полезной главным образом для определения типов с единственной операцией и для типов, у которых одна операция настолько преобладает, что другие в большинстве ситуаций можно не принимать во внимание.

    Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к данным, которые хранятся в assoc, поэтому он сделан другом:

    Илон Маск рекомендует:  Как программно вставить изображение в excel

    Итератор определяется как

    Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать операцией (). По достижении конца массива он возвращает 0:

    Конструкторы и деструкторы просты (как обычно):

    Как обычно, операции присваивания очень похожи на конструкторы. Они должны обрабатывать очистку своего первого (левого) операнда:

    Благоразумно обеспечить, чтобы присваивание объекта самому себе работало правильно:

    Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции ostream& operators n

    Операция ввода использует стандартную функцию ввода символьной строки (#8.4.1).

    Головная программа просто немного опробует действия над строками. Она читает слова со ввода в строки, а потом эти строки печатает. Она продолжает это делать до тех пор, пока не распознает строку done, которая завершает сохранение слов в строках, или не встретит конец файла. После этого она печатает строки в обратном порядке и завершается.

    Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для «настоящего объекта», в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:

    Поэтому операция, изменяющее состояние объекта, должно быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++ и т.д.), наиболее естественно определяются как члены.

    И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, || и т.д.).

    Если никакие преобразования типа не определены, то оказывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова другому. Например, оказывается, что большинство предпочитает для обращения матрицы m запись m.inv(). Конечно, если inv() действительно обращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть другом.

    При прочих равных условиях выбирайте, чтобы функция была членом: никто не знает, вдруг когда-нибудь кто-то определит операцию преобразования. Невозможно предсказать, потребуют ли будущие изменения изменить статус объекта. Синтаксис вызова функции члена ясно указывает пользователю, что объект можно изменить; ссылочный параметр является далеко не столь очевидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использовать неявный this. Если только не применяется перегрузка, имена членов обычно короче имен друзей.

    6.11 Предостережение

    Как и большая часть возможностей в языках программирования, перегрузка операций может применяться как правильно, так и неправильно. В частности, можно так воспользоваться возможность определять новые значения старых операций, что они станут почти совсем непостижимы. Представьте, например, с какими сложностями столкнется человек, читающий программу, в которой операция + была переопределена для обозначения вычитания.

    Данный аппарат должен уберечь программиста/читателя от худших крайностей применения перегрузки, потому что программист предохранен от изменения значения операций для основных типов данных вроде int, а также потому, что синтаксис выражений и приоритеты операций сохраняются.

    Может быть. разумно применять перегрузку операций главным образом так, чтобы подражать общепринятому применению операций. В тех случаях, когда нет общепринятой операции или имеющееся в C++ множество операций не подходит для имитации общепринятого применения, можно использовать запись вызова функции.

    6.12 Упражнения

    1. (*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию «добавить в конец» +=. Какие еще операции над string вы хотели бы осуществлять?
    2. (*1.5) Задайте с помощью перегрузки () операцию выделения подстроки для класса строк.
    3. (*3) Постройте класс string так, чтобы операция выделения подстроки могла использоваться в левой части присваивания. Напишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут быть разными.
    4. (*2) Постройте класс string так, чтобы для присваивания, передачи параметров и т.п. он имел семантику по значению, то есть в тех случаях, когда копируется строковое представление, а не просто управляющая структура данных класса sring.
    5. (*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходимо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не будет изменена. Не пытайтесь одновременно с этим иметь операцию выделения подстроки, которая может использоваться в левой части.
    6. (*4) Разработайте класс string с семантикой по значению, копированием с задержкой и операцией подстроки, которая может стоять в левой части.
    7. (*2) Какие преобразования используются в каждом выражении следующей программы:

    Определите X и Y так, чтобы они оба были целыми типами. Измените программу так, чтобы она работала и печатала значения всех допустимых выражений.

  • (*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().
  • (*1) Определите класс RINT, который ведет себя в точности как int за исключением того, что единственные возможные операции — это + (унарный и бинарный), — (унарный и бинарный), *, /, %. Подсказка: не определяйте $ (R?)INT::operator int().
  • (*3) Определите класс LINT, ведущий себя как RINT за исключением того, что имеет точность не менее 64 бит.
  • (*4) Определите класс, который реализует арифметику с произвольной точностью. Подсказка: вам надо управлять памятью аналогично тому, как это делалось для класса string.
  • (*2) Напишите программу, доведенную до нечитаемого состояния с помощью макросов и перегрузки операций. Вот идея: определите для INT + чтобы он означал — и наоборот, а потом с помощью макроопределения определите int как INT. Переопределение часто употребляемых функций, использование параметров ссылочного типа и несколько вводящих в заблуждение комментариев помогут устроить полную неразбериху.
  • (*3) Поменяйтесь со своим другом программами, которые у вас получились в предыдущем упражнении. Не запуская ее попытайтесь понять, что делает программа вашего друга. После выполнения этого упражнения вы будете знать, чего следует избегать.
  • (*2) Перепишите примеры с comlpex (#6.3.1), tiny (#6.3.2) и string (#6.9) не используя friend функций. Используйте только функции члены. Протестируйте каждую из новых версий. Сравните их с версиями, в которых используются функции друзья. Еще раз посмотрите Упражнение 5.3.
  • (*2) Определите тип vec4 как вектор их четырех float. Определите operator[] для vec4. Определите операции +, -, *, /, =, +=, -=, *=, /= для сочетаний векторов и чисел с плавающей точкой.
  • (*3) Определите класс mat4 как вектор из четырех vec4. Определите для mat4 operator[], возвращающий vec4. Определите для этого типа обычные операции над матрицами. Определите функцию, которая производит исключение Гаусса для mat4.
  • (*2) Определите класс vector, аналогичный vec4, но с длиной, которая задается как параметр конструктора vector::vector(int).
  • (*3) Определите класс matrix, аналогичный mat4, но с размерностью, задаваемой параметрами конструктора matrix::matrix(int,int).
  • Перегрузка стандартных операций


    В С++ существует возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Например, если и б2 — объекты нового класса МуБ^^, то их конкатенацию (соединение) удобно было бы обозначить как 51+52. Однако бинарная операция + в обычном контексте языка С++ предназначена для арифметических операндов и не предусматривает работу с объектами класса Му51пп§.

    Чтобы иметь возможность использовать стандартную для языка С++ операцию (например, «+» или «*») с пользовательскими типами данных, необходимо специальным образом определить, каким образом будут выполняться действия с операндами. В языке С++ для этой цели служат специальные операции-функции, которые задаются особым образом.

    Операция-функция определяет алгоритм выполнения перегруженной операции, когда эта операция применяется к объектам класса, для которого операция-функция введена. Для обеспечения явной связи с классом операция-функция должна быть:

    • 1) либо компонентом класса;
    • 2) либо она должна быть определена в классе как дружественная;
    • 3) либо у нее должен быть хотя бы один параметр типа класс (или ссылка на класс).

    Кроме того, для правильной перегрузки операторов-функций должны соблюдаться следующие правила:

    • • число операндов оператора-функции должно совпадать с числом операндов стандартной операции (унарные операции и бинарные);
    • • нельзя перегружать следующие операции:
      • — — операцию изменения доступа;
      • — . — операцию доступа к члену статической структуры;
      • — -> — операцию доступа к члену динамической структуры;
      • — .* — операцию доступа к динамическому члену статической структуры;
      • — ->* — операцию доступа к динамическому члену динамической структуры;
      • — ?: — условную операцию;
    • • при использовании нескольких операторов-функций в выражении приоритет выполнения операций должен совпадать с приоритетом выполнения стандартных операций.

    Формат определения операции-функции:

    тип_результата operator знак_операции (список_пара-метров)

    В этом определении используется ключевое слово operator.

    При необходимости может добавляться и прототип операции-функции с форматом:

    тип_результата operator знак_операции (список_пара-метров);

    Пример. При работе с точками на геометрической плоскости возникает необходимость складывать и вычитать значения координат точек. Перегрузим операции «=», «+», «-» для объектов класса CPoint.

    class CPoint //определение класса «точка»

    private: int x, у;

    //’определение конструктора класса CPoint

    this->x = x; this->y = y;

    //определение члена-функции GetX класса CPoint

    //определение члена-функции GetY класса CPoint

    void SetXY(int х, int у)

    //определение члена-функции SetXY класса CPoint

    this -> х = х; this -> у = у;

    void info () //’определение члена-функции info класса CPoint

    printf(«x — %d, у — %d «, х, у);

    CPoint& operator = (CPoint &p)

    < //для объектов класса CPoint

    this -> х = р.х; //по первому способу

    this -> у = р.у; return *this;

    /*прототип дружественного оператора функции «

    »объектов класса CPoint по второму способу*/

    CPoint& operator — (CPoint &pl, CPoint &p2)

    static CPoint temp; temp.x = pl.x — p2.x; temp.у = pi.у — p2.y; return temp;

    CPoint& operator + (CPoint &pl, CPoint &p2)

    Основы перегрузки операторов

    C# — Руководство по C# — Основы перегрузки операторов

    В C#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму:

    Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

    По сути, функциональность операции + уникальным образом базируются на представленных типах данных (строках или целых в данном случае). Когда операция + применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк.

    Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

    Операция C# Возможность перегрузки
    +, -, !, ++, —, true, false Этот набор унарных операций может быть перегружен
    +, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
    ==, !=, = Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки «подобных» операций (т.е. , =, == и !=)
    [] Операция [] не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
    () Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
    +=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

    Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово operator, определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса. Существуют две формы операторных методов (operator): одна — для унарных операторов, другая — для бинарных. Ниже приведена общая форма для каждой разновидности этих методов:

    Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2. Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа — public и static.

    Перегрузка бинарных операторов

    Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

    Лекция 5. Перегрузка операций

    1 Введение

    C++ позволяет переопределить действие большинства операций для манипулирования с объектами конкретных типов. Это оказывается очень удобно, поскольку привычные символы обозначения операций могут быть использованы в разных смыслах, в зависимости от типов.

    Можно перегружать любые операции, существующие в С++, за исключением:

    Перегрузка операций осуществляется с помощью методов специального вида и подчиняется следующим правилам:

    • При перегрузке сохраняются количество аргументов, приоритеты операций и правила ассоциаций (справа налево или слева направо), используемые в стандартных типах данных.
    • Для стандартных типов перегружать операции нельзя.
    • Функции-операторы не могут иметь аргументов по умолчанию.
    • Функции-операторы могут наследоваться (за исключением =).
    • Функции-операторы не могут быть статическими.
    • Для одного и того же оператора можно объявить несколько перегруженных операторов — функций. Они должны отличаться по типу и количеству аргументов.

    2 Бинарные операции

    Можно определить в виде:

    • нестатической функции-члена с одним аргументом,
    • функции-не-члена с двумя аргументами.

    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL