Смекни!
smekni.com

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

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

};

Рядок - це структура даних, що містить вказівник на вектор символів і розмір цього вектора. Вектор створюється конструктором і знищується деструктором. Але тут можуть виникнути проблеми:

void f ()

{

string s1 (10);

string s2 (20)

s1 = s2;

}

Тут будуть розміщені два символьних вектори, але в результаті присвоювання s1 = s2 вказівник на один з них буде знищений, і заміниться копією другого. Після виходу з f () буде викликаний для s1 і s2 деструктор, що двічі видалить той самий вектор, результати чого по всій видимості будуть жалюгідні. Для рішення цієї проблеми потрібно визначити відповідне присвоювання об'єктів типу string:

struct string {

char* p;

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

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

string& operator= (const string&);

};

string& string:: operator= (const string& a)

{

if (this! =&a) { // небезпечно, коли s=s

delete p;

p = new char [size=a. size];

strcpy (p,a. p);

}

return *this;

}

При такім визначенні string попередній приклад пройде як задумано. Але після невеликої зміни в f () проблема виникає знову, але в іншому виді:

void f ()

{

string s1 (10);

string s2 = s1; // ініціалізація, а не присвоювання

}

Тепер тільки один об'єкт типу string будується конструктором string:: string (int), а знищуватися буде два рядки. Справа в тому, що користувальницька операція присвоювання не застосовується до неініціалізованого об'єкта. Досить глянути на функцію string:: operator (), щоб зрозуміти причину цього: вказівник p буде тоді мати невизначене, по суті випадкове значення. Як правило, в операції присвоювання передбачається, що її параметри проініціалізовані. Отже, щоб упоратися з ініціалізацією потрібна схожа, але своя функція:

struct string {

char* p;

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

string (int size) { p = new char [size=sz]; }

~string () { delete p; }

string& operator= (const string&);

string (const string&);

};

string:: string (const string& a)

{

p=new char [size=sz];

strcpy (p,a. p);

}

Ініціалізація об'єкта типу X відбувається за допомогою конструктора X (const X&). Особливо це важливо в тих випадках, коли визначений деструктор. Якщо в класі X є нетривіальний деструктор, наприклад, що робить звільнення об'єкта у вільній пам'яті, найімовірніше, у цьому класі буде потрібно повний набір функцій, щоб уникнути копіювання об'єктів по членах:

class X {

// ...

X (something); // конструктор, що створює об'єкт

X (const X&); // конструктор копіювання

operator= (const X&); // присвоювання:

// видалення й копіювання

~X (); // деструктор

};

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

string g (string arg)

{

return arg;

}

main ()

{

string s = "asdf";

s = g (s);

}

Очевидно, після виклику g () значення s повинне бути "asdf". Не важко записати в параметр s копію значення s, для цього треба викликати конструктор копіювання для string. Для одержання ще однієї копії значення s по виходу з g () потрібний ще один виклик конструктора string (const string&). Цього разу ініціалізується тимчасова змінна, котра потім привласнюється s. Для оптимізації одну, але не обидві, з подібних операцій копіювання можна забрати. Природно, тимчасові змінні, використовувані для таких цілей, знищуються належним чином деструктором string:: ~string ().

Якщо в класі X операція присвоювання X:: operator= (const X&) і конструктор копіювання X:: X (const X&) явно не задані програмістом, що бракують операції будуть створені транслятором. Ці створені функції будуть копіювати по членах для всіх членів класу X. Якщо члени приймають прості значення, як у випадку комплексних чисел, це, те, що потрібно, і створені функції перетворяться в просте й оптимальне поразрядное копіювання. Якщо для самих членів визначені користувальницькі операції копіювання, вони й будуть викликатися відповідним чином:

class Record {

string name, address, profession;

// ...

};

void f (Record& r1)

{

Record r2 = r1;

}

Тут для копіювання кожного члена типу string з об'єкта r1 буде викликатися string:: operator= (const string&).

У нашому першому й неповноцінному варіанті строковий клас має член-вказівник і деструктор. Тому стандартне копіювання по членах для нього майже напевно невірно. Транслятор може попереджати про такі ситуації.

1.15.6 Інкремент і декремент

Нехай є програма з розповсюдженою помилкою:

void f1 (T a) // традиційне використання

{

T v [200];

T* p = &v [10];

p--;

*p = a; // Приїхали: `p' настроєні поза масивом,

// і це не виявлено

++p;

*p = a; // нормально

}

Природне бажання замінити вказівник p на об'єкт класу CheckedPtrTo, по якому непряме звертання можливо тільки за умови, що він дійсно вказує на об'єкт. Застосовувати інкремента і декремента до такого вказівника буде можна тільки в тому випадку, що вказівник настроєний на об'єкт у границях масиву й у результаті цих операцій вийде об'єкт у границях того ж масиву:

