سربارگذاری عملگرها (Operator Overloading)

سربارگذاری عملگرها به شما اجازه می‌دهد که رفتار عملگرهای ++C را بسته به نوع عملوندهای آنها سفارشی کنید. سربارگذاری عملگرها همچنین به عملگر اجازه می‌دهد که یک شیء را به روشی دیگر ترجمه کند. به کد زیر توجه کنید :

   1: #include <iostream>
   2: #include <string>
   3: using namespace std;
   4: 
   5: class MyNumber
   6: {
   7:     public: 
   8:         int number;        
   9: };
  10: 
  11: int main()
  12: {
  13:     MyNumber firstNumber;
  14:     MyNumber secondNumber;
  15: 
  16:     firstNumber.Number = 10;
  17:     secondNumber.Number = 5;
  18: 
  19:     MyNumber sum = firstNumber + secondNumber;
  20: 
  21:     cout << "Sum = " << sum.Number;
  22: }

خط پررنگ شده در کد بالا (خط 19) کد قابل قبولی نیست چون کامپایلر نمی‌تواند دو شیء را با هم جمع کند. رفتاری که ما از کد بالا انتظار داریم اضافه کردن مقادیر به خاصیت Number دو عملوند و سپس ایجاد یک شیء جدید که حاصل جمع دو مقدار در داخلان قرار بگیرد. سپس این شیء جدید به متغیر sum تخصیص داده شود.

   1: #include <iostream>
   2: #include <string>
   3: using namespace std;
   4: 
   5: class MyNumber
   6: {
   7:     public: 
   8:         int number;
   9: 
  10:         friend MyNumber operator +(MyNumber n1, MyNumber n2)
  11:         {                                                   
  12:             MyNumber result;                                
  13:             result.number = n1.number + n2.number;          
  14:             return result;                                  
  15:         }                                                   
  16: };
  17: 
  18: int main()
  19: {
  20:     MyNumber firstNumber;
  21:     MyNumber secondNumber;
  22: 
  23:     firstNumber.number = 10;
  24:     secondNumber.number = 5;
  25: 
  26:     MyNumber sum = firstNumber + secondNumber;
  27: 
  28:     cout << "Sum = " << sum.number;
  29: }
Sum = 15

برای سربارگذاری عملگرها به صورت زیر عمل کنید :

class className
{
    public:
       returnType operator symbol (arguments)
       {

       } 
};

همانطور که مشاهده می‌کنید در سربارگذاری عملگرها از یک متد در داخل کلاس استفاده می‌شود. همانطور که در کد بالا مشاهده می‌کنید بعد از کلمه کلیدی operator از یک عملگر مانند + یا – استفاده می‌کنیم.

مزایای سربارگذاری عملگرها

  • سربارگذاری عملگرها برنامه نویس را قادر می‌سازد تا خوانایی برنامه نویس را بالاتر ببرد. برای مثال برای جمع کردن ماتریس‌ها نوشتن M1+M2 خوانایی بالاتری نسبت به انجام عملیات جمع با استفاده از یک تابع مانند M1.add(M2) دارد.
  • در سربارگذاری عملگرها از ساختاری مشابه عملگرهای از پیش تعریف شده پیروی می‌شود.
  • سربارگذاری عملگرها باعث درک آسان‌تر برنامه می‌شود.

محدودیت‌های سربارگذاری عملگرها

  • فقط عملگرهای پیشفرض مانند +، -، *، / و … می‌توانند سربارگذاری شوند.
  • تعداد عملوندهای یک عملگر نمی‌توانند تغییر کنند. برای مثال نمی‌تواند برای عملگر + سه عملوند را در نظر گرفت.
  • اولویت عملگرها را نمی‌توان تغییر داد.
  • عملگر سربارگذاری شده حداقل باید یک عملوند داشته باشد.
  • عملگرهای =، []، ()، -> باید به عنوان member function (داخل یک کلاس) تعریف شوند. عملگر باقیمانده تقسیم هم می‌تواند عضو یک تابع باشد و هم می‌تواند نباشد.
  • برخی از عملگرها مانند =، & و کاما به صورت پیش فرض از قبل سربارگذاری شده‌اند.
  • عملگرهایی مانند::،. و?: نمی‌توانند سربارگذاری شوند.

برای سربارگذاری عملگرها باید نکات زیر را در نظر بگیرید:

سربارگذاری عملگرها می‌تواند به دو روش پیاده سازی شود که عبارتند از:

  • استفاده از یک member function
  • با استفاده از یک friend function

