Класи і об'єкти

Зверніть увагу на те, де ми описуємо тіло того чи іншого методу. Методи mod () і protection () описані разом зі своїми тілами безпосередньо всередині структури. Але можна поступити інакше: помістити прототипи методу всередину структури, а визначення тіла функції - поза структурою, як ми вчинили з оператором "+".







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

Крім представленого вище способу створення вбудованих функцій (записати тіло методу безпосередньо в структурі), є ще один спосіб - вставити специфікатор inline перед визначенням методу:

inline _3d _3d :: operator + (_3d b)
_3d c;
c.x = x + b.x;
c.y = y + b.y;
c.z = z + b.z;
return c;
>

Тепер оператор "+" стане вбудовується функцією.

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

Важливо розуміти, що inline не є командою для компілятора, це скоріше прохання зробити метод вбудовуваним. Якщо з якихось причин (наприклад, при наявності в тілі функції операторів циклу, switch або goto) компілятор не виконає запит, то функція буде відкомпільована як невстраіваемая.

Конструктори і деструктори

_3d vectorA;
double m;
vectorA.x = 17.56;
vectorA.y = 35.12;
vectorA.z = 1.0;
m = vectorA.mod ();

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

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

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

Метод set () дозволяє привласнити деякі початкові значення координат вектора (і тільки цей метод!).

Ще одне зауваження: хоча x, y і z тепер відносяться до захищених членів класу, явне звернення цих елементів об'єкта, переданого в якості параметра (див. Метод projection (.) І оператор "+"), як і раніше допускається.

Конструктори в мові С ++ мають імена, що збігаються з ім'ям класу. Конструктор може бути визначений користувачем, або компілятор сам згенерує конструктор за замовчуванням. Конструктор може викликатися явно, або неявно. Компілятор сам автоматично викликає відповідний конструктор там, де Ви визначаєте новий об'єкт класу. Конструктор не повертає ніякого значення, і при описі конструктора не використовується ключове слово void.

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

У мові С ++ деструктори мають імена: "

ім'я_класу ". Як і конструктор, деструктор не повертає ніякого значення, але на відміну від конструктора не може бути викликаний явно. Конструктор і деструктор не можуть бути описані в закритій частині класу.

_3d :: _ 3d () // конструктор класу _3d

x = y = z = 0;
cout <<'Работа конструктора _3d \n';
>

main ()
_3d A; // створюється об'єкт A і відбувається ініціалізація його елементів
// A.x = A.y = A.z = 0;
A.set (3,4,0); // Тепер A.x = 3.0, A.y = 4.0, A.z = 0.0
cout <>

Результат роботи програми:

Робота конструктора _3d
5.0
Робота деструктора _3d

Конструктори з параметрами і перевантаження конструкторів

class _3d
double x, y, z;
public:
_3d ();
_3d (double initX, double initY, double initZ);
.
>;

_3d :: _ 3d (double initX, double initY, double initZ)
// конструктор класу _3d з параметрами
x = initX;
y = initY;
z = initZ;
cout <<'Работа конструктора _3d \n';
>

main ()
_3d A; // створюється об'єкт A і відбувається ініціалізація його елементів
// A.x = A.y = A.z = 0;
A.set (3,4,0); // Тепер A.x = 3.0, A.y = 4.0, A.z = 0.0
_3d B (3,4,0); // створюється об'єкт B і відбувається ініціалізація його елементів
// B.x = 3.0, B.y = 4.0, B.z = 0.0
>

Такий спосіб виклику конструктора є скороченою формою запису виразу

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







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

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

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

Присвоєння об'єктів

class ClassName1
int a, b;
public:
void set (int ia, int ib)
>;

class ClassName2
int a, b;
public:
void set (int ia, int ib)
>;

Так що спроба виконати

ClassName1 c1;
ClassName2 c2;
c2 = c1;

В результаті роботи програми отримаємо "c2.b = 111", а не 11, як очікувалося.

Перевантажений оператор присвоювання

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

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

Наївно було б припускати, що для кожної нової змінної типу _3d створюється копія функції, що реалізує оператори "+" і "=". Кожна функція представлена ​​в єдиному екземплярі і в момент виклику отримує один прихований параметр - покажчик на екземпляр, для якого вона викликана. Цей покажчик має ім'я this. Якщо використовувана змінна не описана всередині функції, не є глобальною, то вважається, що вона є членом структури і належить робочої змінної this. Тому при реалізації функцій операторів ми опускали шлях доступу до полів структури, для якої цей оператор буде викликатися.

В якості аргументів функцій-операторів виступають операнди, а повертається значення - результат застосування оператора. Зокрема для оператора "=" це необхідно, щоб забезпечити можливість послідовного присвоювання (a = b = c). Бінарні оператори мають один аргумент - другий передається через покажчик this. Унарні, відповідно, один - this.

Передача в функції і повернення об'єкта

Об'єкти можна передавати у функції в якості аргументів точно так же, як передаються дані інших типів. Всім цим ми вже користувалися, коли реалізовували методи класса_3d. в якому в якості параметрів методів projection (.), operator + (.) і operator = (.) передавали об'єкт типу _3d.

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

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

class ClassName
public:
ClassName ()
cout <<'Работа конструктора \n';
>

ClassName ()
cout <<'Работа деструктора \n';
>
>;

void f (ClassName o)
cout <<'Работа функции f \n';
>

Ця програма виконає наступне

Робота конструктора
Робота функції f
Робота деструктора
Робота деструктора

Конструктор викликається тільки один раз. Це відбувається при створенні з1. Однак деструкція спрацьовує двічі: один раз для копії o. другий раз для самого об'єкта c1. Той факт, що деструкція викликається двічі, може стати потенційним джерелом проблем, наприклад, для об'єктів, деструкція яких вивільняє динамічно виділену область пам'яті.

Схожа проблема виникає і при використанні об'єкта в якості значення, що повертається.

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

class ClassName public:
ClassName ()
cout <<'Работа конструктора \n';
>

ClassName ()
cout <<'Работа деструктора \n';
>
>;

ClassName f ()
ClassName obj;
cout <<'Работа функции f \n';
return obj;
>

Ця програма виконає наступне

Робота конструктора
Робота конструктора
Робота функції f
Робота деструктора
Робота деструктора
Робота деструктора

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

Конструктор викликається два рази: для з1 і obj. Однак деструкторов тут три. Як же так? Зрозуміло, що один деструктор руйнує з1, ще один - obj. "Зайвий" виклик деструктора (другий за рахунком) викликається для так званого тимчасового об'єкта. який є копією об'єкта, що повертається. Формується ця копія, коли функція повертає об'єкт. Після того, як функція повернула своє значення, виконується деструкція тимчасового об'єкта. Зрозуміло, що якщо деструктор, наприклад, вивільняє динамічно виділену пам'ять, то руйнування тимчасового об'єкта призведе до руйнування об'єкта, що повертається.

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

Будь-конструктор копіювання має наступну форму:

ім'я_класу (const ім'я_класу obj)
. // тіло конструктора
>

class ClassName
public:
ClassName ()
cout <<'Работа конструктора \n';
>
ClassName (const ClassName obj)
cout <<'Работа конструктора копирования\n';
>

ClassName ()
cout <<'Работа деструктора \n';
>
>;

main ()
ClassName c1; // виклик конструктора
ClassName c2 = c1; // виклик конструктора копіювання
>

Зауваження: конструктор копіювання не впливає на операцію присвоювання.

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

Покажчики та посилання на об'єкти

До сих пір доступ до членів об'єкта здійснювався, використовуючи операцію ".". Це правильно, якщо ви працюєте з об'єктом. Однак доступ до членів об'єкта можна здійснювати і через покажчик на об'єкт. В цьому випадку зазвичай застосовується операція стрілка "->".

Покажчик на об'єкт оголошується точно так же, як і покажчик на змінну будь-якого типу.

void ToZero (_3d * vec)

В С ++ можна зробити те ж саме, не використовуючи покажчики, за допомогою параметра-посилання.

void ToZero (_3d vec)
vec.set (0,0,0); // використовується "."
>

На відміну від передачі в якості параметра об'єкта, використання посилання-параметра передбачає, що функція, в яку переданий параметр, буде маніпулювати НЕ копією переданого об'єкта, а саме цим переданим об'єктом.

_3d _3d :: operator = (_3d b)

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







Схожі статті