C++

Матеріал з Вікіпідручника
Перейти до: навігація, пошук

C++ (Сі-плюс-плюс) — універсальна мова програмування високого рівня з підтримкою декількох парадигм програмування: об'єктно-орієнтованої, узагальненої та процедурної. Розроблена Б'ярном Страуструпом (Bjarne Stroustrup) в AT&T Bell Laboratories (Мюррей-Хілл, Нью-Джерсі) у 1979 році та названа «Сі з класами». Страуструп перейменував мову у C++ у 1983 р. Базується на мові Сі. Визначена стандартом ISO/IEC 14882:2003.

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

Зміст

Особливості[ред.]

При створенні С++ прагнули зберегти сумісність з мовою С. Більшість програм на С справно працюватимуть і з компілятором С++. С++ має синтаксис, заснований на синтаксисі С.

Нововведеннями С++ порівняно з С є:

У 1998 році ратифіковано міжнародний стандарт мови С++: ISO/IEC 14882 «Standard for the C++ Programming Language». Поточна версія цього стандарту — ISO/IEC 14882:2003.

Приклад програми «Hello, world!»[ред.]

Нижче наведено приклад простої програми на С++, яка виводить на стандартний канал виводу рядок Hello, world!.

  #include <iostream>

  int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
  }

Директиви препроцесора[ред.]

Директиви препроцесора починаються зі знаку «решітка» - «#», який має бути самим першим символом рядка. Програма яка обробляє ці директиви, називається препроцесором (в сучасних компіляторах препроцесор зазвичай є частиною самого компілятора). Файли заголовків включаються в текст програми за допомогою директиви #include. Директива #include включає в програму вміст вказаного файлу. Ім'я файлу може бути вказано двома способами:

#include "my_file.h"
#include <string>

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

Файл заголовку також може містити директиви #include. Тому іноді, важко зрозуміти, які конкретно файли заголовку вже включені в даний код і деякі файли заголовків можуть виявитись включеними декілька разів. Повторне включення файлу призводить до помилки компіляції із-за повтору імен. Уникнути цього дозволяють умовні директиви компілятора. Приклад:

#ifndef BOOKSTORE_H
...
#endif

Умовна директива #ifndef дозволяє перевірити, чи не було значення BOOKSTORE_H визначено раніше. (BOOKSTORE_H – константа препроцесора; такі константи прийнято писати великими буквами). Препроцесор дозволяє виконати наступні рядки коду програми до самого включення директиви #endif. У випадку коли умова не виконується і константа вже була визначена, він пропускає ці рядки, які знаходяться між #ifndef і #endif.

Директива

 #define BOOKSTORE_H

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

Змінні[ред.]

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

У мові програмування C++ існують специфікатори збереження і кваліфікатори доступу. Із-за того, що вони не так часто використовуються, і існує деяка плутанина взаємозамінних термінів, іноді виглядає складною як для початківців, так і для тих хто вже програмує і використовує ці речі.

Кваліфікатори доступу[ред.]

Кваліфікатори доступу ще іноді називають кваліфікаторами типів (Type Qualifiers), дозволяють задати змінним особливих властивостей, якщо їх вказати перед їх оголошенням. Існуть такі кваліфікатори:

  • const – змінні, оголошені з цим кваліфікатором, не можуть бути змінені в ході виконання програми.
  • volatile – модифікатор вказує компілятору, що значення змінної може бути змінене не явним способом, тобто без явного використання оператору присвоювання.
  • restrict – застосовується до оголошення або визначення функції, яка повертає вказівник; повідомляє компілятору, що цей об’єкт, який не можна буде зв’язувати ні з якими іншими вказівниками. З’явився лише в стандарті C99.

Специфікатори збереження[ред.]

Специфікатори збереження (storage class), це такі спеціальні ключові слова, які використовуються для змінних і функцій і визначають область видимості і час існування змінної або функції в програмі C++. Ці специфікатори передують перед назвою типу. В мові C++ існують наступні специфікатори:

  • auto
  • register
  • static
  • extern
  • mutable

Автоматичні змінні[ред.]

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

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

{
   int mount;
   auto int month;
}

Статичні змінні[ред.]

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

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

#include <iostream>
 
// Function declaration
void func(void);
 
static int count = 10; /* Глобальна змінна */
 
main()
{
    while(count--)
    {
       func();
    }
    return 0;
}

void func( void )
{
    static int i = 5; // локальна статична змінна
    i++;
    std::cout << "i = " << i ;
    std::cout << ", count = " << count << std::endl;
}

Після виконання цей код виведе на екран наступне:

i = 6, count = 9
i = 7, count = 8
i = 8, count = 7
i = 9, count = 6
i = 10, count = 5
i = 11, count = 4
i = 12, count = 3
i = 13, count = 2
i = 14, count = 1
i = 15, count = 0

Регістрові змінні[ред.]