تفاوت بین این دو روش را می‌توانید در جدول زیر مشاهده کنید:

friend function member function
تعداد پارامترهایی که می‌تواند پاس داده شود بیشتر است تعداد پارامترهایی که می‌تواند پاس داده شود فقط یکی است و شیء فراخوانی شده به صورت ضمنی فقط یک عملوند دارد.
عملگرهای Unary فقط یک پارامتر را به صورت صریح می‌گیرند عملگرهای Unary هیچ پارامتر صریحی نمی‌گیرند
عملگرهای Binary دو پارامتر را به صورت صریح می‌گیرند عملگرهای Binary فقط یک پارامتر صریح می‌گیرند
عملوند سمت چپ به شیء یک کلاس نیاز ندارد عملوند سمت چپ باید به وسیله شیء فراخوانی شود
نوشتن موارد زیر مجاز است

Obj2=Obj+10

Obj2=10+Obj1

نوشتن Obj2=Obj1+10 مجاز است ولی Obj2=10+Obj1 مجاز نیست

سربارگذار عملگرهای تک عملوندی (Unary)

عملگرهایی که فقط با یک عملوند کار می‌کنند به عنوان عملگرهای Unary شناخته می‌شوند. برای مثال ++ (عملگر افزایش)، – – (عملگر کاهش)، – (عملگر منهای تک عملوندی)،! (عملگر منطقی نقیض) و … در این دسته قرار دارند. برنامه زیر نشان می‌دهد که چگونه عملگر منهای تک عملوندی (-) را به وسیله تابع عضو سربارگذاری می‌کنیم:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;

    public:
        Number(int x)
        {
            this->x = x;
        }

        void operator -()
        {
            x = -x;
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number n1(10);
    -n1;
    n1.display();
}
-10

در برنامه بالا مقدار x تغییر می‌کند و با استفاده از تابع ()display چاپ می‌شود. همانطور که در برنامه زیر مشاهده می‌کنید ما می‌توانیم یک شیء از کلاس Number را نیز بعد از تغییر x برگردانیم:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;

    public:
        Number(int x)
        {
            this->x = x;
        }

        Number operator -()
        {
            x = -x;
            return Number(x);
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number n1(10);
    Number n2 = -n1;
    n2.display();
}
-10

زمانی که یک friend function برای سربارگذاری یک عملگر Unary مورد استفاده قرار می‌گیرد باید به نکات زیر توجه شود:

  • تابع فقط یک عملوند را به عنوان پارامتر قبول می‌کند.
  • عملوند یک شیء از یک کلاس است.
  • تابع می‌تواند به اعضای خصوصی (private) فقط از طریق شیء دسترسی داشته باشد.
  • تابع ممکن است یک مقدار برگرداند و ممکن است هیچ مقداری را بر نگرداند.

برنامه زیر نشان می‌دهد که چگونه می‌توان یک عملگر Unary را با استفاده از friend function سربارگذاری کرد:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;
    public:
        Number(int x)
        {
            this->x = x;
        }

        friend Number operator -(Number &);

        void display()
        {
            cout << "x = " << x << endl;
        }
};

Number operator -(Number &n)
{
    return Number(-n.x);
}

int main()
{
    Number n1(20);
    Number n2 = -n1;
    n2.display();
}
-20

سربارگذاری عملگرهای پیشوندی (prefix)

ساختار کلی سربارگذاری عملگرهای پشوندی افزایش (++) و کاهش (–) به شکل زیر می‌باشد:

return-type operator ++()
{

}

برنامه زیر نشان می‌دهد چگونه می‌توانیم عملگرهای پیشوندی کاهش و افزایش را سربارگذاری کنیم:

#include <iostream>
using namespace std;
class Number
{
    private:
        int x;

    public:
        Number(int x)
        {
            this->x = x;
        }

        Number operator ++()
        {
            x = x + 1;
            return Number(x);
        }

        Number operator --()
        {
            x = x - 1;
            return Number(x);
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number n1(20);
    Number n2 = ++n1;
    n2.display();
    Number n3(20);
    Number n4 = --n3;
    n4.display();
}
x = 21
x = 19

سربارگذاری عملگرهای پسوندی (postfix)

برای سربارگذاری عملگرهای پسوندی افزایش و کاهش ما باید یک int اضافه را به عنوان پارامتر مشخص کنیم تا از تداخل با عملگرهای پیشوندی جلوگیری کند. ساختار کلی سربارگذاری عملگر پسوندی افزایش به صورت زیر است:

return-type operator ++(int)
{

}

پارامتر int یک پارامتر اضافی است و نیازی به دریافت آن نداریم. برنامه زیر نشان می‌دهد که چگونه می‌توانیم عملگرهای پسوندی افزایش و کاهش را سربارگذاری کنیم:

#include <iostream>
using namespace std;
class Number
{
    private:
        int x;

