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

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

Дана тема відображає можливості мови C++ з реалізації перевантаження операторів. Не всі сучасні мови програмування підтримують перевантаження операторів. Добре розуміння процесу програмування перевантажених операторів є показником професійності та високого класу сучасного програміста.

Перед розглядом даної теми рекомендується ознайомитись з наступною темою:


Зміст



1. Що таке унарні та бінарні оператори?

Розрізняють три основні види операторів: унарні, бінарні та n-арні.
Унарні оператори – це оператори, які для обчислення вимаюгають одного операнда, який може розміщуватись справа або зліва від самого оператора.
Приклади унарних операторів:

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-(). Операторна функція 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 SetY(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+(), яка реалізує перевантаження оператора ‘+’ для масивів типу 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];
    }

    // методи доступу
    int GetSize(void) { return size; }
    void SetSize(int nsize)
    {
        if (size>0)
        {
            delete A;
            size = 0;
        }

        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;
    }

    // перевантаження оператора '+',
    // операторна функція
    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 та операторної функції 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, в якому реалізовано дві операторні функції, що перевантажують оператор ‘+’ різними способами. Ці функції відрізняються вхідними параметрами.

 


Зв’язані теми