Предыдущая Оглавление Следующая

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

В языке С++ определены множества операций над переменными стандартных типов, такие как +, -, *, / и т.д. Каждую операцию можно применить к операндам определенного типа.

К сожалению, лишь ограниченное число типов непосредственно поддерживается любым языком программирования. Например, С и С++ не позволяют выполнять операции с комплексными числами, матрицами, строками, множествами. Однако, все эти операции можно выполнить через классы в языке С++.

Рассмотрим пример.

Пусть заданы множества А и В:
А = { а1, а2, а3 };
В = { a3, a4, a5 },
и мы хотим выполнить операции объединения (+) и пересечения (*) множеств.

А + В = { a1, a2, a3, a4, a5 }
А * В = { a3 }.

Можно определить класс Set - "множество" и определить операции над объектами этого класса, выразив их с помощью знаков операций, которые уже есть в языке С++, например, + и *. В результате операции + и * можно будет использовать как и раньше, а также снабдить их дополнительными функциями (объединения и пересечения). Как определить, какую функцию должен выполнять оператор: старую или новую? Очень просто – по типу операндов. А как быть с приоритетом операций? Сохраняется определенный ранее приоритет операций. Для распространения действия операции на новые типы данных надо определить специальную функцию, называемую "операция-функция" (operator-function). Ее формат:

тип_возвр_значения operator знак_операции(специф_параметров)
{операторы_тела_функции}

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

тип_возвр_значения operator знак_операции(специф_параметров);

Если принять, что конструкция operator знак_операции есть имя некоторой функции, то прототип и определение операции-функции подобны прототипу и определению обычной функции языка С++. Определенная таким образом операция называется перегруженной (overload).

Чтобы была обеспечена явная связь с классом, операция-функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная и у нее должен быть хотя бы один параметр типа класс (или ссылка на класс). Вызов операции-функции осуществляется так же, как и любой другой функции С++: operator @. Однако разрешается использовать сокращенную форму ее вызова: a @ b, где @ - знак операции.

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

Любая унарная операция @ может быть определена двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. В первом случае выражение @ Z означает вызов Z.operator @(), во втором - вызов operator @(Z).

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

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

Синтаксис:

а) в первом случае (описание в области класса):

тип_возвр_значения operator знак_операции

б) во втором случае (описание вне области класса):

тип_возвр_значения operator знак_операции (идентификатор_типа)

Пример.

class Person {
public:
  void operator ++()
  {
    ++age; 
  }
protected:
  int age;
  ...
};

int main(void)
{
  Person John;
  ++John;
  ...
}
class Person {
protected:
  int age;
  ...
friend void operator ++(Person &);
};

void operator ++(Person &ob)
{
  ++ob.age;
}

int main (void)
{
  Person John;
  ++John;
  ...
}

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

Любая бинарная операция @ может быть определена двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае x @ y означает вызов x.operator @(y), во втором – вызов operator @(x, y).

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

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

Пример.

  1. class Person { ... };
    
    class AdressBook
    {
    public:
      Person &operator [](int);// доступ к i
    protected:
      // содержит в качестве компонентных данных множество объектов типа 
      // Person представляемых как динамический массив, список или дерево
      ...
    };
    
    Person &AdressBook::operator [](int i) 
    { ... }
    
    int main(void)
    {
      AdressBook persons;
      Person record;
      ...
      record = persons[3];
      ...
    }
  2. class Person { ... };
    
    class AdressBook
    {
    protected:
      // содержит в качестве компонентных данных множество объектов типа 
      // Person представляемых как динамический массив, список или дерево
      ...
    friend Person &operator [](const AdressBook &, int); // доступ к i
    };
    
    Person &AdressBook::operator [](const AdressBook &, int i) 
    { ... }
    
    int main(void)
    {
      AdressBook persons;
      Person record;
      ...
      record = persons[3];
      ...
    }

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

Унарные операции инкремента ++ и декремента -- существуют в двух формах: префиксной и постфиксной. В современной спецификации С++ определен способ, по которому компилятор может различить эти две формы. В соответствии с этим способом задаются две версии функции operator ++() и operator --(). Они определены следующим образом:

Префиксная форма:

operator ++();
operator --();

Постфиксная форма:

operator ++(int);
operator --(int);

Указание параметра int для постфиксной формы не специфицирует второй операнд, а используется только для отличия от префиксной формы.

Пример.

class Person
{
public:
  ...
  void operator ++()
  {
    ++age; 
  }
  void operator ++(int) 
  {
    age++; 
  }
protected:
  int age;
  ...
};

int main(void)
{
  Person John;
  John++;
  ++John;
}

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