    public:
        Number(int x)
        {
            this->x = x;
        }

        Number operator ++(int)
        {
            return Number(x++);
        }

        Number operator --(int)
        {
            return Number(x--);
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number n1(20);
    Number n2 = n1++;
    n1.display();
    n2.display();
    Number n3 = n2--;
    n3.display();
    n2.display();
}
x = 21
x = 20
x = 20
x = 19

سرباگذاری عملگرهای باینری

همانطور که عملگرهای Unary می‌توانند سربارگذاری شوند، همچنین ما می‌توانیم عملگرهای باینری را نیز سربارگذاری کنیم. ساختار سربارگذاری یک عملگر باینری با استفاده از member function به صورت زیر می‌باشد:

return-type operator op(ClassName &)
{

}

ساختار سربارگذاری یک عملگر باینری با استفاده از friend function به صورت زیر می‌باشد:

return-type operator op(ClassName &, ClassName &)
{

}

برنامه زیر نشان می‌دهد که چگونه می‌توان عملگر باینری + را با استفاده از member function سربارگذاری کرد:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;

    public:
        Number() {}

        Number(int x)
        {
            this->x = x;
        }

        Number operator +(Number &n)
        {
            Number temp;
            temp.x = x + n.x;
            return temp;
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number n1(20);
    Number n2(10);
    Number n3 = n1 + n2;
    n3.display();
}
x = 30

برنامه زیر نشان می‌دهد که چگونه می‌توان عملگر باینری + را با استفاده از friend function سربارگذاری کرد:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;

    public:
        Number() {}

        Number(int x)
        {
            this->x = x;
        }

        friend Number operator +(Number &, Number &);

        void display()
        {
            cout << "x = " << x << endl;
        }
};

Number operator +(Number &n1, Number &n2)
{
    Number temp;
    temp.x = n1.x + n2.x;
    return temp;
}

int main()
{
    Number n1(20);
    Number n2(10);
    Number n3 = n1 + n2;
    n3.display();
}
x = 30

عملگرهایی از قبیل =، ()، [] و -> نمی‌توانند با استفاده از یک friend function سربارگذاری شوند.

سربارگذاری عملگرهای خاص

برخی از عملگرهای خاص در C++ عبارتند از:

  • new : به منظور تخصیص حافظه مورد استفاده قرار می‌گیرد.
  • delete : به منظور آزاد کردن حافظه مورد استفاده قرار می‌گیرد.
  • ( ) و []: عملگرهای زیرمجموعه.
  • -> : عملگر دسترسی به اعضاء.

++C به برنامه نویس اجازه می‌دهد تا عملگرهای new و delete را سربارگذاری کند که وظایف این عملگرهای عبارتند از:

  • به منظور افزودن ویژگی‌های بیشتر در هنگام تخصیص و آزاد کردن حافظه
  • به کاربران اجازه می‌دهد تا برنامه خود را دیباگ کنند و عملیات تخصیص و ازاد سازی حافظه را ردیابی کنند.

ساختار سربارگذاری عملگر new به صورت زیر می‌باشد:

void* operator new(size_t size);

پارامتر size، مشخص می‌کند چه میزان حافظه برای نوع داده size_t تخصیص داده شود. ساختار سربارگذاری عملگر delete به صورت زیر می‌باشد:

void operator delete(void*);

این تابع یک پارامتر از نوع void* دریافت می‌کند و هیچ چیز بر نمی‌گرداند. هر دو تابعی که برای سربارگذاری new و delete نوشته‌ایم به صورت پیش فرض از نوع static هستند و با this نمی‌توان به آن‌ها دسترسی داشت. برای حذف یک آرایه از اشیاء عملگر []delete باید سربارگذاری شود. برنامه زیر سربارگذاری عملگرهای new و delete را نشان می‌دهد:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x;

    public:
        Number(int x)
        {
            this->x = x;
        }

        void* operator new(size_t size)
        {
            void *ptr = ::new int[size];
            cout << "Memory allocated of size: " << size << endl;
            return ptr;
        }