Ключове слово register застосовується до локальних змінних, якщо хочуть щоб вони зберігалися в регістрах процесора замість оперативної пам'яті RAM. Це означає, що змінна має максимальний розмір, який дозволено заносити в регістр, а також до неї не можна застосовувати унарний оператор взяття адреси комірки '&', оскільки ця змінна не має адреси в пам’яті.

{
   register int  miles;
}

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

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

Зовнішні змінні[ред.]

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

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

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

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

Перший файл: main.cpp

#include <iostream>
 
int count ;
extern void write_extern();
 
main()
{
   count = 5;
   write_extern();
}

Другий файл: support.cpp

#include <iostream>
 
extern int count;
 
void write_extern(void)
{
   std::cout << "Count is " << count << std::endl;
}

mutable змінні[ред.]

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

Стекові і динамічні змінні[ред.]

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

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

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

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

Складні типи даних[ред.]

Масиви[ред.]

Масив це послідовність елементів одного типу даних, які розміщуються в суміжних комірках пам'яті, до кожного з яких можна доступитися за допомогою індексу, який додається до ідентифікатору. Це означає що, наприклад, для масиву, який складається з 5 значень типу int, не потрібно створювати 5 окремих змінних, де кожна буде з власним ідентифікатором. Замість того, ми можемо зберігати 5 різних значень одного типу, під одним унікальним ідентифікатором.

Наприклад, структура массиву під назвою “array” може бути представлений так:

ArrayView.png

Де кожен пустий прямокутник представляє собою елемент масиву типу int. Ці елементи нумеруються з 0 до 4, оскільки перший індекс масиву завжди 0. Як і звичайний тип даних масив повинен бути оголошений перед використанням. Типовий формат оголошення масиву наступний:

назва_типу ім’я [розмір];

де “назва_типу” це тип даних (такий як int, float...), ім’я це унікальний ідентифікатор і розмір – це кількість елементів в масиві, яка завжди сказується у квадратних дужках. Таким чином, наш массив з 5-и елементів буде створений таким чином:

int array [5];

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

Ініціалізація масивів[ред.]

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

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

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

int array [5] = { 1, 2, 4, 8, 16 };

Цей масив буде виглядати так:

ArrayView2.png

Кількість значень між дужками { } не повинна бути більшою ніж розмір масиву, який ми задали раніше. При ініціалізації значень масиву, C++ дозволяє залишити квадратні дужки пустими. В такому випадку, компілятор буде вважати, що розмір масиву відповідатиме кількості заданих значень в фігурних дужках. Наприклад, такий вираз також створить масив з 5-ти елементів:

int array [] = { 1, 2, 4, 8, 16 };

Доступ до елементів масиву[ред.]

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

array[n]

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

int array [5] = { 1, 2, 0, 8, 16 }; 
array[2] = 4; // запишемо значення 4 для третього елементу масиву
int a = array[2]; // дістанемо значення третього елементу і запишемо його у змінну a

Якщо ми спробуємо доступитися до шостого елементу нашого масиву, оскільки елементів всього п’ять ми вийдемо за рамки массиву. В C++ це не вважається синтаксичною помилкою, і компілятор нічього не повідомить і така помилка проявить себе лише в момент виконання програми.

Багатомірні масиви[ред.]

Багатомірні масиви можна описати як "масиви масивів". Наприклад, двомірний масив можна розглядати як таблицю елементів, які мають однаковий тип даних.

Приклад оголошення масиву розміром 3 на 5 виглядатиме так:

int array [3][5];
Twodimarray.png

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

Передача масивів у функції[ред.]

Іноді може бути потрібним передати масив у функцію у вигляді параметру. В C++ не можливо передати повний блок даних у пам’яті по значенню як параметр, але можливо передати його адресу. На практиці це майже те саме, і це навіть більш швидка і ефективна операція. Для того, щоб функція могла прийняти масив як параметр, при оголошенні функції ми повинні для даного переметру вказати тип елементів масиву, ідинтифікатор і порожні квадратні дужки []. Наприклад:

int sum_array (int arg[])

приймає в якості параметра масив цілих чисел з ідентифікатором arg.

Код, кий передаватиме в цю функцію масив буде виглядати так:

int myarray [5] = { 1, 2, 4, 8, 16 };

sum_array (myarray);

Ось повний приклад:

int sum_array (int arg[], int length) 
{
	int result = 0;
	for (int i=0; i<length; i++) 
	{
	 	result+=arg[i];
	}
	return result;
}

int main () 
{ 
	int myarray [5] = { 1, 2, 4, 8, 16 };
	cout << "\n"; 
	cout << "Сума дорівнює:" << sum_array (myarray,5)<< "\n"; 
	return 0; 
}

Оскільки нам треба знати довжину масива, щоб використати у циклі, ми передаємо її за допомогою другого параметру int length.

В функцію можливо передавати також багатовимірні масиви. Формат передачі у функцію параметру багатовимірного масиву буде таким:

