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

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

Кроме перегрузки функций С++ позволяет организовать перегрузку операций. Механизм перегрузки операций позволяет обеспечить более традиционную и удобную запись действий над объектами. Для перегрузки встроенных операторов используется ключевое слово operator.

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

 
 
 
 
тип operator @ (список_параметров-операндов)
{
// тело функции
}

где

  • @ — знак перегружаемой операции (-, +, *  и т.д.),
  • тип — тип возвращаемого значения.

Тип возвращаемого значения должен быть отличным от void, если необходимо использовать перегруженную операцию внутри другого выражения.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <iostream>
using namespace std;
class matrix; // прототип класса matrix
class vect
{
  int* p = NULL;
  int size;
public:
  vect(int s = 0)
  {
    size = s;
    if (s == 0) return;
    p = new int[s];
    for (int i = 0; i < s; i++)
      p[i] = 0;
  }
  void in(void)
  {
    for (int i = 0; i < size; i++)
    {
      cout << "vect[" << i << "] = ";
      cin >> p[i];
    }
  }
  void out(void)
  {
    cout << "vect: ";
    for (int i = 0; i < size; i++)
      cout << p[i] << " ";
  }
  ~vect() { delete[] p; }     // деструктор
  friend vect operator *(const vect&, const matrix&);
};

class matrix
{
  int** base;
  int col_size, row_size;
public:
  matrix(int row = 0, int col = 0)
  {
    base = new int* [row];
    for (int i = 0; i < row; i++)
    {
      base[i] = new int[col];
      for (int j = 0; j < col; j++)
        base[i][j] = 0;
    }
    col_size = col;
    row_size = row;
  }
  void in(void)
  {
    for (int i = 0; i < row_size; i++)
      for (int j = 0; j < col_size; j++)
      {
        cout << "matrix[" << i << "][" << j << "] = ";
        cin >> base[i][j];
      }
  }
  void out(void)
  {
    cout << "matrix: " << endl;
    for (int i = 0; i < row_size; i++)
    {
      for (int j = 0; j < col_size; j++)
        cout << base[i][j] << " ";
      cout << endl;
    }
  }
  ~matrix()       // деструктор
  {
    for (int i = 0; i < row_size; i++)
      delete[] base[i];
    delete[] base;
  }
  friend vect operator *(const vect&, const matrix&);
};

// Дружественная функция умножения
vect operator *(const vect& v, const matrix& m)
{
  int i, j;
  vect rez(m.col_size);
  if (v.size != m.row_size)
  {
    cout << "No rezult " << endl;
    return rez;
  }
  for (j = 0; j < m.col_size; j++)
  {
    rez.p[j] = 0;
    for (i = 0; i < m.row_size; i++)
      rez.p[j] += v.p[i] * m.base[i][j];
  }
  return rez;
}

int main()
{
  matrix m(3, 2);
  vect v(3);
  v.in();
  m.in();
  v.out();
  cout << endl;
  m.out();
  vect r = v*m;
  r.out();
  cin.get(); cin.get();
  return 0;
}

Любой перегруженный оператор можно вызвать с использованием функциональной формы записи (функции-операции):

 
r = operator *(v, m);

Функция-операция описывается и может вызываться так же, как любая другая функция. Использование операции – это лишь сокращенная запись явного вызова функции операции.

Пример:

 
 
 
 
 
void f(complex a, complex b) 
{
  complex c = a + b;          // сокращенная запись
  complex d = operator+(a, b); // явный вызов
}

Имеется два способа описания функции, соответствующей переопределяемой операции:

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

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

+ * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ [] () new delete

Операции, не допускающие перегрузки:

  • .  прямой выбор члена объекта класса;
  • .*  обращение к члену через указатель на него;
  • ? :  условная тернарная операция;
  • ::  операция указания области видимости (разрешение контекста);
  • sizeof  операция вычисления размера в байтах;
  • #  препроцессорная операция.

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

  • Язык C++ не допускает определения для операций нового лексического символа, кроме уже определенных в языке. Например, нельзя определить в качестве знака операции @.
  • Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел:
     
    int operator +(int i, int j);
  • Нельзя переопределить приоритет операции.
  • Нельзя изменить синтаксис операции в выражении. Например, если некоторая операция определена как унарная, то ее нельзя определить как бинарную. Если для операции используется префиксная форма записи, то ее нельзя переопределить в постфиксную. Например, нельзя переопределить как а!
  • Перегружать можно только операции, для которых хотя бы один аргумент представляет тип данных, определенный пользователем. Функция-операция должна быть определена либо как функция-член класса, либо как внешняя функция, но дружественная классу.