        void operator delete(void *ptr)
        {
            cout << "Memory deallocated" << endl;
        }

        void display()
        {
            cout << "x = " << x << endl;
        }
};

int main()
{
    Number *n = new Number(10);
    n->display();
    delete n;
}
Memory allocated of size: 4
x = 10
Memory deallocated

در برنامه بالا new:: و delete:: به عملگرهای new و delete سراسری اشاره می‌کنند. زمانی که new فراخونی می‌شود، کامپایلر تابع سربارگذاری شده برای new را فراخوانی می‌کند و همچنین به صورت خودکار متد سازنده را نیز فراخوانی می‌کند. سربارگذاری عملگرهای new و delete دارای مزایای زیر است :

  • تابع سربارگذاری شده عملگر new می‌تواند یک چند پارامتر را دریافت کند. این کار باعث انعطاف پذیری و شخصی سازی حافظه تخصیص داده شده می‌شود.
  • تابع سربرگذاری شده عملگر delete عملیات زباله روب (garbage collection) را برای اشیاء کلاس‌ها ارائه می‌دهد.
  • برنامه نویسان می‌توانند مدیریت خطا را نیز در حین تخصیص حافظه انجام دهند.
  • برنامه نویسان می‌توانند از توابع مدیریت حافظه مانند ()malloc() ،realloc و ()free داخل توابع سربارگذاری شده‌ی new و delete استفاده کنند.

عملگر [] برای دسترسی به عناصر یک آرایه مورد استفاده قرار می‌گیرد. تابع تعریف شده برای سربارگذاری [] یا () باید از اعضای یک کلاس و از نوع non-static باشد. ساختار کلی سربارگذاری عملگر [] به صورت زیر می‌باشد:

int& operator [](int x)
{

}

تابع سربارگذاری شده باید یک عدد صحیح را به روش ارجاع برگرداند. مثال زیر سربارگذاری عملگر [] را نشان می‌دهد:

#include <iostream>
using namespace std;

class Number
{
    private:
        int x[5];

    public:
        void read(int n)
        {
            cout << "Enter " << n << " numbers: ";
            for (int i = 0; i < n ; i++)
            {
                cin >> x[i];
            }
        }

        int& operator [](int i)
        {
            return x[i];
        }
};

int main()
{
    Number n1;
    n1.read(5);
    cout << "Element is: " << n1[2];
    return 0;
}
Enter 5 numbers: 1 2 3 4 5
Element is: 3

زمانی که چند زیر مجموعه داریم می‌توانیم به جای سربارگذاری []، از سربارگذای عملگر () استفاده می‌کنیم. ساختار کلی سربارگذاری عملگر () به صورت زیر می‌باشد:

int& operator () (int i, int j,...)
{

}

مثال زیر سربارگذاری عملگر () را نشان می‌دهد:

#include <iostream>
using namespace std;

class Matrix
{
    private:
        int x[2][2];

    public:
        void read()
        {
            cout << "Enter 2x2 matrix elements: ";

            for (int i = 0; i<2; i++)
            {
                for (int j = 0; j<2; j++)
                    cin >> x[i][j];
            }
        }

        int& operator ()(int i, int j)
        {
            return x[i][j];
        }
};

int main()
{
    Matrix m;
    m.read();
    cout << "Element is: " << m(1, 1);
}
Enter 2x2 matrix elements: 1 2 3 4
Element is: 4

سربارگذاری عملگر دسترسی به اعضای یک کلاس اعضای یک کلاس را می‌توانیم با سربارگذاری عملگر -> کنترل کنیم. این عملگر یک عملگر unary است که فقط یک شیء به را به عنوان عملوند دارد. این تابع سربارگذاری شده باید یک تابع non-static باشد که ساختار آن را در زیر مشاهده می‌کنید:

ClassName * operator ->(void)
{

}

برنامه زیر سربارگذاری عملگر دسترسی به اعضای یک کلاس (->) را نشان می‌دهد:

#include <iostream>
using namespace std;

class Number
{
    public:
        int x;

        Number(int x)
        {
            this->x = x;
        }

        Number * operator ->()
        {
            return this;
        }
};

int main()
{
    Number n1(30);
    cout << "x = " << n1->x;
}
x = 30

لیست عملگرهایی که قابلیت سربارگذاری را دارند در زیر آمده است.

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

لیست عملگرهایی که قابلیت سربارگذاری را ندارند در زیر آمده است.

:: .* . ?: