Інкапсуляція та виділення об’єктів у купі

1. Інкапсуляція та Підстановка

На минулих лекціях ми розглянули такі об'єкти як Array, Complex, які були створені для того, щоб не замислюватися про їх докладну внутрішню будову, а лише використовувати їх. (кроки):

більш високі рівні

покажчик, int, char тощо. (базові типи)

Таким чином з'являється позитивна особливість: кожен новий рівень дозволяє приховати нижчий, наприклад, клас Complex нас цікавить не його реалізація, а лише набір його можливостей.

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

Розглянемо наступний код, який використовує клас комплексних чисел: Complex a,b; a.add(b); У другому рядку відбувається виклик методу класу. Для виклику методу (функції) потрібен деякий час, щоб покласти аргументи на стек і перейти до коду цієї функції. У методі add відбудеться додавання чотирьох чисел. Припустимо ми використовуватимемо клас комплексних чисел, а використовуватимемо просто чотири числа, що характеризують два комплексних числа. Тоді на їх додавання витратиться менша кількість часу, тому що не потрібно буде викликати функцію складання комплексних чисел. Таким чином видно, що при інкапсуляції буде відбуватися збільшення часу роботи програми, а це є недоліком.

Спробуємо усунути його.

Ми можемо помістити функцію в той файл, в якому вона викликається. Означає в момент компіляції функція буде виявлена ​​компілятором у цьому файлі і швидше за все її код буде підставлений на місце дзвінка. Це називаєтьсяПідстановка(Inline). Значить, якщо ми розмістимо реалізацію функції у файлі з її викликом, то збільшити час роботи не відбудеться (фактично функція не буде викликатися). Якщо ми маємо кілька файлів у які необхідно вставити реалізацію функції, зробимо це для кожного файлу. Але тепер, коли ми спробуємо скомпонувати всі об'єктні файли, збирач виведе помилку, пов'язану з тим, що він знайшов кілька реалізацій однієї і тієї ж функції.

Для того, щоб не виникало помилок, описаних наприкінці першого пункту, будемо використовувати ключове словоinline. Воно вказується перед типом даних, що повертаються функцією, наприклад inline void add(Complex & b); Таким чином ми не тільки повідомляємо компілятору, що бажано підставити реалізацію в місце виклику, але й повідомляємо збирачеві, що не потрібно виводити помилку при виявленні декількох реалізації однієї функції, а достатньо вибрати будь-яку з цих реалізацій. Тепер розглянемо наступну проблему : якщо нам необхідно змінити функцію, реалізація якої знаходиться в декількох файлах, то нам необхідно змінити її в кожному файлі, а це незручна і можлива поява помилок.

Винесемо реалізацію функції в заголовний файл. Таким чином, підключаючи цей заголовний файл до всіх файлів, в яких викликається наша функція, ми усунемо проблему множинних реалізацій. Наведемо приклад такого файлу заголовка (файл «complex.h»):

Таким чином частково вирішена і ця проблема підстановки, тому що не завжди відбувається підстановка (вона на розсуд компілятора).

Тепер необхідно зрозуміти, коли можна і потрібно використовувати підстановку, а коли — ні.

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

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

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

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

Примітка: Компілятори - досить розумні програми, щоб замість підстановки функції

місце її виклику int a = factorial(5); скомпілювати це місце виклику як int a = 120;

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

2. Створення об'єктів у купі.

Доти розглядалося створення об'єктів на стеку: Array a(10); Але час життя такого об'єкта обмежено областю видимості (переважно фігурними дужками). А значить неможливо створити функцію, яка б створювала всередині себеоб'єкт деякого типу і передавала його функції, що викликала. Ще раніше ми розглядали покажчик на стандартний тип, і виглядав він приблизно так: int * ptrInt1; int * ptrInt2 = new int;

Створення покажчика на об'єкт.

