Смекни!
smekni.com

Розробка власного класу STRING (стр. 12 из 16)

class complex {

double re, im;

public:

complex (double r, double i) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

};

Тут наведена проста реалізація поняття комплексного числа, коли воно представлено парою чисел із плаваючою крапкою подвійної точності, з якими можна оперувати тільки за допомогою операцій + і *. Інтерпретацію цих операцій задає програміст у визначеннях функцій з іменами operator+ і operator*. Так, якщо b і c мають тип complex, те b+c означає (по визначенню) operator+ (b,c). Тепер можна наблизитися до звичного запису комплексних виражень:

void f ()

{

complex a = complex (1,3.1);

complex b = complex (1.2,2);

complex c = b;

a = b+c;

b = b+c*a;

c = a*b+complex (1,2);

}


Зберігаються звичайні пріоритети операцій, тому другий вираз виконується як b=b+ (c*a), а не як b= (b+c) *a.

1.15.1 Операторні функції

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

+ - * /% ^ & | ~!

= < > += - = *= /=%= ^= &=

|= << >> >>= <<= ==! = <= >= &&

|| ++ - і - >*, - > [] () new delete

Останні п'ять операцій означають: непряме звертання, індексацію, виклик функції, розміщення у вільній пам'яті й звільнення. Не можна змінити пріоритети цих операцій, так само як і синтаксичні правила для виразів. Так, не можна визначити унарну операцію%, також як і бінарну операцію!. Не можна ввести нові лексеми для позначення операцій, але якщо набір операцій вас не влаштовує, можна скористатися звичним позначенням виклику функції. Тому використайте pow (), а не **. Ці обмеження можна ввжати драконівськими, але більш вільні правила легко приводять до неоднозначності. Припустимо, ми визначимо операцію ** як піднесення до степеня, що на перший погляд здається очевидним і простим завданням. Але якщо як варто подумати, то виникають питання: чи належні операції ** виконуватися ліворуч праворуч або праворуч ліворуч? Як інтерпретувати вираження a**p як a* (*p) або як (a) ** (p)?

Ім'ям операторної функції є службове слово operator, за яким іде сама операція, наприклад, operator<<. Операторна функція описується й викликається як звичайна функція. Використання символу операції є просто короткою формою запису виклику операторної функції:


void f (complex a, complex b)

{

complex c = a + b; // коротка форма

complex d = operator+ (a,b); // явний виклик

}

З урахуванням наведеного опису типу complex ініціалізатори в цьому прикладі є еквівалентними.

1.15.2 Бінарні й унарні операції

Бінарну операцію можна визначити як функція-член з одним параметром, або як глобальну функцію із двома параметрами. Виходить, для будь-якої бінарної операції @ вираження aa @ bb інтерпретується або як aa. operator (bb), або як operator@ (aa,bb). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Префіксна або постфіксна унарна операція може визначатися як функція-член без параметрів, або як глобальна функція з одним параметром. Для будь-якої префиксної унарної операції @ вираження @aa інтерпретується або як aa. operator@ (), або як operator@ (aa). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Для будь-якої постфіксної унарної операції @ вираз @aa інтерпретується або як aa. operator@ (int), або як operator@ (aa, int). Якщо визначені обидві функції, то вибір інтерпретації відбувається за правилами зіставлення параметрів. Операцію можна визначити тільки відповідно до синтаксичних правил, наявними для неї в граматиці С++. Зокрема, не можна визначити% як унарну операцію, а + як тернарну. Проілюструємо сказане прикладами:

class X {

// члени (неявно використається покажчик 'this'):

X* operator& (); // префіксна унарная операція &

// (узяття адреси)

X operator& (X); // бінарна операція &

X operator++ (int); // постфіксний інкремент

X operator& (X,X); // помилка: & не може бути тернарною

X operator/ (); // помилка: / не може бути унарною

};

// глобальні функції (звичайно друзі)

X operator- (X); // префіксний унарный мінус

X operator- (X,X); // бінарний мінус

X operator-і (X&, int); // постфіксний інкремент

X operator- (); // помилка: немає операнда

X operator- (X,X,X); // помилка: тернарна операція

X operator% (X); // помилка: унарна операція%

1.15.3 Операторні функції й типи користувача