Это операция '()'. Она является бинарной операцией. Первым операндом обычно является объект класса, вторым – список параметров.

Пример.

class Matrix // двумерный массив вещественных чисел
{
public:
  ...
  double operator ()(int, int); //доступ к элементам матрицы по индексам
};

double Matrix::operator() (int i, int j)
{ ... }

int main (void)
{
  Matrix a;
  double k;
  ...
  k = a(5, 6);
  ...
}

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

Операция отличается тремя особенностями:

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

Пользовательский класс - строка String:

class String
{
public:
  String(char *);
  ~String();
  void show();
protected:
  char *p; // указатель на строку
  int len; // текущая длина строки
};

String::String(char *ptr)
{
  len = strlen(ptr);
  p = new chat[len + 1];
  if (!p)
  {
    cout << "Ошибка выделения памяти\n");
    exit(1);
  }
  strcpy(p, ptr);
}

String::~String()
{
  delete [] p;
}

void String::show(void)
{
  cout << *p& lt;< "\n";
}

int main(void)
{
  String s1("Это первая строка"),
         s2("А это вторая строка");
  s1.show();
  s2.show()
  s2 = s1; // Это ошибка
  s1.show();
  s2.show();
  return 0;
}

В чем здесь ошибка? Когда объект s1 присваивается объекту s2, указатель p объекта s2 начинает указывать на ту же самую область памяти, что и указатель p объекта s1. Таким образом, когда эти объекты удаляются, память, на которую указывает указатель p объекта s1, освобождается дважды, а память, на которую до присваивания указывал указатель p объекта s2, не освобождается вообще.

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

В этом случае необходимо самим перегружать операцию присваивания. Покажем как это сделать для нашего класса String.

class String
{
public:
  ...
  String &operator =(String &);
protected:
  char *p; // указатель на строку
  int len; // текущая длина строки
};

String &String::operator =(String &ob);
{
  if (this == &ob) return *this;
  if (len < ob.len)
  {
    // требуется выделить дополнительную память
    delete [] p;
    p = new char[ob.len + 1];
    if (!p)
    {
      cout << "Ошибка выделения памяти\n");
      exit(1);
    }
  }
  len = ob.len;
  strcpy(p, ob.p);
  return *this;
}

В этом примере выясняется, не происходит ли самоприсваивание (типа ob = ob). Если имеет место самоприсваивание, то просто возвращается ссылка на объект.

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

Отметим две важные особенности функции operanor =(). Во-первых, в ней используется параметр-ссылка. Это необходимо для предотвращения создания копии объекта, передаваемого через параметр по значению. В случае создания копии, она удаляется вызовом деструктора при завершении работы функции. Но деструктор освобождает память, на которую указывает p. Однако эта память все еще необходима объекту, который является аргументом. Параметр-ссылка помогает решить эту проблему.

Во-вторых, функция operator =() возвращает не объект, а ссылку на него. Смысл этого тот же, что и при использовании параметра-ссылки. Функция возвращает временный объект, который удаляется после завершения ее работы. Это означает, что для временной переменной будет вызван деструктор, который освобождает память по адресу p. Но она необходима для присваивания значения объекту. Поэтому, чтобы избежать создания временного объекта, в качестве возвращаемого значения используется ссылка.

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

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

Синтаксис

Операция new, заданная по умолчанию, может быть в двух формах:

1) new тип <инициализирующее выражение>
2) new тип [];

Первая форма используется не для массивов, вторая - для массивов.

Перегруженную операцию new можно определить в следующих формах, соответственно для не массивов и для массивов:

void *operator new (size_t t[,остальные аргументы]);
void *operator new [] (size_t t[,остальные аргументы]);

Первый и единственный обязательный аргумент t всегда должен иметь тип size_t. Если аргумент имеет тип size_t, то в операцию-функцию new автоматически подставляется аргумент sizeof(t), т.е. она получает значение, равное размеру объекта t в байтах.

Например, пусть задана следующая функция:

void *operator new(size_t t, int n)
{ return new char [t * n]; }

и она вызывается следующим образом:

double *d = new(5) double;

Здесь t = double, n = 5.

В результате после вызова значение t в теле функции будет равно sizeof(double).

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

new((double) 5) double;

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

В соответствии со стандартом С++ в заголовочном файле <new> определены следующие функции-операции new, позволяющие передавать, наряду с обязательным первым size_t аргументом и другие.

void *operatop new(size_t t) throw (bad_alloc);
void *operatop new(size_t t, void *p) throw ();
void *operatop new(size_t t, const nothrow &) throw ();
void *operatop new(size_t t, allocator &a);
void *operatop new [](size_t t) throw (bad_alloc);
void *operatop new [](size_t t, void *p) throw ();
void *operatop new [](size_t t, const nothrow &) throw ();