base_type identifier[][depth][depth]

Наприклад:

void myprocedure (int myarray[][3][4])

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

Символьні послідовності[ред.]

Оскільки строки являють собою послідовность символів, в C++ ми можемо представити їх у вигляді масивів символів, де кожен елемент буде мати тип char. Наприклад:

char message [20];

Такий масив може зберігати до 20 елементів типу char. Таким чином, в цьому масиві ми можемо зберігати послідовності символів довжиною до 20 літер. Але ми можемо зберігати і коротчі послідовності. У змінній message ми можемо зберігати послідовності різної довжини, наприклад, ми можемо зберегти в ній строку “Hello”, або "Wikipedia world", оскільки вони коротше ніж 20 символів. Таким чином, оскільки масив може містити послідовності символів, довжину яких слід якось позначити, застосовується спаціалний символ, який позначає кінець послідовності. Це нульовий символ, константне значення якого можна записати у вигляді літералу '\0' .

Наш масив з 20 елементів, який зберігає вищевказані строки тоді виглядатиме так:

Char arrays.png

Елементи, що знаходяться справа від символу '\0' в даному масиві, не є частиною нашої строки, але оскільки наш масив має фіксовану довжину, вони залишаються зарезервованими в пам’яті.

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

char message[] = { 'H', 'e', 'l', 'l', 'o', '\0' };