Метод класса

 
 
 
 
 
 
 
class String
{
  …
public:
  String operator + (const String &);
  …
};

Дружественная функция

 
 
 
 
 
 
 
class String
{
  …
public:
  friend String operator +(String &, String &);
  …
};

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

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

Если унарная операция перегружается дружественной функцией, то она должна иметь один аргумент – объект, для которого она выполняется. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator @(aa).

Если определена и та, и другая, то и aa@ и @aa являются ошибками.

Метод класса

 
 
 
 
 
 
 
class А
{
  …
public:
  A operator !();
  …
};

Дружественная функция

 
 
 
 
 
 
 
class A
{
  …
public:
  friend A operator !(A);
  …
};

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

Если бинарная операция перегружается с использованием метода класса, то в качестве своего первого аргумента она получает неявно переданную переменную класса (указатель this на объект), а в качестве второго — аргумент из списка параметров. То есть, фактически бинарная операция, перегружаемая методом класса, имеет один аргумент (правый операнд), а левый передается неявно через указатель this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class complex 
{
  double real;
  double imag;
public:
  complex operator +(const complex &);
  …
};
complex complex :: operator +(complex &c) 
{
  complex temp;
  temp.real = this->real + c.real;
  temp.imag = this->imag + c.imag;
  return temp;
}

Если бинарная операция перегружается дружественной функцией, то в списке параметров она должна иметь оба аргумента:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
class complex
{
  double real;
  double imag;
public:
  complex(double r = 0, double i = 0)
  {
    real = r; imag = i;
  }
  void out(void)
  {
    cout << real;
    if (imag > 0)
      cout << " +";
    cout << imag << "i" << endl;
  }
  friend complex operator + (const complex& c1, const complex& c2);
};
complex operator + (const complex& c1, const complex& c2)
{
  complex temp;
  temp.real = c1.real + c2.real;
  temp.imag = c1.imag + c2.imag;
  return(temp);
}
int main()
{
  complex a(3.1, 4.5), b(2.3, 6.7); // инициализация
  complex c;
  c = a + b;
  c.out();
  cin.get();
  return 0;
}

Результат выполнения

Перегрузка операции сложения комплексных чисел

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

 
 
 
 
complex a, c, d;
double b;
c = a + b;
d = b + a;

то необходимо переопределить операцию сложения дважды:

1
2
friend complex operator + (complex, double);
friend complex operator + (double, complex);

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

Перегрузка операторов инкремента ++ и декремента −− имеет префиксную и постфиксную формы записи:

 
 
