48870 (Розробка власного класу STRING), страница 2

2016-07-30СтудИзба

Описание файла

Документ из архива "Розробка власного класу STRING", который расположен в категории "". Всё это находится в предмете "информатика" из 1 семестр, которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "курсовые/домашние работы", в предмете "информатика, программирование" в общих файлах.

Онлайн просмотр документа "48870"

Текст 2 страницы из документа "48870"

int second; // 0-59 };

void printMilitary (const Time &); // прототип void printStandard (const Time &); // прототип

main ()

{

Time dinnerTime; // змінна нового типу Time

// завдання елементам правильні значення dinnerTime. hour = 18; dinnerTime. minute = 30; dinnerTime. second = 0;

cout " "Обід відбудеться в ";

printMilitary (dinnerTime);

cout " " за військовим часом," " endl

<< "що відповідає "; printStandard{dinnerTime); cout << " за стандартним часом." << endl;

// завдання елементам неправильних значень

dinnerTime. hour = 29;

dinnerTime. minute = 73; dinnerTime. second = 103;

cout " endl << "Час із неправильними значеннями: "; printMilitary (dinnerTime); cout << endl; return 0;

// Друк часу у військовому форматі void printMilitary (const Time &t)

{

cout " (t. hour < 10?"0": "")" t. hour

"": "" (t. minute < 10?"0": "")" t. minute

"": "" (t. second < 10?"0": "")" t. second;

}

// друк часу в стандартному форматі

void printStandard (const Time &t)

{

cout " ( (t. hour == 0 || t. hour == 12)? 12: t. hour%12)" ": "" (t. minute < 10?"0": "")" t. minute " ": "" (t. second < 10?"0": "")" t. second " (t. hour < 12?" AM": " PM");

}

Обід відбудеться в 18: 30: 00 за військовим часом,

що відповідає 6: 30: 00 РМ за стандартним часом.

Час із неправильними значеннями: 29: 73: 103

Мал.1. Створення структури, завдання й друк її елементів

Існують і інші проблеми, пов'язані зі структурами в стилі С. У С структури не можуть бути надруковані як єдине ціле, тільки по одному елементу з відповідним форматом кожного. Для друку елементів структури в якому-небудь потрібному форматі повинна бути написана функція. "Перевантаження операцій" покаже, як перевантажити операцію ", щоб надати можливість простого друку об'єктів типу структура (C++ розширює поняття структури) або типу клас. У С структури не можна порівнювати в цілком, їх потрібно порівнювати елемент за елементом. Далі покажемо, як перевантажити операції перевірки рівності й відношення, щоб можна було в С++ порівнювати об'єкти типів структура й клас.

У наступному розділі ми знову використаємо нашу структуру Time, але вже як клас, і продемонструємо деякі переваги створення таких, так званих абстрактних типів даних, як класи. Ми побачимо, що класи й структури в C++ можна використовувати майже однаково. Різниця між ними складається в доступності за замовчуванням елементів кожного із цих типів. Це буде більш детально пояснено пізніше.

1.5 Використання абстрактного типу даних Time за допомогою класу

Класи надають програмістові можливість моделювати об'єкти, які мають атрибути (представлені як дані-елементи) і варіанти поведінки або операції (представлені як функції-елементи). Типи, що містять дані-елементи й функції-елементи, звичайно визначаються в C++ за допомогою ключового слова class.

Функції-елементи іноді в інших об’єктно-орієнтовних мовах називають методами, вони викликаються у відповідь на повідомлення, що посилаються об'єкту. Повідомлення відповідає виклику функції-елемента.

Коли клас визначений, ім'я класу може бути використане для об’явлення об'єкта цього класу. Мал.1 містить просте визначення класу Time.

Визначення нашого класу Time починається із ключового слова class. Тіло визначення класу береться у фігурні дужки ({ }). Визначення класу закінчується крапкою з комою. Визначення нашого класу Time, як і нашої структури Time, містить три цілих елементи hour, minute і second.

сlass Time {

public:

Time ();

void setTime (int, int, int);

void printMilitary ();

void printStandatd (); private:

int hour; // 0-23

int minute; // 0 - 59

int second; // 0-59

};

Мал.1 Просте визначення класу

Інші частини визначення класу - нові. Мітки public: (відкрита) і private: закрита) називаються специфікаторами доступу до елементів. Будь-які дані-елементи й функції-елементи, об’явлені після специфікатора доступу до елементів public: (і до наступного специфікатора доступу до елементів), доступні при будь-якому звертанні програми до об'єкта класу Time. Будь-які дані-елементи й функції-елементи, об’явлені після специфікатора доступу до елементів private: (і до наступного специфікатора доступу до елементів), доступні тільки функціям-елементам цього класу. Специфікатори доступу до елементів завжди закінчуються двокрапкою (:) і можуть з'являтися у визначенні класу багато разів і в будь-якому порядку. Надалі в тексті нашої роботи ми будемо використовувати записи специфікаторів доступу до елементів у вигляді public і private (без двокрапки).

Гарний стиль програмування: Використовуйте при визначенні класу кожний специфікатор доступу до елементів тільки один раз, що зробить програму більш ясною й простій для читання. Розміщайте першими елементи public, що є загальнодоступними.

Визначення класу в нашій програмі містить після специфікатора доступу до елементів public прототипи наступних чотирьох функцій-елементів: Time, setTime, printMilitary і printStandard. Це - відкриті функції-елементи або відкритий інтерфейс послуг класу. Ці функції будуть використовуватися клієнтами класу (тобто частинами програми, що грають роль користувачів) для маніпуляцій з даними цього класу.