Эти функции используют генерацию исключений (throw) и собственный распределитель памяти (allocator).

Версия с nothrow выделяет память как обычно, но если выделение заканчивается неудачей, возвращается 0, а не генерируется bad_alloc. Это позволяет нам для выделения памяти использовать стратегию обработки ошибок до генерации исключения.

Правила использования операции new
  1. Объекты, организованные с помощью new имеют неограниченное время жизни. Поэтому область памяти должна освобождаться оператором delete.
  2. Если резервируется память для массива, то операция new возвращает указатель на первый элемент массива.
  3. При резервировании памяти для массива все размерности должны быть выражены положительными величинами.
  4. Массивы нельзя инициализировать.
  5. Объекты классов могут организовываться с помощью операции new, если класс имеет конструктор по умолчанию.
  6. Ссылки не могут организовываться с помощью операции new, так как для них не выделяется память.
  7. Операция new самостоятельно вычисляет потребность в памяти для организуемого типа данных, поэтому первый параметр операции всегда имеет тип size_t.
Обработка ошибок операции new.

Обработка ошибок операции new происходит в два этапа:

  1. Устанавливается, какие предусмотрены функции для обработки ошибок. Собственные функции должны иметь тип new_handler и создаются с помощью функции set_new_handler. В файле new.h объявлены
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler new_p);
  2. Вызывается соответствующая new_handler функция. Эта функция должна:

Диагностический класс bad_alloc объявлен в new.h.

В реализации ВС++ включена специальная глобальная переменная _new_handler, значением которой является указатель на new_handler функцию, которая выполняется при неудачном завершении new. По умолчанию, если операция new не может выделить требуемое количество памяти, формируется исключение bad_alloc. Изначально это исключение называлось xalloc и определялось в файле except.h. Исключение xalloc продолжает использоваться во многих компиляторах. Тем не менее, оно вытесняется определенным в стандарте С++ именем bad_alloc.

Рассмотрим несколько примеров.

Пример 1. В примере использование блока try ... catch дает возможность проконтролировать неудачную попытку выделения памяти.

#include <iostream>
#include <new>

int main(void)
{
  double *p;
  try {
    p = new double[1000];
    cout << "Память выделилась успешно" << endl;
  }
  catch (bad_alloc xa) {
    cout << "Ошибка выделения памяти\n";
    cout << xa.what();
    return 1;
  }
  return 0;
}

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

#include <iostream>
#include <new>

int main(void)
{
  double *p;
  do
  {
    try {
      p = new double[1000];
      cout << "Память выделилась успешно" << endl;
    }
    catch (bad_alloc xa) {
      cout << "Ошибка выделения памяти\n";
      cout << xa.what();
    }
  }
  while (p);
  return 0;
}

Пример 3. Демонстрируется перегруженная форма операции new - операция new (nothow).

#include <iostream>
#include <new>

int main(void)
{
  double *p;
  struct nothrow noth_ob;
  do
  {
    p = new(noth_ob) double[1000];
    if (!p) cout << "Ошибка выделения памяти\n";
    else cout << "Память выделилась успешно\n";
  }
  while (p);
  return 0;
}

Пример 4. Демонстрируются различные формы перегрузки операции new.

#include <iostream.h>
#include <new.h>

double *p, *q, **pp;

class Demo
{
public:
  Demo()
  {
    value = 0; 
  }
  Demo(int i)
  {
    value = i; 
  }

  void *operator new(size_t, int, int);
  void *operator new(size_t, int);
  void *operator new(size_t, char *);

protected:
  int value;
};

void *Demo::operator new(size_t t, int i, int j)
{
  if (j) return new(i) Demo;
  return NULL;
}

void *Demo::operator new(size_t t, int i)
{
  Demo *p = ::new Demo;
  (*p).value = i;
  return p;
}

void *Demo::operator new(size_t t, char *z)
{
  return ::new (z) Demo;
}

int main(void)
{
  Demo *p_ob1, *p_ob2;
  struct nothrow noth_ob;
  p = new double;
  pp = new double *;
  p = new double(1.2); // инициализация
  q = new double[3];    // массив
  p_ob1 = new Demo[10]; // массив объектов demo
  void (**f_ptr)(int) // указатель на указатель на функцию
  f_ptr = new(void(*[3])(int)) // массив указателей на функцию
  char z[sizeof(Demo)]; // резервируется память в соответствии с величиной demo
  p_ob2 = new(z) Demo; // организуется demo-объект в области памяти на
                        // которую указывает переменная z
  p_ob2 = new(3) Demo; // demo-объект с инициализацией
  p_ob1 = new(3, 0) Demo; // возвращает указатель NULL
  p_ob2 = new(noth_ob) Demo[5]; // массив demo-объектов,
                                 // в случае ошибки возвращает NULL
  return 0;
}

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