void operator ++() // префиксная форма
void operator ++ (int// постфиксная форма

Как правило, операции инкремента и декремента производят действие над самим объектом, поэтому возвращаемое значение имеет тип void.

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

В качестве примера рассмотрим класс «вектор», в котором операция инкремента будет расширять его размер на 1 ячейку.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
using namespace std;
class vector
{
  double* v;
  int size;
public:
  vector(int size = 0)
  {
    this->size = size;
    v = new double[size];
    for (int i = 0; i < size; i++)
      v[i] = 0;
  }
  ~vector() { delete[] v; }
  void set()
  {
    for (int i = 0; i < size; i++)
    {
      cout << "v[" << i << "]= ";
      cin >> v[i];
    }
  }
  void out(void)
  {
    for (int i = 0; i < size; i++)
      cout << v[i] << " ";
  }
  void operator ++ ()     // префиксная форма
  {
    double* temp = v;
    size++;
    v = new double[size];
    for (int i = 0; i < size - 1; i++)
      v[i] = temp[i];
    v[size - 1] = 0;
    delete[] temp;
    return;
  }
  void operator ++ (int)     // постфиксная форма
  {
    double* temp = v;
    size++;
    v = new double[size];
    for (int i = 0; i < size - 1; i++)
      v[i] = temp[i];
    v[size - 1] = 0;
    delete[] temp;
    return;
  }
};
int main()
{
  vector s(4);
  s.set();
  s.out();
  cout << endl;
  ++s;      // префиксная форма
  s.out();
  cout << endl;
  s++;      // постфиксная форма
  s.out();
  cout << endl;
  cin.get();
  cin.get();
  return 0;
}

Результат выполнения

Перегрузка операций индексирования и вызова функции

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

Переопределение операции [] позволяет использовать синтаксис доступа к элементам массива (имя объекта с квадратными скобками).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <string.h>
#include <iostream>
using namespace std;
class String     // Строка переменной длины
{
  char* str = 0;   // Динамический массив символов
  int     size;   // Длина строки
public:
  String& operator()(intint); // Операция выделения подстроки
  char   operator[](int);      // Операция выделения символа
  void print()
  {
    if (str) cout << str << endl;
  }
  String(char* s = (char*)"")
  {
    size = strlen(s) + 1;
    str = new char[size];
    strcpy_s(str, size, s);
  }
  String(String& r)
  {
    str = new char[r.size];
    strcpy_s(str, r.size, r.str);
    size = r.size;
  }
  ~String() { delete[] str; }
};
String& String::operator()(int n1, int n2)
{
  size = n2 - n1 + 2;
  char* tmp = new char[size];
  for (int i = 0; i < size - 1; i++)
    tmp[i] = str[n1 + i];
  tmp[size - 1] = '\0';
  delete[] str;
  str = new char[size];
  strcpy_s(str, size, tmp);
  delete[] tmp;
  return (*this);
}
//------ Операция выделения символа -------------------
char String::operator[](int index)
{
  return (str[index]);
}
int main()
{
  String s1((char*)"abcdefghi");
  cout << "s1: ";
  s1.print();
  String s3 = s1;
  cout << "s3: ";
  s3.print();
  cout << endl;
  String s2 = s1(2, 4);
  cout << "s1: ";
  s1.print();
  cout << "s2: ";
  s2.print();
  cout << "s3: ";
  s3.print();
  char ch = s2[1];
  cout << "ch =" << ch << endl;
  cin.get();
  return 0;
}

Результат выполнения

Перегрузка операции присваивания

Любой конструктор вызывается явно либо неявно в том случае, если необходимо создать новый объект какого-либо класса.

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

При выполнении операции присваивания по умолчанию копирование значений происходит «поверхностно», но такое копирование не всегда допустимо. Например, недопустимо копирование массивов или указателей.

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

Перепишем функцию main() приведенного выше примера в виде:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
  String s1((char*)"abcdefghi");
  cout << "s1: ";
  s1.print();
  String s3;
  s3 = s1;
  cout << "s3: ";
  s3.print();
  cout << endl;
  String s2 = s1(2, 4);
  cout << "s1: ";
  s1.print();
  cout << "s2: ";
  s2.print();
  cout << "s3: ";
  s3.print();
  char ch = s2[1];
  cout << "ch =" << ch << endl;
  cin.get();
  return 0;
}

Изменения в строчках 6, 7.

Результат выполнения

Объект s3 уже был создан, поэтому конструктор копирования не вызывается.

Операция присваивания не перегружена, поэтому присваивание осуществляется поверхностно: после изменения строки s1 освобождается память, на которую ссылается s3.

Для решения этой проблемы достаточно в качестве метода класса добавить перегрузку операции присваивания:

1
2
3
4
5
6
7
8
9
10
11
String& operator=(const String& s)
{
  if (size != s.size) 
  {
    size = s.size;
    delete[] str;
    str = new char[size + 1];
  }
  strcpy_s(str, size, s.str);
  return *this;
}

Результат выполнения совпадет с предыдущим «правильным» примером.

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

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

 
 
void *operator new(size_t size);
void  operator delete (void *);

где

  • void * – указатель на область памяти, выделяемую под объект,
  • size – размер объекта в байтах,
  • size_t – тип размерности области памяти, int или long int.

Переопределение этих операций позволяет написать собственное распределение памяти для объектов класса.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх