Перегрузка операторов в C++. Операторная функция. Ключевое слово operator. Перегрузка базовых арифметических операторов

Перегрузка операторов в C++. Операторная функция. Ключевое слово operator. Перегрузка базовых арифметических операторов +, , *, /. Примеры реализации встроенных операторных функций

Данная тема отображает возможности языка C++ по реализации «перегрузки» операторов. Не все современные языки программирования поддерживают перегрузку операторов. Хорошое понимание процесса программирования перегруженных операторов есть показателем профессиональности и высокого мастерства современного программиста.

Перед рассмотрением данной темы рекомендуется ознакомиться со следующей темой:


Содержание



1. Что такое унарные и бинарные операторы?

Различают три основных вида операторов: унарные, бинарные и n-арные (n>2).

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

Примеры унарных операторов:

i++
--a
-8

Бинарные операторы – это операторы, которые для вычисления требуют двух операндов.

Пример. Ниже отображены фрагменты выражений с бинарными операторами +, , %, *

a+b
f1-f2
c%d
x1*x2

n-арные операторы для вычислений требуют более двух операндов. В языке C++ есть тернарная операция ?:, которая для своей работы требует три операнда. Более подробно об этой тернарной операции ?: описывается здесь.

 

2. В чем состоит суть перегрузки операторов? Что такое операторная функция?

Язык C++ имеет широкие возможности для перегрузки большинства операторов. Перегрузка оператора означает использование оператора для оперирования объектами классов. Перегрузка оператора – способ объявления и реализации оператора таким образом, что он обрабатывает объекты конкретных классов или выполняет некоторые другие действия. При перегрузке оператора в классе вызывается соответствующая операторная функция (operator function), которая выполняет действия, которые касаются данного класса.
Если оператор «перегружен», то его можно использовать в других методах в обычном для него виде. Например, команды поэлементного суммирования двух массивов a1 и a2

a1.add(a2);
a3 = add(a1, a2);

лучше вызвать более естественном способом:

a1 = a1 + a2;
a3 = a1 + a2;

В данном примере оператор ‘+’ считается перегруженным.

 

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

Для заданного класса операторную функцию в классе можно реализовать:

  • внутри класса. В этом случае, операторная функция есть методом класса;
  • за пределами класса. В этом случае операторная функция объявляется за пределами класса как «дружественная» (с ключевым словом friend). Более подробно о реализации «дружественных» функций за пределами класса описывается здесь.

 

4. Общая форма операторной функции, которая реализована в классе. Ключевое слово operator

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

return_type ClassName::operator#(arguments_list)
{
    // некоторые операции
    // ...
}

где

  • return_type – тип значения, которое возвращается операторной функцией;
  • ClassName – имя класса, в котором реализована операторная функция;
  • operator# – ключевое слово, определяющее операторную функцию в классе. Символ # заменяется оператором языка C++, который перегружается. Например, если перегружается оператор +, то нужно указать operator+;
  • argument_list – список параметров, которые получает операторная функция. Если перегружается бинарный оператор, то argument_list содержит один аргумент. Если перегружается унарный оператор, то список аргументов пустой.

 

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

Объявляется класс Point, реализующий точку на координатной плоскости. В классе реализованы:

  • две внутренние переменные x, y, которые есть координатами точки;
  • два конструктора класса;
  • методы доступа к внутренним переменным класса GetX(), GetY(), SetX(), SetY();
  • две операторные функции operator+() и operator-().
// Класс, который реализует точку на координатной плоскости
// класс содержит две операторные функции
class Point
{
private:
    int x, y; // координаты точки

public:
    // конструкторы класса
    Point()
    {
        x = y = 0;
    }

    Point(int nx, int ny)
    {
        x = nx;
        y = ny;
    }

    // методы доступа к членам класса
    int GetX(void) { return x; }
    int GetY(void) { return y; }
    void SetX(int nx) { x = nx; }
    void Set(int ny) { y = ny; }

    // перегруженный бинарный оператор '+'
    Point operator+(Point pt)
    {
        // p - временный объект, который создается с помощью конструктора без параметров
        Point p;
        p.x = x + pt.x;
        p.y = y + pt.y;
        return p;
    }

    // перегруженный унарный оператор '-'
    Point operator-(void)
    {
        Point p;
        p.x = -x;
        p.y = -y;
        return p;
    }
};

Как видно из вышеприведенного кода, операторная функция operator+() получает один параметр. Это значит, что эта функция реализует бинарный оператор ‘+’. Этот параметр соответствует операнду, который размещается в правой части бинарного оператора ‘+’. Операнд, который размещается в левой части оператора ‘+’ передается операторной функции неявно с помощью указателя this данного класса.
Вызов операторной функции осуществляет объект, который размещается в левой части оператора присваивания.
Демонстрация использования перегруженных операторов класса Point в другом методе:

// объявление переменных - объектов класса CPoint
Point P1(3,4);
Point P2(5,7);
Point P3;
int x, y; // дополнительные переменные

// 1. Использование перегруженного бинарного оператора '+'
P3 = P1 + P2; // объект P1 вызывает операторную функцию

// проверка
x = P3.GetX(); // x = 8
y = P3.GetY(); // y = 11

// 2. Использование перегруженного унарного оператора '-'
P3 = -P2;
x = P3.GetX(); // x = -5
y = P3.GetY(); // y = -7

В вышеприведенном коде, в операции суммирования ‘+’ объект P1 вызывает операторную функцию. То есть, фрагмент строки

P1 + P2

заменяется вызовом

P1.operator+(P2)

Реализовать операторную функцию operator+() в классе можно и по другому

Point operator+(Point pt)
{
    // вызов конструктора с двумя параметрами
    return Point(x+pt.x, y+pt.y); // создается временный объект, который затем копируется
}

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

// конструктор с двумя параметрами
Point(int nx, int ny)
{
    // ...
}

то вышеприведенный вариант функции operator+() работать не будет, так как для создания объекта типа Point эта функция использует конструктор с двумя параметрами. В этом случае компилятор выдаст сообщение

Point::Point : no overloaded function takes 2 arguments

что значит: нет метода (конструктора) Point::Point() принимающего 2 аргумента.

 

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

В примере реализуется операторная функция operator*(), которая умножает поэлементно значения внутренних массивов объектов класса ArrayFloat. Если размер массивов неодинаков, то умножается только то число элементов, которое есть минимальным между двумя размерами массивов.

// массив вещественных чисел
class ArrayFloat
{
private:
    float A[10]; // массив вещественных чисел, фиксированный размер массива
    int size;

public:
    ArrayFloat()
    {
        size = 0;
    }

    ArrayFloat(int nsize, float nA[])
    {
        size = nsize;
        for (int i=0; i<nsize; i++)
        A[i] = nA[i];
    }

    // методы доступа
    float GetAi(int i)
    {
        if ((i>=0) && (i<=size-1))
            return A[i];
        else
            return 0;
    }

    void SetAi(int i, float value)
    {
        if ((i>=0) && (i<=size-1))
            A[i] = value;
    }

    // перегруженный оператор '*'
    ArrayFloat operator*(ArrayFloat AF)
    {
        ArrayFloat tmp;
        int n;

        if (size<AF.size)
            n = AF.size;
        else
            n = size;

        for (int i=0; i<n; i++)
            tmp.A[i] = A[i] * AF.A[i];
        tmp.size = n;
        return tmp;
    }
};

Использование класса ArrayFloat в другом методе

// дополнительные переменные и массивы
float x, y;
float AF1[] = { 2, 5, 7, 9, 12 };
float AF2[] = { 3, 4, 9, 8, 10, 13 };

// создать объекты класса ArrayFloat
ArrayFloat A1(5, AF1);
ArrayFloat A2(6, AF2);
ArrayFloat A3;

// вызов операторной функции operator*
A3 = A1 * A2; // осуществляется поэлементное умножение

// проверка
x = A3.GetAi(0); // x = 6
y = A3.GetAi(1); // y = 20
x = A3.GetAi(2); // x = 63
y = A3.GetAi(4); // y = 120

 

7. Пример суммирования двух массивов. Операторная функция operator+() размещается внутри класса

Задан класс ArrayFloat, реализующий динамический массив чисел типа float. В классе реализованы:

  • внутренние переменные size, A, описывающие размер массива и сам массив;
  • два конструктора, инициализирующие начальными значениями элементы массива;
  • конструктор копирования;
  • методы доступа GetSize(), SetSize(), GetAi(), SetAi(), которые реализуют доступ к внутренним переменным массива с соответствующими операциями (выделение памяти, проверка на допустимые границы);
  • операторная функция operator=(), которая реализует копирование объектов;
  • операторная функция operator+(), которая реализует перегрузку оператора ‘+’ для массивов типа ArrayFloat. Операторная функция добавляет поэлементно значения массивов, которые являются операндами операции ‘+’.
  • деструктор.

 

// класс - массив типа float
class ArrayFloat
{
private:
    int size; // размер массива
    float * A; // динамический размер массива

public:
    // конструкторы класса
    // конструктор без параметров
    ArrayFloat()
    {
        size = 0;
        A = NULL;
    }

    // конструктор с двумя параметрами
    ArrayFloat(int nsize, float * nA)
    {
        if (size>0)
            delete A;

        size = nsize;
            A = new float[size];

        for (int i=0; i<nsize; i++)
            A[i] = nA[i];
    }

    // конструктор копирования
    ArrayFloat(const ArrayFloat& _A)
    {
        size = _A.size;
        A = new float[size];
        for (int i = 0; i < size; i++)
            A[i] = _A.A[i];
    }

    // методы доступа
    int GetSize(void) { return size; }

    void SetSize(int nsize)
    {
        if (size>0)
            delete A;

        size = nsize;
        A = new float[size]; // выделить новый фрагмент памяти

        // заполнить массив нулями
        for (int i=0; i<size; i++)
            A[i] = 0.0f;
    }

    float GetAi(int index)
    {
        if ((index>=0) && (index<size))
            return A[index];
        else
            return 0;
    }

    void SetAi(int index, float value)
    {
        if ((index>=0) && (index<size))
            A[index] = value;
    }

    // оператор копирования operator=()
    ArrayFloat operator=(const ArrayFloat& _A)
    {
        if (size > 0)
            delete[] A;
        size = _A.size;
        A = new float[size]; // выделить память

        for (int i = 0; i < size; i++)
            A[i] = _A.A[i];

        return *this;
    }
    // перегрузка оператора '+',
    // операторная функция
    ArrayFloat operator+(ArrayFloat AF2)
    {
        int n;

        // взять минимальный размер из двух массивов
        if (size<AF2.size) n = size;
        else n = AF2.size;

        ArrayFloat tmpA; // объект класса
        tmpA.SetSize(n); // новый размер массива

        // поэлементное суммирование
        for (int i=0; i<n; i++)
            tmpA.A[i] = A[i] + AF2.A[i];
        return tmpA;
    }

    // деструктор
    ~ArrayFloat()
    {
        if (size > 0)
            delete[] A;
    }
};

Ниже продемонстрировано использование класса ArrayFloat и операторной функции operator+() этого класса.

// дополнительные переменные, массивы
float x, y;
float F1[] = { 3.8f, 2.9f, 1.5f };
float F2[] = { 4.3f, 1.5f, 7.0f, 3.3f };

// объявление объектов класса ArrayFloat
ArrayFloat AF1(3, F1);
ArrayFloat AF2(4, F2);

// проверка
x = AF1.GetAi(0); // x = 3.8
y = AF2.GetAi(3); // y = 3.3

// объявление дополнительного объекта - результата
ArrayFloat AF3;
AF3 = AF1 + AF2; // вызов операторной функции operator+

// проверка
x = AF3.GetAi(0); // x = 3.8 + 4.3 = 8.1
y = AF3.GetAi(1); // y = 2.9 + 1.5 = 4.4

// еще один вызов
AF3 = AF1 + AF1 + AF2;

x = AF3.GetAi(1); // x = 2.9 + 2.9 + 1.5 = 7.3
y = AF3.GetAi(2); // y = 1.5 + 1.5 + 7.0 = 10.0

 

8. Какие ограничения накладываются на перегруженные операторы?

На использование перегруженных операторов накладываются следующие ограничения:

  • при перегрузке оператора нельзя изменить приоритет этого оператора;
  • нельзя изменить количество операндов оператора. Однако, в коде операторной функции можно один из параметров (операндов) не использовать;
  • нельзя перегружать операторы ::, ., *, ?:;
  • нельзя вызвать операторную функцию с аргументами по умолчанию. Исключение – операторная функция вызова функции operator()().

 

9. Какие операторы нельзя перегружать?

Нельзя перегружать следующие операторы:

  • :: – расширение области видимости;
  • . (точка) – доступ к члену структуры или класса;
  • * – доступ по указателю;
  • ?: – тернарная операция.

 

10. Объекты каких типов может возвращать операторная функция? Примеры операторных функций, которые возвращают объекты разных типов

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

Пример. Задан класс Complex, в котором перегружаются два оператора:

  • унарный оператор ‘+’, возвращающий модуль комплексного числа (тип double);
  • бинарный оператор ‘+’, возвращающий сумму комплексных чисел. Операторная функция возвращает объект типа Complex;
  • бинарный оператор ‘+’, который добавляет к комплексному числу некоторое вещественное число. В этом случае операторная функция получает входным параметром вещественное число и возвращает объект типа Complex.

Текст класса следующий:

// класс Complex
class Complex
{
private:
    float real; // действительная часть
    float imag; // мнимая часть

public:
    // конструкторы
    Complex(void)
    {
        real = imag = 0;
    }

    Complex(float _real, float _imag)
    {
        real = _real;
        imag = _imag;
    }

    // методы доступа
    float GetR(void) { return real; }
    float GetI(void) { return imag; }

    void SetRI(float _real, float _imag)
    {
        real = _real;
        imag = _imag;
    }

    // объявление операторной функции, перегружающей бинарный '+'
    // функция возвращает объект, содержащий сумму двух комплексных чисел
    Complex operator+(Complex c)
    {
        Complex c2; // временный объект

        // суммирование комплексных чисел
        c2.real = real + c.real;
        c2.imag = imag + c.imag;

        return c2;
    }

    // объявление операторной функции, перегружающей унарный '+'
    // функция возвращает модуль комплексного числа
    float operator+(void)
    {
        float res;
        res = std::sqrt(real*real+imag*imag);
        return res;
    }

    // объявление операторной функции operator+()
    // функция добавляет к комплексному числу некоторое число, которое есть входящим параметром
    Complex operator+(float real)
    {
        Complex c2; // результирующий объект
        c2.real = this->real + real;
        c2.imag = this->imag;
        return c2;
    }
};

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

Complex c1(1,5);
Complex c2(3,-8);
Complex c3; // результирующий объект
double d;

// проверка
c3 = c1 + c2;
d = c3.GetR(); // d = 1 + 3 = 4
d = c3.GetI(); // d = 5 + (-8) = -3

// перегруженный унарный оператор '+'
d = +c1; // d = |1 + 5j| = 5.09902 - модуль числа
d = +c2; // d = |3 + (-8)j| = 8.544

// вызов перегруженного бинарного '+',
// добавить к комплексному числу число
c3 = c1 + 5.0;
d = c3.GetR(); // d = 1 + 5 = 6

 

11. Можно ли изменять значения операндов в операторной функции?

Да, можно. Однако такие действия не являются полезными с точки зрения здравого смысла. Так, например, операция умножения

6 * 9

не изменяет значения своих операндов 6 и 9. Результат равен 54. Если операторная функция operator*() будет изменять значения своих операндов, то это может привести к невидимым ошибкам в программах, поскольку программист по привычке, будет считать, что значения операндов есть неизменными.

 

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

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

cannot overload functions distinguished by return type alone

что значит

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

Например. Нельзя в классе перегружать оператор ‘+’ так как показано ниже

class SomeClass
{
    // ...

    SomeClass operator+(SomeClass c1)
    {
        // ...
    }

    double operator+(SomeClass c1) // это есть ошибка!
    {
        // ...
    }

    // ...
}

Это правило касается любых функций класса.

 

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

Да, можно. В п. 10 реализован класс Complex, в котором реализованы две операторные функции, которые перегружают оператор ‘+’ разными способами. Эти функции отличаются входящими параметрами.

 


Связанные темы