Операция-функция delete бывает двух видов:

void operator delete(void *);
void operator delete(void *, size_t);

Вторая форма включает аргумент типа size_t, передаваемый delete. Он передается компилятору как размер объекта, на который указывает p.

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

Основные правила перегрузки операций.

  1. Вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка С++ , нельзя.
  2. Не все операции языка С++ могут быть перегружены. Нельзя перегрузить следующие операции:
    . – прямой выбор компонента,
    .* – обращение к компоненту через указатель на него,
    ? : – условная операция,
    :: – операция указания области видимости,
    sizeof, # и ## – препроцессорные операции.
  3. Каждая операция, заданная в языке, имеет определенное число операндов, свой приоритет и ассоциативность. Все эти правила, установленные для операций в языке, сохраняются и для ее перегрузки, т.е. изменить их нельзя.
  4. Любая унарная операция @ определяется двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. Выражение @z означает в первом случае вызов z.operator @(), во втором - вызов operator @(z).
  5. Любая бинарная операция @ определяется также двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае x @ y означает вызов x.operator @(y), во втором – вызов operator @(x, y).
  6. Перегруженная операция не может иметь аргументы (операнды), заданные по умолчанию.
  7. В языке С++ установлена идентичность некоторых операций, например, ++z – это тоже, что и z += 1. Эта идентичность теряется для перегруженных операций.
  8. Функцию operator можно вызвать по ее имени, например, z = operator * x, y) или z = x.operator *(y). В первом случае вызывается глобальная функция, во втором – компонентная функция класса X, и x – это объект класса X. Однако, чаще всего функция operator вызывается косвенно, например, z = x * y.
  9. За исключением перегрузки операций new и delete функция operator должна быть либо нестатической компонентной функцией, либо иметь как минимум один аргумент (операнд) типа "класс" или "ссылка на класс" (если это глобальная функция).
  10. Операции '=', '[]', '–>' можно перегружать только с помощью нестатической компонентной функции operator @. Это гарантирует, что первыми операндами будут леводопустимые выражения.
  11. Операция '[]' рассматривается как бинарная. Пусть a – объект класса A, в котором перегружена операция '[]'. Тогда выражение a[i] интерпретируется как a.operator [](i).
  12. Операция '()' вызова функции рассматривается как бинарная. Пусть a – объект класса A, в котором перегружена операция '()'. Тогда выражение a(x1, x2, x3, x4) интерпретируется как a.operator ()(x1, x2, x3, x4).
  13. Операция '–>' доступа к компоненту класса через указатель на объект этого класса рассматривается как унарная. Пусть a – объект класса A, в котором перегружена операция '–>'. Тогда выражение a–>m интерпретируется как (a.operator–>())–>m. Это означает, что функция operator –>() должна возвращать указатель на класс A, или объект класса A, или ссылку на класс A.
  14. Перегрузка операций '++' и '--', записываемых после операнда (z++, z--), отличается добавлением в функцию operator фиктивного параметра int, который используется только как признак отличия операций z++ и z-- от операций ++z и --z.
  15. Глобальные операции new можно перегрузить и в общем случае они могут не иметь аргументов (операндов) типа "класс". В результате разрешается иметь несколько глобальных операций new, которые различаются путем изменения числа и (или) типов аргументов.
  16. Глобальные операции delete не могут быть перегружены. Их можно перегрузить только по отношению к классу.
  17. Заданные в самом языке глобальные операции new и delete можно изменить, т.е. заменить версию, заданную в языке по умолчанию, на свою версию.
  18. Локальные функции operator new() и operator delete() являются статическими компонентами класса, в котором они определены, независимо от того, использовался или нет спецификатор static (это, в частности, означает, что они не могут быть виртуальными).
  19. Для правильного освобождения динамической памяти под базовый и производный объекты следует использовать виртуальный деструктор.
  20. Если для класса X операция "=" не была перегружена явно и x и y - это объекты класса X, то выражение x = y задает по умолчанию побайтовое копирование данных объекта y в данные объекта x.
  21. Функция operator вида operator type() без возвращаемого значения, определенная в классе A, задает преобразование типа A к типу type.
  22. За исключением операции присваивания '=' все операции, перегруженные в классе X, наследуются в любом производном классе Y.
  23. Пусть X – базовый класс, Y – производный класс. Тогда локально перегруженная операция для класса X может быть далее повторно перегружена в классе Y.
Предыдущая Оглавление Следующая