Зверніть увагу на функцію-елемент із тим же ім'ям, що й клас. Вона називається конструктором цього класу. Конструктор - це спеціальна функція-елемент, що ініціалізує дані-елементи об'єкта цього класу. Конструктор класу викликається автоматично при створенні об'єкта цього класу. Ми побачимо, що звичайно клас має декілька конструкторів; це досягається за допомогою перевантаження функції.

Після специфікатора доступу до елементів private слідують три цілих елементи. Це говорить про те, що ці дані-елементи класу є доступними тільки функціям-елементам класу й, як ми побачимо далі, "друзям" класу. Таким чином, дані-елементи можуть бути доступні тільки чотирьом функціям, прототипи яких включені у визначення цього класу (або друзів цього класу). Звичайно дані-елементи перераховуються в частині private, а функції-елементи - у частині public. Як ми побачимо далі, можна мати функції-елементи private і дані public; останнє не типовим й вважається в програмуванні поганим тоном.

Коли клас визначений, його можна використати як тип в оголошеннях, наприклад, у такий спосіб:

Time sunset, // об'єкт типу Time

arrayOfTimes [5], // масив об'єктів типу Time

*pointerToTime, // вказівник на об’єкт типу Time

&dinnerTime = sunset; // посилання на об'єкт типу Time

Ім'я класу стає новим специфікатором типу. Може існувати безліч об'єктів класу як і безліч змінних типу, наприклад, такого, як int. Програміст по мірі необхідності може створювати нові типи класів. Це одна з багатьох причин, з яких C++ є розширюваною мовою.

Програма на мал.2 використовує клас Time. Ця програма створює єдиний об'єкт класу Time, названий t. Коли об'єкт створюється, автоматично викликається конструктор Time, що явно привласнює нульові початкові значення всім даним-елементам закритої частини private. Потім друкується час у військовому й стандартному форматах, щоб підтвердити, що елементи одержали правильні початкові значення. Після цього за допомогою функцій-елементів setTime встановлюється час і воно знову друкується в обох форматах. Потім функція-елемент setTime намагається дати даним-елементам неправильні значення й час знову друкується в обох форматах.

Знову відзначимо, що дані-елементи hour, minute і second об’явлені специфікатором доступу до елементів private. Ці закриті дані-елементи класу звичайно недоступні поза класом. Глибокий зміст такого підходу полягає в тому, що реальне становище даних усередині класу не стосується клієнтів класу. Наприклад, було б цілком можливо змінити внутрішню структуру даних і представляти, наприклад, час усередині класу як число секунд після опівночі. Клієнти могли б використати ті ж самі відкриті функції-елементи й одержувати ті ж самі результати, навіть не усвідомлюючи про зроблені зміни. У цьому сенсі, говорять, що реалізація класу схована від клієнтів. Таке приховання інформації сприяє модифікаційності програм і спрощує сприйняття класу клієнтами.

// FIG 3. CPP // Клас Time.

#include

// Визначення абстрактного типу даних (АТД) Time

class Time{

public:

Time{); // конструктор

void setTime (int, int, int); // установка годин, хвилин

// та секунд

void printMilitary (); // часу у військовому форматі

void printStandard (); // друк часу

// у стандартному форматі

private:

int hour; // 0-23

int minute; // 0-59

int second; // 0-59

// Конструктор Time привласнює нульові початкові значення // кожному елементу даних. Забезпечує погоджене

// початковий стан всіх об'єктів

Time Time:: Time () { hour = minute = second =0; }

// Завдання нового значення Time у вигляді воєнного часу. // Перевірка правильності значень даних.

// Обнуління неправельних значень,

void Time:: setTime (int h, int m, int s) {

hour = (h>=0&&h<24)? h: 0;

minute = (m >= 0 && m < 60)? m: 0;

second ~ (s > - 0 && s < 60)? s: Q-; }

// Друк часу у військовому форматі

void Time:: printMilitary ()

{

cout " {hour < 10?"0": "")" hour" ": "

" (minute < 10?"0": "")" minute " ": "

" (second < 10?"0": "")" second; }

// Друк часу в стандартному форматі void Time:: printStandard ()

{

cout " ( (hour == 0 || hour == 12)? 12: hour% 12)

"": " " (minute < 10?"0": "")" minute

"": " " (second < 10?"0": "")" second

" (hour < 12?" AM": " PM");

}

// Формування перевірки простого класу Time

main ()

{

Time t; // визначення екземпляра об'єкта t класу Time

cout " "Початкове значення воєнного часу дорівнює "; t. printMilitary (); cout << endl

<< "Початкове значення стандартного часу дорівнює "; t. printStandard ();

t. setTime (13, 27,6):

cout " endl " endl << "Воєнний час після setTime дорівнює "; t. printMilitary ();

cout << endl << "Стандартний час після setTime дорівнює"; t. printStandard ();

t. setTime (99, 99, 99); // спроба встановити неправильні значення cout << endl << endl

<< "Після спроби неправильної установки: "

<< endl " "Воєнний час: "; t. printMilitary ();

cout << endl " "Стандартний час: "; t. printStandard (); cout << endl; return 0; }

Мал.2. Використання абстрактного типу даних Time як класу

Початкове значення воєнного часу дорівнює 00: 00: 00 Початкове значення стандартного часу дорівнює 12: 00: 00 AM

Воєнний час після setTime дорівнює 13: 27: 06

Після спроби неправильної установки: Воєнний час: 00; 00: 00 Стандартний час: 12: 00: 00 AM

У нашій програмі конструктор Time просто встановлює початкові значення, рівні 0, даним-елементам, (тобто задає воєнний час, еквівалентний 12AM). Це гарантує, що об'єкт при його створенні перебуває у відомому стані. Неправильні значення не можуть зберігатися в даних-елементах об'єкта типу Time, оскільки конструктор автоматично викликається при створенні об'єкта типу Time, а всі наступні спроби змінити дані-елементи ретельно розглядаються функцією setTime.

Відзначимо, що дані-елементи класу не можуть одержувати початкові значення в тілі класу, де вони оголошуються. Ці дані-елементи повинні одержувати початкові значення за допомогою конструктора класу або їм можна присвоїти значення через функції.

Функція з тим же ім'ям, що й клас, але з символом-тильда (~) перед нею, називається деструктором цього класу (наш приклад не включає деструктор). Деструктор робить "завершальні службові дії над кожним об'єктом класу перед тим, як пам'ять, відведена під цей об'єкт, буде повторно використана системою.

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

Об’ява класу містить об’яви даних-елементів і функцій-елементів класу. Об’ява функцій-елементів є прототипами функцій. Функції-елементи можуть бути описані всередині класу, але гарний стиль програмування полягає в описі функцій поза визначенням класу.

Відзначимо використання бінарної операції дозволу області дії (::) у кожному визначенні функції-елемента, що випливає за визначенням класу на мал.3. Після того, як клас визначений і його функції-елементи Об’явлені, ці функції-елементи повинні бути описані. Кожна функція-елемент може бути описана прямо в тілі класу (замість включення прототипу функції класу) або після тіла класу. Коли функція-елемент описується після відповідного визначення класу, ім'я функції випереджається ім'ям класу та бінарною операцією дозволу області дії (::). Оскільки різні класи можуть мати елементи з однаковими іменами, операція дозволу області дії "прив'язує" ім'я елемента до імені класу, щоб однозначно ідентифікувати функції-елементи даного класу.

Незважаючи на те, що функція-елемент, об’явлена у визначенні класу, може бути описана поза цим визначенням, ця функція-елемент однаково має областю дії клас, тобто її ім'я відомо тільки іншим елементам класу поки до неї звертаються за допомогою об'єкта класу, посилання на об'єкт класу або покажчика на об'єкт класу. Про області дії класу ми більш докладно ще поговоримо пізніше.

Якщо функція-елемент описана у визначенні класу, вона автоматично вбудовується inline. Функція-елемент, описана поза визначенням класу, може бути inline за допомогою явного використання ключового слова inline. Нагадаємо, що компілятор резервує за собою право не вбудовувати ніяких функцій.

Цікаво, що функції-елементи printMilitary і printStandard не одержують ніяких аргументів. Це відбувається тому, що функції-елементи неявно знають, що вони друкують дані-елементи певного об'єкта типу Time, для якого вони активізовані. Це робить виклики функцій-елементів більш короткими, ніж відповідні виклики функцій у процедурному програмуванні. Це зменшує також ймовірність передачі неправильних аргументів, неправильних типів аргументів або неправильної кількості аргументів.

Класи спрощують програмування, тому що клієнт (або користувач об'єкта класу) має справу тільки з операціями, інкапсульованими або вбудованими в об'єкт. Такі операції звичайно проектуються орієнтовними саме на клієнта, а не на зручну реалізацію. Інтерфейси міняються, але не так часто, як реалізації. При зміні реалізації відповідно повинні змінюватися орієнтовані на реалізацію коди. А шляхом приховання реалізації ми виключаємо можливість для інших частин програми виявитися залежними від особливостей реалізації класу.

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

1.6 Область дії клас і доступ до елементів класу

Дані-елементи класу (змінні, об’явлені у визначенні класу) і функції-елементи (функції, об’явлені у визначенні класу) мають областю дії клас. Функції, що не є елементами класу, мають областю дії файл.

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

Функції-елементи класу можна перевантажувати, але тільки за допомогою інших функцій-елементів класу. Для перевантаження функції-елемента просто забезпечте у визначенні класу прототип для кожної версії перевантаженої функції й позначить кожну версію функції окремим описом.

Але, не можна перевантажити функцію-елемент класу за допомогою функції не з області дії цього класу.

Функції-елементи мають всередині класу область дії функцію: змінні, об’явлені у функції-елементі, відомі тільки цій функції. Якщо функція-елемент об’являє змінну з тим же ім'ям, що й змінна в області дії клас, остання робиться невидимої в області дії функції. Така схована змінна може бути доступна за допомогою операції дозволу області. Невидимі глобальні змінні можуть бути доступні за допомогою унарної операції дозволу області дії.

Операції, для доступу до елементів класу, аналогічні операціям, для доступу до елементів структури. Операція вибору елемента крапка () комбінується для доступу до елементів об'єкта з ім'ям об'єкта або з посиланням на об'єкт. Операція вибору елемента стрілка (->) комбінується для доступу до елементів об'єкта з вказівником на об'єкт.

Програма на мал.3 використає простий клас, названий Count, з відкритим елементом даних х типу int і відкритої функцією-елементом print, щоб проілюструвати доступ до елементів класу за допомогою операції вибору елемента. Програма створює три екземпляри змінних типу Count - counter, counterRef (посилання на об'єкт типу Count) і counterPtr (покажчик на об'єкт типу Count). Змінна counterRef об’явлена, щоб посилатися на counter, змінна counterPtr об’явлена, щоб указувати на counter. Важливо відзначити, що тут елемент даних х зроблений відкритим просто для того, щоб продемонструвати способи доступу до відкритих елементів. Як ми вже встановили, дані звичайно робляться закритими (private).

// FIG6_4. CPP

// Демонстрація операцій доступу до елементів класу. і - >

#include

// Простий клас Count class Count { public:

int x;

void print () { cout << x " endl; } };

main ()

{

Count counter, // створюється об'єкт counter

*counterPtr = &counter, // покажчик на counter &counterRef = counter; // посиланя на counter

cout " "Присвоювання х значення 7 і друк по імені об'єкта: ";

counter. х =7; // присвоювання 7 елементу даних х

counter. print (); // виклик функції-елемента для друку

cout << "Присвоювання х значення 8 і друк по посиланню: ";

counterRef. x = 8; // присвоювання 8 елементу даних х

counterRef. print (); // виклик функції-елемента для друку

cout print (); // виклик функції-елемента для друку

return 0;

}

Мал.3. Доступ до даних-елементів об'єкта й функціям-елементам за допомогою імені об'єкта, посилання й вказівника на об'єкт

Присвоювання х значення 7 і друк по імені об'єкта: 7

Присвоювання х значення 8 і друк по посиланню: 8

Присвоювання х значення 10 і друк по покажчику: 10


1.7 Конструктор класу

Серед інших функцій-членів конструктор виділяється тим, що його ім'я збігається з ім'ям класу. Для оголошення конструктора за замовчуванням ми пишемо:

class Account {

public:

// конструктор за замовчуванням...

Account ();

// ...

private:

char *_name;

unsigned int _acct_nmbr;

double _balance;

};

Єдине синтаксичне обмеження, що накладає на конструктор, полягає в тому, що він не повинен мати тип значення, що повертає, навіть void.

Кількість конструкторів в одного класу може бути будь-яким, аби тільки всі вони мали різні списки формальних параметрів.

Звідки ми знаємо, скільки і які конструктори визначити? Як мінімум, необхідно присвоїти початкове значення кожному члену, що це потребує. Наприклад, номер рахунку або задається явно, або генерується автоматично таким чином, щоб гарантувати його унікальність. Припустимо, що він буде створюватися автоматично. Тоді ми повинні дозволити ініціализувати два члени, що залишилися _name і _balance:

Account (const char *name, double open_balance);

Об'єкт класу Account, ініціалізуємий конструктором, можна об’явити в такий спосіб:

Account newAcct ("Mikey Matz", 0);

Якщо ж є багато рахунків, для яких початковий баланс дорівнює 0, то корисно мати конструктор, що задає тільки ім'я власника й автоматично ініцілізує _balance нулем. Один зі способів зробити це - надати конструктор виду:

Account (const char *name);

Інший спосіб - включити в конструктор із двома параметрами значення за замовчуванням, рівне нулю:

Account (const char *name, double open_balance = 0.0);

Обоє конструктора володіють необхідної користувачеві функціональністю, тому обоє рішення прийнятні. Ми воліємо використати аргумент за замовчуванням, оскільки в такій ситуації загальне число конструкторів класу скорочується.

Потрібно чи підтримувати також завдання одного лише початкового балансу без вказівки імені клієнта? У цьому випадку специфікація класу явно забороняє це. Наш конструктор із двома параметрами, з яких другий має значення за замовчуванням, надає повний інтерфейс для задання початкових значень тих членів класу Account, які можуть бути ініціалізовані користувачем:

class Account {

public:

// конструктор за замовчуванням...

Account ();

// імена параметрів в оголошенні вказувати необов'язково

Account (const char*, double=0.0);

const char* name () { return name; }

// ...

private:

// ...

};

Нижче наведені два приклади правильного визначення об'єкта класу Account, де конструкторові передається один або два аргументи:

int main ()

{

// правильно: в обох випадках викликається конструктор

// с двома параметрами

Account acct ("Ethan Stern");

Account *pact = new Account ("Michael Lieberman", 5000);

if (strcmp (acct. name (), pact->name ()))

// ...

}

C++ вимагає, щоб конструктор застосовувався до певного об'єкта до його першого використання. Це означає, що як для acct, так і для об'єкта, на який указує pact, конструктор буде викликаний перед перевіркою в інструкції if.

Компілятор перебудовує нашу програму, вставляючи виклики конструкторів.

От як, цілком ймовірно, буде модифіковане визначення acct усередині main ():

// псевдокод на C++,

// іллюструючий внутрішню вставку конструктора

int main ()

{

Account acct;

acct. Account:: Account ("Ethan Stern", 0.0);

// ...

}

Звичайно, якщо конструктор визначений як вбудований, то він підставляється в точці виклику.

Обробка оператора new трохи складніше. Конструктор викликається тільки тоді, коли він успішно виділив пам'ять. Модифікація визначення pact у трохи спрощеному виді виглядає так:

// псевдокод на C++,

// іллюструючий внутрішню вставку конструктора при обробці new

int main ()

{

// ...

Account *pact;

try {

pact = _new (sizeof (Account));

pact->Acct. Account:: Account (

"Michael Liebarman", 5000.0);

}

catch (std:: bad_alloc) {

// оператор new закінчився невдачею:

// конструктор не викликається

}

// ...

}

Існує три в загальному випадку еквівалентні форми завдання аргументів конструктора:

// загалом ці конструктори еквівалентні

Account acct1 ("Anna Press");

Account acct2 = Account ("Anna Press");

Account acct3 = "Anna Press";

Форма acct3 може використовуватися тільки при завданні єдиного аргументу. Якщо аргументів два або більше, рекомендовано користуватися формою acct1, хоча припустимо й acct2.

// рекомендує форма, що, виклику конструктора

Account acct1 ("Anna Press");

Визначати об'єкт класу, не вказуючи списку фактичних аргументів, можна в тому випадку, якщо в ньому або об’явлений конструктор за замовчуванням, або взагалі немає об’яв конструкторів. Якщо в класі об’явлений хоча б один конструктор, то не дозволяється визначати об'єкт класу, не викликаючи жодного з них. Зокрема, якщо в класі визначений конструктор, що приймає один або більше параметрів, але не визначений конструктор за замовчуванням, то в кожному визначенні об'єкта такого класу повинні бути присутнім необхідні аргументи. Можна заперечити, що не має змісту визначати конструктор за замовчуванням для класу Account, оскільки не буває рахунків без імені власника. У переглянутій версії класу Account такий конструктор виключений:

class Account {

public:

// імена параметрів в оголошенні вказувати необов'язково

Account (const char*, double=0.0);

const char* name () { return name; }

// ...

private:

// ...

};

Тепер при оголошенні кожного об'єкта Account у конструкторі обов'язково треба вказати як мінімум аргумент типу C-рядка, але це швидше за все безглуздо. Чому? Контейнерні класи (наприклад, vector) вимагають, щоб для класу елементів, що поміщають у них, був або заданий конструктор за замовчуванням, або взагалі ніяких конструкторів. Аналогічна ситуація має місце при виділенні динамічного масиву об'єктів класу. Так, що інструкція викликала б помилку компіляції для нової версії Account:

// помилка: потрібен конструктор за замовчуванням для класу

Account *pact = new Account [new_client_cnt];

На практиці часто потрібно задавати конструктор за замовчуванням, якщо є які-небудь інші конструктори.

А якщо для класу немає розумних значень за замовчуванням? Наприклад, клас Account вимагає задавати для будь-якого об'єкта прізвище власника рахунку.

У такому випадку найкраще встановити стан об'єкта так, щоб було видно, що він ще не ініціалізований коректними значеннями:

// конструктор за замовчуванням для класу Account

inline Account:: Account () {

_name = 0;

_balance = 0.0;

_acct_nmbr = 0;

}

Однак у функції-члени класу Account прийдеться включити перевірку цілісності об'єкта перед його використанням.

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

// конструктор за замовчуванням класу Account з використанням

// списку ініціалізації членів

inline Account::

Account ()

: _name (0),

_balance (0.0), _acct_nmbr (0)

{}

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

inline Account::

Account (const char* name, double opening_bal)

: _balance (opening_bal)

{

_name = new char [strlen (name) +1];

strcpy (_name, name);

_acct_nmbr = get_unique_acct_nmbr ();

}

Конструктор не можна об’являти із ключовими словами const або volatile, тому наведені записи невірні:

class Account {

public:

Account () const; // помилка

Account () volatile; // помилка

// ...

};

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

Розглянемо наступний фрагмент програми:

// у якімсь заголовному файлі

extern void print (const Account &acct);

// ...

int main ()

{

// перетворить рядок "oops" в об'єкт класу Account

// за допомогою конструктора Account:: Account ("oops", 0.0)

print ("oops");

// ...

}

За замовчуванням конструктор з одним параметром (або з декількома - за умови, що всі параметри, крім першого, мають значення за замовчуванням) відіграє роль оператора перетворення. У цьому фрагменті програми конструктор Account неявно застосовується компілятором для трансформації літерального рядка в об'єкт класу Account при виклику print (), хоча в даній ситуації таке перетворення не потрібно.

Ненавмисні неявні перетворення класів, наприклад трансформація "oops" в об'єкт класу Account, виявилися джерелом помилок, що виявляють важко. Тому в стандарт C++ було додано ключове слово explicit, що говорить компіляторові, що такі перетворення не потрібні:

class Account {

public:

explicit Account (const char*, double=0.0);

};

Даний модифікатор застосуємо тільки до конструктора.

1.8 Конструктор копіювання

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

Часто почленна ініціалізація не забезпечує коректну дію класу. Тому ми явно визначаємо конструктор копіювання. У нашому класі Account це необхідно, інакше два об'єкти будуть мати однакові номери рахунків, що заборонено специфікацією класу.

Конструктор копіювання приймає як формальний параметр посилання на об'єкт класу (рекомендовано зі специфікатором const). Його реалізація:

inline Account::

Account (const Account &rhs)

: _balance (rhs. _balance)

{

_name = new char [strlen (rhs. _name) + 1];

strcpy (_name, rhs. _name);

// копіювати rhs. _acct_nmbr не можна

_acct_nmbr = get_unique_acct_nmbr ();

}

Коли ми пишемо:

Account acct2 (acct1);

компілятор визначає, чи оголошений явний конструктор копіювання для класу Account. Якщо він оголошений і доступний, то він і викликається; а якщо недоступний, то визначення acct2 вважається помилкою. У випадку, що коли конструктор копіювання не об’явлений, виконується почленна ініціалізація за замовчуванням. Якщо згодом об’явлення конструктор копіювання буде додане або вилучене, ніяких змін у програми користувачів вносити не прийдеться. Однак перекомпілювати їх все-таки необхідно.

1.9 Деструктор класу

Одна із цілей, що ставляться перед конструктором, - забезпечити автоматичне виділення ресурсу. Ми вже бачили в прикладі із класом Account конструктор, де за допомогою оператора new виділяється пам'ять для масиву символів і привласнюється унікальний номер рахунку. Можна також представити ситуацію, коли потрібно одержати монопольний доступ до поділюваної пам'яті або до критичної секції потоку. Для цього необхідна симетрична операція, що забезпечує автоматичне звільнення пам'яті або повернення ресурсу після завершення часу життя об'єкта, - деструктор. Деструктор - це спеціальна обумовлена користувачем функція-член, що автоматично викликається, коли об'єкт виходить із області видимості або коли до покажчика на об'єкт застосовується операція delete. Ім'я цієї функції створено з імені класу з попереднім символом “тильда" (~). Деструктор не повертає значення й не приймає ніяких параметрів, а отже, не може бути перевантажений.

Хоча дозволяється визначати кілька таких функцій-членів, лише одна з них буде застосовуватися до всіх об'єктів класу. От, наприклад, деструктор для нашого класу Account:

class Account {

public:

Account ();

explicit Account (const char*, double=0.0);

Account (const Account&);

~Account ();

// ...

private:

char *_name;

unsigned int _acct_nmbr;

double _balance;

};

inline

Account:: ~Account ()

{

delete [] _name;

return_acct_number (_acct_nnmbr);

}

Зверніть увагу, що в нашому деструкторі не скидаються значення членів:

inline Account:: ~Account ()

{

// необхідно

delete [] _name;

return_acct_number (_acct_nnmbr);

// необов'язково

_name = 0;

_balance = 0.0;

_acct_nmbr = 0;

}

Робити це необов'язково, оскільки відведена під члени об'єкта пам'ять однаково буде звільнена. Розглянемо наступний клас:

class Point3d {

public:

// ...

private:

float x, y, z;

};

Конструктор тут необхідний для ініціалізації членів, що представляють координати точки. Чи потрібний деструктор? Немає. Для об'єкта класу Point3d не потрібно звільняти ресурси: пам'ять виділяється й звільняється компілятором автоматично на початку й наприкінці його життя.

В загальному випадку, якщо члени класу мають прості значення, скажімо, координати точки, то деструктор не потрібний. Не для кожного класу необхідний деструктор, навіть якщо в нього є один або більше конструкторів. Основною метою деструктора є звільнення ресурсів, виділених або в конструкторі, або під час життя об'єкта, наприклад звільнення пам'яті, виділеної оператором new.

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

{

// початок критичної ділянки програми

#ifdef PROFILE

Timer t;

#endif

// критична ділянка

// t знищується автоматично

// відображається витрачений час...

}

Щоб переконатися в тім, що ми розуміємо поводження деструктора (та й конструктора теж), розберемо наступний приклад:

(1) #include "Account. h"

(2) Account global ("James Joyce");

(3) int main ()

(4) {

(5) Account local ("Anna Livia Plurabelle", 10000);

(6) Account &loc_ref = global;

(7) Account *pact = 0;

(8)

(9) {

(10) Account local_too ("Stephen Hero");

(11) pact = new Account ("Stephen Dedalus");

(12) }

(13)

(14) delete pact;

(15) }

Скільки тут викликається конструкторів? Чотири: один для глобального об'єкта global у рядку (2); по одному для кожного з локальних об'єктів local і local_too у рядках (5) і (10) відповідно, і один для об'єкта, розподіленого в купі, у рядку (11). Ні об’явлення посилання loc_ref на об'єкт у рядку (6), ні об’явлення вказівника pact у рядку (7) не приводять до виклику конструктора. Посилання - це псевдонім для вже сконструйованого об'єкта, у цьому випадку для global. Вказівника також лише адресує об'єкт, створений раніше (у цьому випадку розподілений у купі, рядок (11)), або не адресує ніякого об'єкта (рядок (7)).

Аналогічно викликаються чотири деструктори: для глобального об'єкта global, об’явленого в рядку (2), для двох локальних об'єктів і для об'єкта в купі при виклику delete у рядку (14). Однак у програмі немає інструкції, з якої можна зв'язати виклик деструктора. Компілятор просто вставляє ці виклики за останнім використанням об'єкта, але перед закриттям відповідної області видимості.

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

Деструктор не викликається, коли з області видимості виходить посилання або вказівник на об'єкт (сам об'єкт при цьому залишається).

С++ за допомогою внутрішніх механізмів перешкоджає застосуванню оператора delete до вказівника, що не адресує ніякого об'єкта, так що відповідні перевірки коду необов'язкові:

// необов'язково: неявно виконується компілятором

if (pact! = 0) delete pact;

Щораз, коли усередині функції цей оператор застосовується до окремого об'єкта, розміщеному в купі, краще використати об'єкт класу auto_ptr, а не звичайний вказівник. Це особливо важливо тому, що пропущений виклик delete (скажемо, у випадку, коли збуджується виключення) веде не тільки до витоку пам'яті, але й до пропуску виклику деструктора. Нижче приводиться приклад програми, переписаної з використанням auto_ptr (вона злегка модифікована, тому що об'єкт класу auto_ptr може бути явно із для адресації іншого об'єкта тільки присвоюванням його іншому auto_ptr):

#include

#include "Account. h"

Account global ("James Joyce");

int main ()

{

Account local ("Anna Livia Plurabelle", 10000);

Account &loc_ref = global;

auto_ptr pact (new Account ("Stephen Dedalus"));

{

Account local_too ("Stephen Hero");

}

// об'єкт auto_ptr знищується тут

}

1.10 Явний виклик деструктора

Іноді викликати деструктор для деякого об'єкта доводиться явно. Особливо часто така необхідність виникає у зв'язку з оператором new. Розглянемо приклад.

Коли ми пишемо:

char *arena = new char [sizeof Image];

то з купи виділяється пам'ять, розмір якої дорівнює розміру об'єкта типу Image, вона не ініціалізована й заповнена випадковими бітами.

Якщо ж написати:

Image *ptr = new (arena) Image ("Quasimodo");

то ніякої нової пам'яті не виділяється. Замість цього змінної ptr привласнюється адреса, асоційованою зі змінною arena. Тепер пам'ять, на яку вказує ptr, інтерпретується як займана об'єктом класу Image, і конструктор застосовується до вже існуючої області. Таким чином, оператор розміщення new () дозволяє сконструювати об'єкт у раніше виділеній області пам'яті.

Закінчивши працювати із зображенням Quasimodo, ми можемо зробити якісь операції із зображенням Esmerelda, розміщеним по тій же адресі arena у пам'яті:

Image *ptr = new (arena) Image ("Esmerelda");

Однак зображення Quasimodo при цьому буде затерто, а ми його модифікували й хотіли б записати на диск. Звичайне збереження виконується в деструкторі класу Image, але якщо ми застосуємо оператор delete:

// погано: не тільки викликає деструктор, але й звільняє пам'ять

delete ptr;

то, крім виклику деструктора, ще й повернемо в купу пам'ять, чого робити не варто було б. Замість цього можна явно викликати деструктор класу Image:

ptr->~Image ();

зберігши відведену під зображення пам'ять для наступного виклику оператора розміщення new.

Відзначимо, що, хоча ptr і arena адресують ту саму область пам'яті в купі, застосування оператора delete до arena

// деструктор не викликається

delete arena;

не приводить до виклику деструктора класу Image, тому що arena має тип char*, а компілятор викликає деструктор тільки тоді, коли операндом в delete є вказівник на об'єкт класу, що має деструктор.

1.11 Небезпека збільшення розміру програми

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

Account acct ("Tina Lee");

int swt;

// ...

switch (swt) {

case 0:

return;

case 1:

// щось зробити

return;

case 2:

// зробити щось інше

return;

// і так далі

}

компілятор підставить деструктор перед кожною інструкцією return. Деструктор класу Account невеликий, і витрати часу й пам'яті на його підстановку теж малі. У противному випадку прийдеться або об’явити деструктор невбудованим, або реорганізувати програму. У прикладі вище інструкцію return у кожній мітці case можна замінити інструкцією break для того, щоб у функції була єдина точка виходу:

// переписано для забезпечення єдиної точка виходу

switch (swt) {

case 0:

break;

case 1:

// щось зробити

break;

case 2:

// зробити щось інше

break;

// і так далі

}

// єдина точка виходу

return;

1.12 Константні об'єкти й функції-елементи

Ми ще раз особливо відзначаємо принцип найменших привілеїв як один з найбільш фундаментальних принципів створення гарного програмного забезпечення. Розглянемо один зі способів застосування цього принципу до об'єктів.

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

const Time noon (12, 0, 0);

об’являє як константний об'єкт noon класу Time і присвоює йому початкове значення 12 годин пополудні.

Компілятори C++ сприймають оголошення const настільки неухильно, що в підсумку не допускають ніяких викликів функцій-елементів константних об'єктів (деякі компілятори дають у цих випадках тільки попередження). Це жорстоко, оскільки клієнти об'єктів можливо захочуть використати різні функції-елементи читання "get", а вони, звичайно, не змінюють об'єкт. Щоб обійти це, програміст може оголосити константні функції-елементи; тільки вони можуть оперувати константними об'єктами. Звичайно, константні функції-елементи не можуть змінювати об'єкт - це не дозволить компілятор.

Константна функція вказується як const і в об’яві, і в описі за допомогою ключового слова const після списку параметрів функції, але перед лівою фігурною дужкою, що починає тіло функції. Наприклад, у наведеному нижче прикладі об’являється як константна функція-елемент деякого класу А

int A:: getValue () const {return privateDateMember};

яка просто повертає значення одного з даних-елементів об'єкта. Якщо константна функція-елемент описується поза об’явою класу, то як об’ява функції-елемента, так і її опис повинні включати const.

Тут виникає цікава проблема для конструкторів і деструкторів, які звичайно повинні змінювати об'єкт. Для конструкторів і деструкторів константних об'єктів оголошення const не потрібно. Конструктор повинен мати можливість змінювати об'єкт із метою присвоювання йому відповідних початкових значень. Деструктор повинен мати можливість виконувати підготовку завершення робіт перед знищенням об'єкта.

Програма на мал.4 створює константний об'єкт класу Time і намагається змінити об'єкт не константними функціями-елементами setHour, setMinute і setSecond. Як результат показані згенеровані компілятором Borland C++ попередження.

// TIME5. H

// Оголошення класу Time.

// Функції-елементи описані в TIMES. CPP

#ifndef TIME5_H idefine TIME5_H

class Time { public:

Time (int = 0, int = 0, int = 0); // конструктор за замовчуванням

// функції запису set

void setTime (int, int, int); // установкачасу

void setHour (int); // установкагодин

void setMinute (int); // установкахвилин

void setSecond (int); // установкасекунд

// функції читання get (звичайно об’являється const)

int getHour () const; // повертає значення годин

int getMinute () const; // повертає значення хвилин

int getSecondf) const; // повертає значення секунд

// функції друк (звичайно об’являється const)

void printMilitary () const; // друк військового часу void printStandard () const; // друк стандартного часу

private:

int hour; // 0-23

int minute; // 0-59

int second; // 0-59

};

#endif

// TIME5. CPP

// Опис функцій-елементів класу Time.

finclude

iinclude "time5. h"

// Функція конструктор для ініціалізації закритих даних. // За замовчуванням значення рівні 0 (дивися опис класу). Time:: Time (int hr, int min, int sec) { setTime (hr, min, sec); }

// Встановка значень години, хвилин і секунд, void Time:: setTime (int h, int m, int s) {

hour = (h >= 0 && h < 24)? h: 0;

minute = (m >= 0 && m < 60)? m: 0;

second = (s >= 0 && s < 60)? s: 0; }

// Установка значення годин

void Time:: setHour (int h) { hour = (h >= 0 && h < 24)? h: 0; }

// Установка значення хвилин void Time:: setMinute (int m)

{ minute = (m >= 0 && m < 60)? m: 0; }

// Установка значення секунд void Time:: setSecond (int s)

{ second = (s >= 0 && s < 60)? s: 0; }

// Читання значення годин

int Time:: getHour () const { return hour; }

// Читання значення хвилин

int Time:: getMinute () const { return minute; }

// Читання значення секунд

int Time:: getSecond () const { return second; }

// Відображення часу у військовому форматі: HH: MM: SS

void Time:: printMilitary () const

{

cout " (hour < 10?"0": "")" hour " ": "

" (minute < 10?"0": "")" minute" ": "

" (second < 10?"0": "")" second; }

// Відображення часу в стандартному форматі: HH: MM: SS AM // (або РМ)

void Time:: printStandard () const {

cout " ( (hour == 12)? 12: hour% 12)" ": "

" (minute < 10?"0": "")" minute " ": " " (second < 10?"0": "")" second " (hour< 12?"AM": "PM"); }

// FIG7_1. CPP

// Спроба одержати доступ до константного об'єкта

// з не-константними функціями-елементами.

#include

#include "time5. h"

main () {

const Time t (19, 33, 52); // константний об'єкт

t. setHour (12); // ПОМИЛКА: не-константна функція елемент t. setMinute (20); // ПОМИЛКА: не-константна функція елемент t. setSecond (39); // ПОМИЛКА: не-константна функція елемент

return 0; }

Compiling FIG7_1. CPP:

Warning FIG7_1. CPP: Non-const function

Time:: setHour (int) called for const object Warning FXG7 l. CPP: Non-const function

Time:: setMinute (int) callers for const object Warning FIG7 1. CPP: Non-const function

Time:: setSecond (int) called for const object

Мал.4. Використання класу Time з константними об'єктами й константними функціями-елементами

Зауваження: Константна функція-елемент може бути перевантажена неконстантним варіантом. Вибір того, яка з перевантажених функцій-елементів буде використатися, виконується компілятором автоматично залежно від того, був об’явлений об'єкт як const чи ні.

Константный об'єкт не може бути змінений за допомогою присвоювання, так що він повинен мати початкове значення. Якщо дані-елементи класу об’явлені як const, то треба використати ініціалізатор елементів, щоб забезпечити конструктор об'єкта цього класу початковими значенням даних-елементів. Мал.7 демонструє використання ініціалізатора елементів для завдання початкового значення константному елементу increment класу Increment. Конструктор для Increment змінюється в такий спосіб:

Increment:: Increment (int c, int i): increment (i) { count = c; }

Запис: increment (i) викликає завдання початкового значення елемента increment, рівного i. Якщо необхідно задати початкові значення відразу декільком елементам, просто включіть їх у список після двокрапки, розділяючи комами. Використовуючи ініціатори елементів, можна присвоїти початкові значення всім даним-елементам.

// Використання ініціалізатора елементів для

// ініціалізації даних константного вбудованого типу.

#include

class Increment { public:

Increment (int з = 0, int i = 1);

void addlncrement () { count += increment; }

void print () const;

private:

int count;

const int increment; // константний елемент даних };

// Конструктор класу Increment Increment:: Increment (int c, int i)

: increment (i) // ініціали затор константного елемента

{ count = с; }

// друк даних

void Increment:: print () const

{

Свежие статьи
Популярно сейчас
Как Вы думаете, сколько людей до Вас делали точно такое же задание? 99% студентов выполняют точно такие же задания, как и их предшественники год назад. Найдите нужный учебный материал на СтудИзбе!
Ответы на популярные вопросы
Да! Наши авторы собирают и выкладывают те работы, которые сдаются в Вашем учебном заведении ежегодно и уже проверены преподавателями.
Да! У нас любой человек может выложить любую учебную работу и зарабатывать на её продажах! Но каждый учебный материал публикуется только после тщательной проверки администрацией.
Вернём деньги! А если быть более точными, то автору даётся немного времени на исправление, а если не исправит или выйдет время, то вернём деньги в полном объёме!
Нет! Мы не выполняем работы на заказ, однако Вы можете попросить что-то выложить в наших социальных сетях.
Добавляйте материалы
и зарабатывайте!
Продажи идут автоматически
4144
Авторов
на СтудИзбе
667
Средний доход
с одного платного файла
Обучение Подробнее