class CheckedPtrTo {

// ...

};

void f2 (T a) // варіант із контролем

{

T v [200];

CheckedPtrTo p (&v [0],v, 200);

p--;

*p = a; // динамічна помилка:

// 'p' вийшов за межі масиву

++p;

*p = a; // нормально

}

Інкремент і декремент є єдиними операціями в С++, які можна використати як постфіксні так і префіксні операції. Отже, у визначенні класу CheckedPtrTo ми повинні передбачити окремі функції для префіксних і постфіксних операцій інкремента й декремента:

class CheckedPtrTo {

T* p;

T* array;

int size;

public:

// початкове значення 'p'

// зв'язуємо з масивом 'a' розміру 's'

CheckedPtrTo (T* p, T* a, int s);

// початкове значення 'p'

// зв'язуємо з одиночним об'єктом

CheckedPtrTo (T* p);

T* operator++ (); // префіксна

T* operator++ (int); // постфіксна

T* operator-- (); // префіксна

T* operator-- (int); // постфісна

T& operator* (); // префіксна

};

Параметр типу int служить вказівкою, що функція буде викликатися для постфісної операції. Насправді цей параметр є штучним і ніколи не використається, а служить тільки для розходження постфіксної і префіксної операції. Щоб запам'ятати, яка версія функції operator++ використається як префіксна операція, досить пам'ятати, що префіксна є версія без штучного параметра, що вірно й для всіх інших унарних арифметичних і логічних операцій. Штучний параметр використається тільки для "особливих" постфіксних операцій ++ і - -. За допомогою класу CheckedPtrTo приклад можна записати так:

void f3 (T a) // варіант із контролем

{

T v [200];

CheckedPtrTo p (&v [0],v, 200);

p. operator-і (1);

p. operator* () = a; // динамічна помилка:

// 'p' вийшов за межі масиву

p. operator++ ();

p. operator* () = a; // нормально

}

1.15.7 Перевантаження операцій помістити в потік і взяти з потоку

C++ здатний вводити й виводити стандартні типи даних, використовуючи операцію помістити в потік " і операцію взяти з потоку ". Ці операції вже перевантажені в бібліотеках класів, якими постачені компілятори C++, щоб обробляти кожний стандартний тип даних, включаючи рядки й адреси пам'яті. Операції помістити в потік і взяти з потоку можна також перевантажити для того, щоб виконувати введення й вивід типів користувача. Програма на малюнку 8 демонструє перевантаження операцій помістити в потік і взяти з потоку для обробки даних певного користувачем класу телефонних номерів PhoneNumber. У цій програмі передбачається, що телефонні номери вводяться правильно. Перевірку помилок ми залишаємо для вправ.

На мал.8 функція-операція взяти з потоку (operator") одержує як аргументи посилання input типу istream, і посилання, названу num, на заданий користувачем тип PhoneNumber; функція повертає посилання типу istream. Функція-операція (operator") використається для введення номерів телефонів у вигляді

(056) 555-1212

в об'єкти класу PhoneNumber. Коли компілятор бачить вираження

cin >> phone

в main, він генерує виклик функції

operator>> (cin, phone);

Після виконання цього виклику параметр input стає псевдонімом для cin, а параметр num стає псевдонімом для phone. Функція-операція використовує функцію-елемент getline класу istream, щоб прочитати з рядка три частини телефонного номера викликаного об'єкта класу PhoneNumber (num у функції-операції й phone в main) в areaCode (код місцевості), exchange (комутатор) і line (лінія). Символи круглих дужок, пробілу й дефіса пропускаються при виклику функції-елемента ignore класу istream, що відкидає зазначену кількість символів у вхідному потоці (один символ за замовчуванням). Функція operator" повертає посилання input типу istream (тобто cin). Це дозволяє операціям введення об'єктів PhoneNumber бути зчепленими з операціями уведення інших об'єктів PhoneNumber або об'єктів інших типів даних. Наприклад, два об'єкти PhoneNumber могли б бути уведені в такий спосіб:

cin >> phonel >> phone2;

Спочатку було б виконане вираження cin " phonel шляхом виклику

operator>> (cin, phonel);

// Перевантаження операцій помістити в потік і взяти з потоку.

#include <iostream. h>

class PhoneNumber{

friend ostream soperator << (ostream &, const PhoneNumber &); friend istream ^operator >> (istream &, PhoneNumber &);

private:

char areaCode [4]; // трицифровий код місцевості й нульовий символ

char exchange [4]; // трицифровий комутатор і нульовий символ

char line [5]; // чотирицифрова лінія й нульовий символ

};

// Перевантажена операція помістити в потік

// (вона не може бути функцією-елементом).