В такому випадку ми оголосили масив з 6 елементів типу char, і занесли туди строку зі словом “Hello” плюс символ завершення строки '\0' в кінці. Але символьні масивам можна також задати початкове значення за допомогою строкового літералу, які задаються у вигляді тексту, який записується в подвійних лапках ("). Наприклад :

char message[] = "Hello";

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

Простори імен[ред.]

Простори імен (namespaces) призначені для локалізації імен ідентифікаторів, і попередження їх конфліктів. По замовчуванню всі ідентифікатори знаходяться в глобальному просторі імен, тому часті випадки існування двох різних об'єктів з однаковими іменами, що призводить до помилок. Щоб цьому запобігти глобальний простір імен ділять на менші. Наприклад стандартну бібліотеку C++ винесено в область названу std.

Створення просторів імен[ред.]

Щоб створити простір імен використовують ключове слово namespace. Код:

 namespace space_name
 {
    // Оголошення
 }

Доступ до ідентифікаторів з просторів імен[ред.]

Щоб отримати доступ до об'єктів з простору іменн з за його меж використовують оператор дозволу області видимості ( :: ). Наприклад:

 namespace some
 {
     int something;
 }
 something=1; // Помилка, something невидиме.
 some::something=1; // Ми вказали в якій області його шукати.

Правда такий спосіб може бути страшенно незручним, якщо ми використовуємо багато ідентифікаторів не з нашої області видимості. Щоб полегшити нам життя придумана директива using. Хай ми маємо простір кімнати:

 namespace room
 {
    Wall wall1,wall2,wall3,wall4;
    Ceil ceil;
 }

Можна внести стелю в наш іменний простір написавши що ми її будем використовувати:

 using room::ceil;
 ceil=42; // Тепер стеля видима
 wall1=wall2; // А таке все ще викличе помилку!

А можна взагалі розкрити увесь простір імен:

 using room;
 wall1=wall2=wall3; // Всі ідентифікатори з кімнати доступні

Деякі особливості просторів імен[ред.]

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

Константи[ред.]

Якщо змінна оголошена з ключовим словом const, це означає, що її значення не повинне змінюватись.

const int j = 13; // Константа типу int 
j = 45; // Помилка. Не можна змінювати значення
const int i; // Помилка. відсутнє початкове значення константи.

Константу можна визначити ще і традиційним способом, за допомогою директиви #define, наприклад:

#define five 5

Визначення констант за допомогою const і #define не є еквівалентними. Перший спосіб визначає змінну, яка займає деяку область пам'яті, а другий спосіб - макрос. Зверніть увагу, що константа five не має ніякого конкретного типу (int, char, і т.д.). Директива #define виконує просту текстову заміну. Кожен раз, коли препроцесор зустрічає слово five, він замінює його літералом 5. Оскільки препроцесор запускається перед компілятором, останній ніколи не дізнається про цю константу, а буде бачити лише число 5.

Константи в перерахуваннях[ред.]

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

class Foo {
public:
enum Status { kOpen = 1, kClosed };
};

// Десь у програмі
Foo::Status s = Foo::kOpen;

Зверніть увагу — область дії повинна бути явно вказана як в імені типу, так і в символічному імені. Отже, символічні імена kOpen і kClosed можна використовувати в програмі без великої ймовірності, що такі імена повторяться. Компілятор розглядає символічні імена перерахувань як макроси, а не як константні змінні.

Вказівник на константу[ред.]

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

const int* p; // Вказівник на константу
int i = 17;
p = &i; 
*p = 29; // Помилка

Константний вказівник[ред.]

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

int i = 17;
int j = 29;
int* const p; // Помилка! Необхідно задати початкове значення
int* const p1 = &i; 
*p1 = 29; 
p1 = &j; //Помилка

Константний вказівник на константу[ред.]

Це незмінна адреса незмінної величини.

int i = 17;
int j = 29;
const int* const p; // Помилка
const int* const p1 = &i; // Можна
*p1 = 29; // Помилка
p1 = &j; // Помилка

Вказівники[ред.]

Вказівник це змінна, значенням якої є адреса комірки пам’яті. Тобто вказівник посилається на блок даних із області оперативної пам’яті, при чому в самий його початок. Вказівник може посилатися на змінну або функцію. Для того, щоб дізнатися адресу змінної в С++ існує унарна операція взяття адреси “&”. Така операція повертає адресу об’явленої змінної, для того, щоб присвоїти її вказівнику.

Вказівник об’являється за тим самим принципом що і змінна, тільки перед ім’ям змінної ставиться символ *:

тип_даних * ім’я_змінної;

Наприклад:

int *ptr_a; // вказівник;
int b = 10; // проста змінна
ptr_a = &b; // взяття адреси і присвоєння її вказівнику.

В програмуванні прийнято додавати до імені вказівника приставку ptr, таким чином, не дозволяючи плутати прості змінні і змінні вказівників.

Вказівники на вказівники[ред.]

Вказівники можуть посилатися на інші вказівники. При цьому в комірці пам'яті, на котру буде посилатися перший вказівник, буде міститися не значення, а адреса іншого вказівника. Кількість символів “*” при об’явленні вказівника показує порядок вказівника. Щоб отримати доступ до значення, на яке посилається вказівник його слід розіменувати декілька разів.

Вказівники на функції[ред.]

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

тип_даних (* ім’я_вказівника)(аргумент_1[, аргумент_2, ...]);

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

Динамічне виділення пам'яті[ред.]

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

Основні відмінності між статичними і динамічними об'єктами такі:

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

Для динамічного виділення пам'яті існують оператори new і delete. Оператор new має дві форми. Перша форма виділяє пам'ять під динамічний об'єкт одиничного типу:

int *ptr_int = new int(1024);

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

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

int *ptr_array = new int[4];

В цьому прикладі відбувається виділення пам'яті під масив із чотирьох елементів типу int. Така форма оператора new не дозволяє ініціалізувати елементи масиву, там будуть випадкові значення. Деяку плутанину вносить те, що обидві форми оператору new повертають однаковий вказівник. В прикладі вказівник на ціле число і на масив оголошені однаково. .Коли динамічний об'єкт більше не потрібен, мі повинні явним способом звільнити відведену під нього пам'ять. Це робиться за допомогою оператору delete, який як і new також має дві форми – для одиничної змінної і для масиву:

delete[] ptr_array;

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

Приклад:

int *pi = new int(0);
int *p_array = new int[10];
while ( *pi < 10 ) {
  p_array[*pi] = *pi;
  *pi = *pi + 1;
}
delete pi;

Класи[ред.]

Основні поняття[ред.]

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

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

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

Поля (або властивості) — описують дані, які можуть зберігати в собі екземпляри класу. До полів в середині класу можна звертатися безпосередньо по іменам полів.

Методи класу — це функції, котрі можуть застосовуватися до екземплярів класу. Грубо кажучи, метод – це функція, яка об'явлена всередині класу і призначена для роботи з його об'єктами.

Основні принципи ООП:

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

Синтаксис[ред.]

Класи оголошуються за допомогою ключового слова class, і синтаксис має наступний формат:

class ім’я_класу { 
   специфікатор_доступу_1: 
   член_класу1; 
   специфікатор_доступу_2: 
   член_класу2; 
   ... 
} імена_об’єктів;

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

Наприклад:

// клас з ідентифікатором CRectangle
class CRectangle { 
  int x, y;                   // поля класу
  public:                     // специфікатор доступу
  void set_values (int,int);  // методи
  int area (void); 
} rect;                       // rect є об'єктом класу CRectangle

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

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

rect.set_values (3,4); 
myarea = rect.area();

Члени класу можна визначати за його методами, це в першу чергу стосується функцій. Інтерфейсну частину класу і реалізацію зазвичай розносять в різні файли, в файл заголовку (*.h) і файл початкового коду (*.cpp) відповідно. Для визначення методів за межами класу в такому випадку використовується спеціальний оператор розмежування області видимості ::, без використання якого не зрозуміло було б що це не є звичайна глобальна функція, а це є функція, що належить класу. Приклад:

void CRectangle::set_values (int a, int b) { 
    x = a; 
    y = b; 
}

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

Рівні доступу до членів класу[ред.]

По рівню доступу всі члени класу діляться на:

  • Public (відкриті) – доступні як в середині так і ззовні класу (в тому числі в його нащадках),
  • Private (закриті) – доступні лише в середині класу,
  • Protected (захищені) – доступні всередині класу і в середині всіх його нащадків.

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

class Flower {
    public: // після цього модифікатору слідують відкриті члени класу.
        void setHeight(float value)
        {
            if (value > 0)
                this->height = value;
        }

        float getHeight()
        {
            return height;
        }

        void setName(std::string value)
        {
            this->name = value;
        }

        float getName()
        {
            return name;
        }

    private: // закриті. 
        float height;    // Доступ до цих полів здійснюється за допомогою методів
        std::string name;
};

Конструктор і деструктор[ред.]

Об'єктам зазвичай необхідно задавати початкові значення змінним (полям класу) для того, щоб вони мали якийсь змістовний вміст, або виділяти динамічну пам'ять під час створення. Інакше при використанні об'єкту можна отримати непередбачувану поведінку при виконанні програми. Наприклад, що б сталося у попередньому прикладі, якби ми викликали метод, який рахує площу чотирикутника не задавши йому розмір за допомогою методу set_values()? Напевне вона б повернула, якийсь невизначений випадковий результат, оскільки б змінним x і y ніде б не задавались значення.

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

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

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

class Flower {
    public: 

        Flower(float h, std::string n) // Конструктор
        {
            this->height = 0;
            if (h > 0)
                this->height = h;

            this->name = n;
        }

        //... інші методи

    private:  
        float height;    
        std::string name;
};

int main()
{
    // Передаємо значення в конструктор
    Flower *sunflower = new Flower(54.2, "Соняшник");
    return 0;
}

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

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

Перевантаження конструкторів[ред.]

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

// перевантаження конструкторів класу 
#include <iostream> 
using namespace std; 
 
class CRectangle { 
 int width, height; 
 public: 
 CRectangle (); 
 CRectangle (int,int); 
 int area (void) {return (width*height);} 
}; 
 
CRectangle::CRectangle () {  // конструктор
 width = 5; 
 height = 5; 
} 
 
CRectangle::CRectangle (int a, int b) {  // конструктор з двома параметрами
 width = a; 
 height = b; 
} 
 
int main () { 
 CRectangle rect (3,4);  // буде викликаний другий конструктор, який має два параметри
 CRectangle rectb;       // буде викликаний конструктор за замовченням
 cout << "rect area: " << rect.area() << endl; 
 cout << "rectb area: " << rectb.area() << endl; 
 return 0; 
}

Порядок виклику конструкторів:

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

  1. Спочатку викликаються конструктори базових класів в порядку їх перерахування в списку наслідування (список, в якому після символу : перераховуються базові класи, розділені комами).
  2. Потім викликаються конструктори полів класу в порядку їх появи в об’явленні класу.
  3. Після того як будуть сконструйовані всі базові класи і поля, виконається конструктор нашого класу.

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

Конструктор за замовчанням[ред.]

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

class CExample { 
 public: 
 int a,b,c; 
 void multiply (int n, int m) { a=n; b=m; c=a*b; }; 
 };

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

CExample ex;

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

class CExample { 
 public: 
 int a,b,c; 
 CExample (int n, int m) { a=n; b=m; }; 
 void multiply () { c=a*b; }; 
};
// Лише таке визначення об'єкту буде правильним для цього прикладу
CExample ex (2,3); 
// А такий спосіб викличе помилку компіляції:
CExample ex;

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

  • конструктор копій;
  • оператор присвоювання, який копіює об'єкт;
  • і конструктор за замовчанням.

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

CExample::CExample (const CExample& rv) { 
   a=rv.a; b=rv.b; c=rv.c; 
}

Вказівники на класи[ред.]

В C++ можливо створювати вказівники на класи. Оскільки клас, є таким самим типом як і звичайні, ми можемо використовувати ім'я класу в якості імені типу, для оголошення вказівників. На приклад: CRectangle * prect;

є вказівником на об'єкт класу CRectangle.

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

Приклади комбінацій з використанням операторів "*", "&", ".", "->", "[ ]":

Вираз інтерпретується як
*x Об'єкт на який вказує x
&x Адреса об'єкту x
x.y y - член об'єкту x
x->y y - член об'єкту на який вказує x
(*x).y y - член об'єкту на який вказує x (еквівалент попереднього випадку)
x[0] Перший елемент масиву, на який вказує x
x[1] Другий елемент масиву, на який вказує x
x[n] (n+1) елемент масиву, на який вказує x

Статичні члени класу[ред.]

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

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

// статичні члени класу 
#include <iostream> 
using namespace std; 
 
class CDummy { 
 public: 
 static int n; // статичне поле класу
 CDummy () { n++; }; // конструктор
 ~CDummy () { n--; }; // деструктор
}; 
 
int CDummy::n=0; // визначаємо статичне поле класу 
 
int main () { 
 CDummy a; 
 CDummy b[5]; 
 CDummy * c = new CDummy; 
 cout << a.n << endl; 
 delete c; 
 cout << CDummy::n << endl; 
 return 0; 
}

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

int CDummy::n=0; // визначаємо статичне поле класу

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

cout << a.n; 
cout << CDummy::n;

Ці дві строки коду, звертаються до однієї тієї самої змінної: до статичної змінної n, яка належить до класу CDummy, і є спільною для всіх об'єктів цього класу.

Шаблони[ред.]

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

В початковій версії С++ не було шаблонів, але зараз вони підтримуються кожним компілятором, і разом з бібліотекою шаблонів STL входять до стандарту C++.

Функції[ред.]

Шаблон функції виглядає так:

 template <class Ідентифікатор_типу> Тип_результату Назва_функції(Список_параметрів)
 {
     // Тіло функції
 }

Параметр "Ідентифікатор_типу" задає тип з яким працює функція. Всюди в тілі і заголовку функції компілятор замість цього ідентифікатора підставить фактичний тип, який передається. Наприклад функція перестановки двох змінних:

 template <class Type> void swap(Type &a, Type &b)
 {
       Type tmp=a;a=b;b=tmp;
 }

Замість ключового слова class можна застосовувати typename, але більше поширений перший варіант.

Шаблон може узагальнювати кілька різних типів даних:

 template <class Type1, class Type2> void foo(Type1 a, Type2 b)
 {
     printf("Тут ми щось робимо з даними");
     // Неправда
 }

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

 template <int n> int mul(int x)
 {
      return n*x;
 }

Шаблонну функцію можна перевантажувати. Наприклад:

 unsigned int foo(unsigned int a,unsigned int b)
 {
      return ((a+b+1)*(a+b)/2)+a;
 }

Примітка: за MSVC++ помічена звичка замість генерування коду для кожного типу параметрів просто для найбільшого з використовуваних (наприклад, внаслідок викликів swap<int>() і swap<double>() буде згенеровано код лише для swap<double>(), який при потребі викликатиметься з перетворенням типів)

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

#include <stdio.h>

template <class Type1, class Type2> void foo(Type1 a, Type2 b)
{
	printf("Template foo\n");
}
int foo(int a, int b)
{
	return ((a+b+1)*(a+b)/2)+a;
}
int main(void)
{
	float x;
	foo(x,x);
	for(int i=0;i<3;i++)
		for(int j=0; j<3;j++)
			printf("foo(%i,%i)=%i\n",i,j,foo(i,j));
	return 0;
}

Буде давати такий вивід:

Template foo
foo(0,0)=0
foo(0,1)=1
foo(0,2)=3
foo(1,0)=2
foo(1,1)=4
foo(1,2)=7
foo(2,0)=5
foo(2,1)=8
foo(2,2)=12

Класи[ред.]

Шаблони класів описуються аналогічно до шаблонів функцій. Наприклад шаблон класу для стеку:

#include <stdio.h>

const int SIZE=5;
template <class Type> class Stack
{
	Type st[SIZE];
	int sp;
public:
	Stack()
	{
		sp=0;
	}
	void push(Type a);
	Type pop();
};
template <class Type> void Stack<Type>::push(Type a)
{
	if(sp==SIZE) 
	{
		printf("Переповнення стеку\n");
		return;
	}
	st[sp]=a;
	sp++;

};
template <class Type> Type Stack<Type>::pop()
{
	if(sp==0) 
	{
		printf("Стек порожній\n");
		return 0;
	}
	sp--;
	return st[sp];
}

int main(void)
{
	printf("Введіть \"e\",або \"q\" щоб вийти. \n");
	printf("Введіть \"u\" і ціле число, щоб заштовхнути його в стек.\n");
	printf("Введіть \"p\" щоб витягнути число зі стеку і надрукувати його\n");
	
	Stack<int> s;
	char command=' ';
	while(!((command=='e')||(command=='q')))
	{
		command=getchar();
		switch(command)
		{
			case 'u':
				{
					int tmp;
					scanf("%i",&tmp);
					s.push(tmp);
				}break;
			case 'p':printf("%i\n",s.pop()); 
				 break;
		}
	}
	return 0;
}

Ключове слово typename[ред.]

Добрі люди підказують, що окрім ключового слова class, можна використовувати typename, і вони еквівалентні. Коли Страуструп придумував C++ йому не хотілось використовувати ще одне ключове слово, тому він використав клас. А потім за роботу взявся цілий комітет, і вони вирішили, що це може створити неоднозначності. Тому додали ще і typename.

Правда є випадки, коли typename означає не те, що і клас.

Наприклад, що наступний за ним ідентифікатор є типом:

template < class T>
class MyClass {
    typename T::SubType* ptr;
    ...
};


В даному випадку слово typename означає, що SubType є підтипом класу T, відповідно ptr - показчик на тип T::SubType. Без typename ідентифікатор SubType інтерпретується, як статичний член класу і рядок сприймається, як операція множення значення SubType класу T на значення ptr; Але в даному випадку будь-який тип, який буде підставлений у шаблон, повинен мати внутрішній тип SubType. SubType може бути як класом оголошеним в середині іншого, так і простим typedef-ом.

Розумні вказівники (Smart pointers)[ред.]

В С++ пам’ять, яка виділяється під ресурси, необхідно звільняти. При маніпуляціях з пам’яттю часто виникають програмні помилки:

  • витоки пам’яті;
  • розіменування нульового вказівника, або звернення до неініціалізованї області пам’яті;
  • видалення уже видаленого раніше об’єкту;

Існує техніка управління ресурсами за допомогою локальних об’єктів – RAII. Тобто при отриманні якого-небудь ресурсу, його ініціалізують в конструкторі, а попрацювавши з ним в функції, коректно звільняють в деструкторі. Ресурсом може бути що завгодно, наприклад файл, з’єднання по мережі або блок пам’яті. Приклад:

class VideoBuffer {
	Int *myPixels;
	public:
	
	VideoBuffer() {
		myPixels = new int[640 * 480];
	}
	
	void makeFrame() { /* працюємо з растром */ }

	~VideoBuffer() {
		delete [] myPixels;
	}
};

int game() {
	VideoBuffer screen;
	screen.makeFrame();
}

[1]

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

Інакше довелось би писати так:

int game() {
	int *myPixels = new int[640 * 480]
	//  виконуємо дії
	delete [] myPixels;
}

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

template <typename T>
class smart_pointer {
	T *m_obj;
	public:
	// Віддаємо в управління деякий об’єкт 
	smart_pointer(T *obj) : m_obj(obj)
	{ }
	// По виходу з області видимості цей об’єкт знищується
	~smart_pointer() {
		delete m_obj;
	}
	// Перевантажені оператори
	// Селектор. Дозволяє звертатися до даних типу T за допомогою “стрілочки”
	T* operator->() { return m_obj; }
	// За допомогою такого оператора ми можемо розіменувати вказівник і 	отримати посилання на об’єкт, которий він зберігає
	T& operator* () { return *m_obj; }
};
	
int test() {
	// Віддаємо myClass в управління розумному вказівнику.

	smart_pointer<MyClass> pMyClass(new MyClass(/*параметри*/);    
	// Звертаємось до методу класу MyClass за допомогою селектору
	pMyClass->something();

	// Допустимо, що для нашого класу існує функція виводу його в ostream.
	// Ця функція зазвичай отримує посилання на об’єкт,
	// котрий має вивестись на екран
	std::cout << *pMyClass << std::endl;

	// по виходу із зони видимості об’єкт MyClass буде видалений.
}

Розглянемо конкретні реалізації розумних вказівників у відомих бібліотеках розширення С++:

  • boost::scoped_ptr
  • std::auto_ptr
  • std::tr1::shared_ptr (він же std::shared_ptr в C++11, або boost::shared_ptr із boost)

boost::scoped_ptr[ред.]

Він находиться в бібліотеці boost. Реалізація його проста і зрозуміла, практично ідентична нашій, з наявністю декількох відмінностей. Цей вказівник не можна скопіювати (тобто в нього приватні конструктор копій і оператор присвоювання). Наприклад:

#include <boost/scoped_ptr.hpp>
int test() {
	boost::scoped_ptr<int> p1(new int(6));
	boost::scoped_ptr<int> p2(new int(1));    
	p1 = p2; // Помилка!
}

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

scoped_ptr – шаблонний клас, який зберігає вказівник на динамічно створений об'єкт. (за допомогою виразу new). Об'єкт гарантовано буде видалений, в разі знищення scoped_ptr, або в разі явного знищення.

Приклад використання scoped_ptr:

#include <boost/scoped_ptr.hpp>
#include <iostream>

struct Shoe { ~Shoe() { std::cout << "Buckle my shoe\n"; } };

class MyClass {
    boost::scoped_ptr<int> ptr;
  public:
    MyClass() : ptr(new int) { *ptr = 0; }
    int add_one() { return ++*ptr; }
};

int main()
{
    boost::scoped_ptr<Shoe> x(new Shoe);
    MyClass my_instance;
    std::cout << my_instance.add_one() << '\n';
    std::cout << my_instance.add_one() << '\n';
}

Результат програми: 1 2 Buckle my shoe

std::auto_ptr[ред.]

Трохи більш досконалий варіант попереднього. Для нього реалізований оператор присвоювання і конструктор копій, але праціює він своєрідно:

#include <memory>
int test() {
	std::auto_ptr<MyObject> p1(new MyObject);
	std::auto_ptr<MyObject> p2;    
	p2 = p1;
}

Після виконання операції присвоювання, новий об’єкт MyObject буде знаходитись в p2, а p1 не буде містити нічого. Це так звана семантика переміщення. Оператор копіювання працює таким самим чином. Це потрібно, наприклад, у разі, коли якась функція буде створювати об’єкт і повертати його:

std::auto_ptr<MyObject> giveMeMyObject();

Функція повертає вам тільки що створений об’єкт, але таким чином ви впевненні, що вказівник на цей об’єкт не зберігається ще в якомусь місці. В силу такої поведінки вказівник не можна використовувати в контейнерах STL. Для цих цілей використовують shared_ptr.

std::shared_ptr (С++11)[ред.]

Розумний вказівник з підрахунком посилань. Це означає, що десь існує деяка змінна, яка зберігає кількість вказівників, які посилаються на даний об’єкт. Якщо ця змінна приймає значення 0, то об’єкт знищується. Лічільник збільшується при виклику або оператору копіювання, або оператору присвоювання. Також у вказівника shared реалізовано оператор приведення типів до bool, що дає нам можливість використовувати його в логічних виразах і перевіряти його наявність.

#include <memory>
#include <iostream>
int test() {
	std::shared_ptr<MyObject> p1(new MyObject);
	std::shared_ptr<MyObject> p2;    
	p2 = p1;    
	if (p2)
		std::cout << "Hello, world!\n";
}

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

int test(std::shared_ptr<MyObject> p1) {
	// делаем что-то
}

Якщо ви передасте вказівник за посиланням, то лічильник не буде збільшений, ви маєте бути впевнені, що об’єкт MyObject не буде знищено, доки буде виконуватись функція test.

Недоліки[ред.]

Недоліки використання розумних вказівників:

  • Це робить програму повільнішою, адже потребує виконання додаткових операцій.
  • Це може призвести до сладно читаємого коду, наприклад:
std::tr1::shared_ptr<MyNamespace::Object> ptr = std::tr1::shared_ptr<MyNamespace::Object>(new MyNamespace::Object(param1, param2, param3))

Для того, щоб спростити такі вирази користуються дефайнами, наприклад:

#define sptr(T) std::tr1::shared_ptr<T>
  • Існує проблема циклічних посилань.

Контейнери[ред.]

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

Контейнер для об'єктів різних типів[ред.]

У C/C++ всі масиви є однорідними (тобто всі елементи є одного типу). Тим не менш, створивши додатковий шар логіки, можливо створити структуру, яка поводить себе як різнорідний (гетерогенний) контейнер (з об'єктами різних типів). Існує два випадки:

  • Перший виникає, коли всі об'єкти, які ви хочете зберігати в контейнері публічно успадковані від одного спільного базового класу. Тоді ви можете просто визначити свій контейнер, як набір посилань на базовий клас. Таким чином, в якості елементів, в контейнері будуть зберігатися посилання на різні об'єкти дочірніх класів, а звернення до них буде відбуватися за допомогою вказівників (користуючись поліморфізмом). Якщо необхідно дізнатися тип об'єкту в контейнері, можна використовувати dynamic_cast<> або typeid(). Вам ймовірно доведеться дізнатися про ідіому віртуального конструктора для того, щоб мати можливість копіювати контейнер об'єктів різних типів. Недоліком цього підходу є те, що це робить проблематичним управління пам'яттю (хто буде зберігати вказівник на об'єкти? Якщо видалити ці вказівники на об'єкти при знищенні контейнера, не можна бути впевненим, що хтось вивільнить пам'ять після об'єктів). Це також робить процедуру копіювання контейнеру досить складною.
  • Другий випадок виникає, коли всі об'єкти різнотипні — і вони не мають спільного базового класу. В такому випадку використовують класи-дескриптори, або як він ще називається – непрозорий вказівник (handle class, opaque pointer). Контейнер являтиме собою контейнер об'єктів дескрипторів (за значенням чи за посиланням). Кожен об'єкт знає як «тримати в собі» (іншими словами управляти вказівником) один об'єкт, який ви хочете помістити в колекцію. Ви можете використовувати один клас-дескриптор, з вказівниками на набір різних типів даних, або ієрархію класів, кожен з яких, може зберігати окремий тип з тих що ви бажаєте помістити в колекцію (в контейнері буде знаходитися базовий клас). Недоліком цього методу є те, що кожний раз коли вам треба змінити набір типів, які використовуються в конкретній задачі, вам слід змінювати код, який відповідає за набір типів, доступних для збереження в контейнері. Перевагою є те, що використовуючи класи-дескриптори, ви впорядковуєте всю роботу по управлінню пам'яттю і життєвим циклом об'єктів. [2]

Ключові слова[ред.]

Ключові слова мови програмування С++

Література[ред.]

  1. Герберт Шилдт - Полный справочник C++
  2. Липпман, C++ для начинающих
  3. «С++ for real programmers», Джефф Елджер
  4. cplusplus.com - C++ Language Tutorial, June 2007

Примітки[ред.]

  1. habrahabr.ru - Smart pointers для начинающих
  2. [1] [34.1] [34.4] How can I build a <favorite container> of objects of different types?