Операторна функція повинна бути або членом, або мати принаймні один параметр, що є об'єктом класу (для функцій, що перевизначають операції new і delete, це не обов'язково). Це правило гарантує, що користувач не зуміє змінити інтерпретацію виразів, що не містять об'єктів типу користувача. Зокрема, не можна визначити операторну функцію, що працює тільки з вказівниками. Цим гарантується, що в ++ можливі розширення, але не мутації (не вважаючи операцій =, &, і, для об'єктів класу).

Операторна функція, що має першим параметр основного типу, не може бути функцією-членом. Так, якщо ми додаємо комплексну змінну aa до цілого 2, то при підходящому описі функції-члена aa+2 можна інтерпретувати як aa. operator+ (2), але 2+aa так інтерпретувати не можна, оскільки не існує класу int, для якого + визначається як 2. operator+ (aa). Навіть якби це було можливо, для інтерпретації aa+2 і 2+aa довелося мати справа із двома різними функціями-членами. Цей приклад тривіально записується за допомогою функцій, що не є членами.

1.15.4 Конструктори

Замість того, щоб описувати кілька функцій для кожного випадку виклику (наприклад, комбінації типу double та complex з усіма операціями), можна описати конструктор, що з параметра double створює complex:

class complex {

// ...

complex (double r) { re=r; im=0; }

};

Цим визначається як одержати complex, якщо задано double. Конструктор з єдиним параметром не обов'язково викликати явно:

complex z1 = complex (23);

complex z2 = 23;

Обидві змінні z1 і z2 будуть ініціалізовані викликом complex (23).

Конструктор є алгоритмом створення значення заданого типу. Якщо потрібне значення деякого типу й існує його конструктор, параметром якого є це значення, то тоді цей конструктор і буде використатися. Так, клас complex можна було описати в такий спосіб:

class complex {

double re, im;

public:

complex (double r, double i =0) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

complex operator+= (complex);

complex operator*= (complex);

// ...

};

Всі операції над комплексними змінними й цілими константами з урахуванням цього опису стають законними. Ціла константа буде інтерпретуватися як комплексне число із мнимою частиною, рівної нулю. Так, a=b*2 означає

a = operator* (b, complex (double (2), double (0)))

Нові версії операцій таких, як +, має сенс визначати тільки для підвищення ефективності за рахунок відмови від перетворень типу коштує того. Наприклад, якщо з'ясується, що операція множення комплексної змінної на речовинну константу є критичної, то до безлічі операцій можна додати operator*= (double):

class complex {

double re, im;

public:

complex (double r, double i =0) { re=r; im=i; }

friend complex operator+ (complex, complex);

friend complex operator* (complex, complex);

complex& operator+= (complex);

complex& operator*= (complex);

complex& operator*= (double);

// ...

};

Операції присвоювання типу *= і += можуть бути дуже корисними для роботи з типами користувача, оскільки звичайно запис із ними коротше, ніж з їх звичайними "двійниками" * і +, а крім того вони можуть підвищити швидкість виконання програми за рахунок виключення тимчасових змінних:

inline complex& complex:: operator+= (complex a)

{

re += a. re;

im += a. im;

return *this;

}

При використанні цієї функції не потрібно тимчасовий змінної для зберігання результату, і вона досить проста, щоб транслятор міг "ідеально" зробити підстановку тіла. Такі прості операції як додавання комплексних теж легко задати безпосередньо:

inline complex operator+ (complex a, complex b)

{

return complex (a. re+b. re, a. im+b. im);

}

Тут в операторі return використається конструктор, що дає транслятору коштовну підказку на предмет оптимізації. Але для більше складних типів і операцій, наприклад таких, як множення матриць, результат не можна задати як одне вираження, тоді операції * і + простіше реалізувати за допомогою *= і +=, і вони будуть легше піддаватися оптимізації:

matrix& matrix:: operator*= (const matrix& a)

{

// ...

return *this;

}

matrix operator* (const matrix& a, const matrix& b)

{

matrix prod = a;

prod *= b;

return prod;

}

Відзначимо, що в певної подібним чином операції не потрібних ніяких особливих прав доступу до класу, до якого вона застосовується, тобто ця операція не повинна бути другом або членом цього класу.

Користувальницьке перетворення типу застосовується тільки в тому випадку, якщо воно єдине.

Побудований у результаті явного або неявного виклику конструктора, об'єкт є автоматичним, і знищується за першою нагодою, як правило відразу після виконання оператора, у якому він був створений.

1.15.5 Присвоювання й ініціалізація

Розглянемо простий строковий клас string:

struct string {

char* p;

int size; // розмір вектора, на який указує p