Однак, крім покажчиків на стандартні типи існують і покажчики на типи користувача даних. Нехай є клас Complex, що представляє комплексне число. Тоді покажчик на нього записуватиметься аналогічно покажчику на простий тип: Complex * prtC1; Самому покажчику при цьому можна присвоїти 0. prtC1 = 0;

Використання вказівника на об'єкт, створення об'єкта в купі.

Але просто вказівник нам навряд чи потрібний. Було б дуже добре, якби він вказував на об'єкт класу Complex. Для цього потрібно або

Створимо об'єкт у купі. Виділення об'єктів власних класів у купі відбувається так само, як і для звичайних типів, з використанням new і зазначенням імені типу. Але, на відміну від простих типів, у об'єкта класу завжди викликається конструктор, тому необхідно ставити круглі дужки після імені типу, навіть якщо є конструктор без аргументів. Таким чином, у рядках Complex * ptrB = new Complex(); Complex * ptrC = new Complex(10,0); відбувається спочатку виділення пам'яті під об'єкт, потім виклик конструктора, і вже наприкінці привласнення покажчику.

Як і простих типів, ми можемо створити відразу масив об'єктів Complex * ptrArr = new Complex[10]; У такому випадку необхідно вказати розмір масиву і у класу повинен бути конструктор за замовчуванням, тому як не можна вказувати параметри конструктору при виділенні масивів. замовчуванням.

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

Використання об'єкта, створеного в купі.

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

Спочатку створимо два покажчика різні комплексні числа: Complex * a = new Complex(1,0); Complex * b = new Complex(0,1);

Але щоразу писати операцію розіменування незручно. Для усунення цієї проблеми існує спеціальний оператор -> , який фактично перетворюється на розіменування. Тобто наступний рядок буде еквівалентним минулому: int aRe = a->getRe();

Як і для простих типів, до класів користувача застосовні посилання, тобто Complex & c = * a; Тоді буде посилатися на a. Отже можна писати і так: int aIm = c.getIm(); Відмінною рисою посилань і те, що ми користуємося ними як самими екземплярами класів.

Вибір методу зберігання об'єкта.

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

Розглянемо деякі міркування щодо методів зберігання класу залежно від його характеристик:

Оскільки розмір стека обмежений, не слід зберігати на ньому навіть невелику кількість великих об'єктів. У цьому випадку необхідно використати купу.

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

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

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

Щоб запобігти витоку пам'яті, потрібно видалити об'єкт. І тому використовується операції delete і delete[] . Відрізняються вони тим, перша видаляє об'єкт вважаючи, що передано покажчик однією об'єкт, а друга видаляє масив об'єктів (їй передається покажчик початку масиву). Використання буде таке: delete a; delete[] ptrArr;

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

Проблеми з вказівниками на об'єкт

Розглянемо можливі проблеми, пов'язані з вказівниками на об'єкти:

Complex *pa = новий Complex(); Complex *pb = новий Complex(); pb->add(*pa);

Такий код чудово працюватиме. Але якщо написати так:

Complex *pa = new Complex(); Complex *pb = 0; pb-add(*pa);

Тоді код компілюватиметься, але програма зламається при виконанні pb->add(*pa) при спробі доступу до полів об'єктаpb, якого не існує.

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

Згадаймо про масиви об'єктів у купі, про які говорили раніше. Створюються вони як і масиви найпростіших типів. int * p = new int [20]; Complex * r = new Complex [10];

При цьому в пам'яті виділяється місце під 10 елементів типу Complexі у кожного викликається конструктор за замовчуванням. Відповідно видалення здійснюється так:

delete p; вивільняється пам'ять. delete[] r; викликається деструктор кожного елемента (всього 10) і потім вивільняється пам'ять.

Таким чиномnewіdeleteна відміну відmallocіfreeвикликають так само конструктор і деструктор відповідно.

При викликуnew[]іdelete[], ці процедури використовують додаткову інформацію про кількість елементів.