Namespace CounterNameSpace 9 страница

Для того, щоб повторно згенерувати винятки в його обробнику, скористайтеся throw-настановою без вказання типу винятки. У цьому випадку поточний виняток буде передано в зовнішню try/catch-послідовність. Найчастіше причиною для такого виконання настанови throw слугує прагнення дати змогу доступ до одного винятку декільком обробникам. Наприклад, перший обробник винятків повідомлятиме про один аспект винятку, а другий – про інше. Винятки можна повторно згенерувати тільки в catch-блоці (або в будь-якій функції, які викликається з цього блоку). При повторному генеруванні виняток не перехоплюватиметься тією ж catch-настановою. Воно розповсюдиться на найближчу try/catch-послідовність.

Повторне генерування винятку типу char * продемонстровано у наведеному нижче коді програми.

Код програми 18.11. демонстрація повторного генерування винятку типу char *

#include <iostream>// Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

void Xhandler() {

try {

throw "Привіт"; // Генерує винятки типу char *

}

catch(char *) { // Перехоплює винятки типу char *

cout << "Перехоплення винятку у функції Xhandler.\n";

throw; // Повторне генерування винятку типу char *,

// яке буде перехоплене поза функцією Xhandler.

}

}

 

int main()

{

cout << "Початок.\n";

 

try {

Xhandler();

}

catch(char *) {

cout << "Перехоплення винятку у функції main().\n";

}

 

cout << "Кінець програми";

 

getch(); return 0;

}

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

Початок.

Перехоплення винятку у функції Xhandler.

Перехоплення винятку у функції main().

Кінець програми

18.3. Оброблення винятків, згенерованих оператором new

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

Спершу необхідно відзначити, що у цьому розділі описується поведінка оператора new відповідно до стандарту мови C++. Як було відзначено в розд. 9, дії, що виконуються системою при неуспішному використанні оператора new, з моменту винаходу мови C++ змінювалися вже кілька разів. Спочатку оператор new повертав при невдачі значення NULL. Пізніше така поведінка була замінена генеруванням винятків. Окрім того, кілька разів мінялося ім'я цього винятки. Нарешті, було вирішено, що оператор new генеруватиме винятки за замовчуванням, але як альтернативний варіант він може повертати і нульовий покажчик. Отже, оператор new у різний час був реалізований різними способами. І хоча всі сучасні компілятори реалізують оператор new відповідно до стандарту мови C++, компілятори "поважнішого" віку можуть містити відхилення від нього. Якщо наведені тут приклади кодів програм не працюють з Вашим компілятором, то зверніться до документації, що додається до компілятора, і поцікавтеся, як саме він реалізує функціонування оператора new.

Згідно з стандартом мови C++, у випадку неможливості задовольнити запит на виділення пам'яті, потрібної оператором new, генерується виняток типу bad_alloc. Якщо Ваша програма не перехопить його, то вона буде достроково завершена. Хоча така поведінка годиться для коротких прикладів програм, в реальних додатках необхідно перехоплювати цей виняток і розумно обробляти його. Щоб отримати доступ до винятку типу bad_alloc, потрібно залучити до програми заголовок <new>.

Розглянемо приклад використання оператора new, поміщеного в try/catch-блок для вистежування невдалих результатів запиту на виділення пам'яті.

Код програми 18.12. Демонстрація оброблення винятків, що генеруються оператором new

#include <iostream>// Для потокового введення-виведення

#include <new> // Для перевантаження операторів new і delete

using namespace std; // Використання стандартного простору імен

 

int main()

{

int *p, i;

try {

p = new int[32]; // Запит на виділення пам'яті

// для 32-елементного int-масиву

}

catch(bad_alloc ха) {

cout << "Пам'ять не виділена.\n";

return 1;

}

 

for(i=0; i<32; i++) p[i] = i;

for(i=0; i<32; i++) cout << p[i] << " ";

delete [] p; // Звільнення пам'яті

 

getch(); return 0;

}

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

Альтернативна форма оператора new – nothrow

Стандарт мови C++ при невдалій спробі виділення пам'яті замість генерування винятку також дає змогу оператору new повертати значення NULL. Ця форма використання оператора new особливо корисна у процесі компілювання старих програм із застосуванням сучасного С++-компілятора. Цей засіб також дуже корисний при заміні викликів функції malloc() оператором new. Це звичайна практика у процесі перекладу С-коду програми на мову програмування C++. Отже, цей формат оператора new має такий вигляд:

p_var = new(nothrow) тип;

У цьому записі елемент p_var – це покажчик на змінну типу тип. Цей nothrow-формат оператора new працює подібно до оригінальної версії оператора new, яка використовувалася кілька років тому. Оскільки оператор new(nothrow) повертає при невдачі значення NULL, його можна "упровадити" в старий код програми, не вдаючись до оброблення винятків. Проте в нових програмах на C++ все ж таки краще мати справу з винятками.

У наведеному нижче прикладі показано, як використовується альтернативний варіант new(nothrow). Неважко здогадатися, що нижче наведено варіацію на тему попередньої програми.

Код програми 18.13. Демонстрація механізму використання nothrow-версії оператора new

#include <iostream>// Для потокового введення-виведення

#include <new> // Для перевантаження операторів new і delete

using namespace std; // Використання стандартного простору імен

 

int main()

{

int *p, i;

 

p = new(nothrow) int[32]; // Використання nothrow-версії

 

if(!p) {

cout << "Пам'ять не виділена.\n";

return 1;

}

 

for(i=0; i<32; i++) p[i] = i;

for(i=0; i<32; i++) cout << p[i] << " ";

delete [] p; // Звільнення пам'яті

 

getch(); return 0;

}

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

18.4. Перевантаження операторів new і delete

Оскільки new і delete – оператори, то їх також можна перевантажувати. Хоча перевантаження операторів ми розглядали в розд. 14, тема перевантаження операторів new і delete була відкладена до знайомства з темою оброблення винятків, оскільки правильно перевантажена версія оператора new(та, яка відповідає стандарту мови C++) повинна у разі невдачі генерувати виняток типу bad_alloc. З ряду причин Вам варто створити власну версію оператора new. Наприклад, створіть процедури виділення пам'яті, які, якщо область купи виявиться вичерпаною, автоматично починають використовувати дисковий файл як віртуальну пам'ять. У будь-якому випадку реалізація перевантаження цих операторів є не складнішою за перевантаження будь-яких інших.

Нижче наведено скелет функцій, які перевантажують оператори new і delete.

// Виділення пам'яті для об'єкта

void *operator new(size_t size)

{

/* У разі неможливості виділити пам'ять генерується виняток

типу bad_alloc. Конструктор викликається автоматично. */

 

return pointer_to_memory;

}

 

// Видалення об'єкта.

void operator delete(void *p)

{

/* Звільняється пам'ять, яка адресується покажчиком p.

Деструктор викликається автоматично. */

}

Тип size_t спеціально визначено, щоб забезпечити зберігання розміру максимально можливої області пам'яті, яка може бути виділена для об'єкта[70]. Параметр size визначає кількість байтів пам'яті, що є необхідною для зберігання об'єкта, для якого виділяється пам'ять. Іншими словами, це об'єм пам'яті, як повинна виділити Ваша версія оператора new. Операторна функція new повинна повертати покажчик на пам'ять, що виділяється нею, або генерувати винятки типу bad_alloc у випадку виникнення помилки. Окрім цих обмежень, операторна функція new може виконувати будь-які потрібні дії. Під час виділення пам'яті для об'єкта за допомогою оператора new(його початкової форми або Вашої власної) автоматично викликається конструктор об'єкта.

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

Щоб виділити пам'ять для масиву об'єктів, а потім звільнити її, необхідно використовувати наступні формати операторів new і delete.

// Виділення пам'яті для масиву об'єктів

void *operator new[](size_t size)

{

/* У разі неможливості виділити пам'ять генерується виняток

типу bad_alloc. Кожен конструктор викликається автоматично. */

return pointer_to_memory;

}

 

// Видалення масиву об'єктів.

void operator delete[](void *p)

{

/* Звільняється пам'ять, яка адресується покажчиком p.

При цьому автоматично викликається деструктор для

кожного елемента масиву. */

}

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

Оператори new і delete, як правило, перевантажуються відносно класу. Заради простоти у наведеному нижче прикладі використовується не нова схема розподілу пам'яті, а перевантажені функції new і delete, які просто викликають С-орієнтовані функції виділення пам'яті malloc() і free(). У своєму власному додатку Ви можете реалізувати будь-який метод виділення пам'яті.

Щоб перевантажити оператори new і delete для конкретного класу, достатньо зробити ці перевантажені операторні функції членами цього класу. У наведеному нижче прикладі коду програми оператори new і delete перевантажуються для класу kooClass. Це перевантаження дає змогу виділити пам'ять для об'єктів і масивів об'єктів, а потім звільнити її.

Код програми 18.14. Демонстрація перевантажених операторів new і delete

#include <iostream>// Для потокового введення-виведення

#include <new> // Для перевантаження операторів new і delete

#include <cstdlib> // Для використання бібліотечних функцій

using namespace std; // Використання стандартного простору імен

 

class kooClass { // Оголошення класового типу

int x, y, z; // Тривимірні координати

public:

kooClass() { x = y = z = 0; cout << "Створення об'єкта 0, 0, 0\n";}

kooClass(int izm, int jzm, int kzm) { x = izm; y = jzm; z = kzm;

cout << "Створення об'єкта " << izm << ", ";

cout << jzm << ", " << kzm << "\n";}

~kooClass() { cout << "Руйнування об'єкта\n";}

void *operator new(size_t size);

void *operator new[](size_t size);

void operator delete(void *p);

void operator delete[](void *p);

void showBase();

};

 

// Перевантаження оператора new для класу kooClass.

void *kooClass::operator new(size_t size)

{

void *p;

cout << "Виділення пам'яті для об'єкта класу kooClass.\n";

p = malloc(size);

 

// Генерування винятку у разі невдалого виділення пам'яті.

if(!p) {

bad_alloc ba;

throw ba;

}

 

return p;

}

 

// Перевантаження оператора new для масиву об'єктів типу kooClass.

void *kooClass::operator new[](size_t size)

{

void *p;

 

cout << "Виділення пам'яті для масиву kooClass-oб'єктів." << "\n";

// Генерування винятку при невдачі.

p = malloc(size);

if(!p) {

bad_allос ba;

throw ba;

}

 

return p;

}

 

// Перевантаження оператора delete для класу kooClass.

void kooClass::operator delete(void *p)

{

cout << "Видалення об'єкта класу kooClass.\n";

free(p);

}

 

// Перевантаження оператора delete для масиву об'єктів типу kooClass.

void kooClass::operator delete[](void *p)

{

cout << "Видалення масиву об'єктів типу kooClass.\n";

free(p);

}

 

// Відображення тривимірних координат x, y, z.

void kooClass::showBase()

{

cout << "Координати об'єкта <" << s << ">:\n";

cout << "\tx= " << x << ", y= " << y << ", z= " << z << "\n";

}

 

int main()

{

kooClass *p1, *p2;

 

try {

p1 = new kooClass[3]; // Виділення пам'яті для масиву

р2 = new kooClass(5, 6, 7); // Виділення пам'яті для об'єкта

}

catch(bad_alloc ba) {

cout << "Помилка під час виділення пам'яті.\n";

return 1;

}

 

p1[1].showBase();

p2->showBase();

 

delete [] p1; // Видалення масиву

delete p2; // Видалення об'єкта

 

getch(); return 0;

}

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

Виділення пам'яті для масиву kooClass-oб'єктів.

Створення об'єкта 0, 0, 0

Створення об'єкта 0, 0, 0

Створення об'єкта 0, 0, 0

Виділення пам'яті для об'єкта класу kooClass.

Створення об'єкта 5, 6, 7

0, 0, 0

5, 6, 7

Руйнування об'єкта

Руйнування об'єкта

Руйнування об'єкта

Видалення масиву об'єктів типу kooClass.

Руйнування об'єкта

Видалення об'єкта класу kooClass.

Перші три повідомлення Створення об'єкта 0, 0, 0 видані конструктором класу kooClass (який не має параметрів) під час виділення пам'яті для триелементного масиву. Як ми вже зазначали вище, під час виділення пам'яті для масиву автоматично викликається конструктор кожного елемента. Повідомлення Створення об'єкта 5, 6, 7 видано конструктором класу kooClass (який приймає три аргументи) під час виділення пам'яті для одного об'єкта. Перші три повідомлення Руйнування об'єкта видані деструктором в результаті видалення триелементного масиву, оскільки при цьому автоматично викликався деструктор кожного елемента масиву. Останнє повідомлення Руйнування об'єкта видане під час видалення одного об'єкта класу kooClass. Важливо розуміти, що, коли оператори new і delete перевантажені для конкретного класу, то в результаті їх використання для даних інших типів будуть задіяні оригінальні версії операторів new і delete. Це означає, що при додаванні у функцію main() наступного рядка буде виконана стандартна версія оператора new:

int *f = new int; // Використовується стандартна версія оператора new.

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

Перевантаження nothrow-версії оператора new

Можна також створити перевантажені nothrow-версії операторів new і delete. Для цього використовуйте такі схеми:

// Перевантаження nothrow-версії оператора new

void *operator new(size_t size, const nothrow_t &n)

{

// Виділення пам'яті.

if(success) return pointer_to_memory;

else return 0;

}

 

// Перевантаження nothrow-версії оператора new для масиву.

void *operator new[](size_t size, const nothrow_t &n)

{

// Виділення пам'яті.

if(success) return pointer_to_memory;

else return 0;

}

 

// Перевантаження nothrow-версії оператора delete.

void operator delete(void *p, const nothrow_t &n)

{

// Звільнення пам'яті.

}

 

// Перевантаження nothrow-версії оператора delete для масиву.

void operator delete[](void *p, const nothrow_t &n)

{

// Звільнення пам'яті.

}

Тип nothrow_t визначається в заголовку <new>. Параметр типу nothrow_t не використовується. Як вправу проекспериментуйте з throw-версіями операторів new і delete самостійно.


Розділ 19. С++-система введення-виведення потокової інформації

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

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

19.1. Порівняння C- та С++-систем введення-виведення

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

З погляду програміста, є дві істотні відмінності між старою С- і новою С++-бібліотеками введення-виведення. По-перше, нова бібліотека містить ряд додаткових засобів і визначає декілька нових типів даних. Тому нову бібліотеку С++-системи введення-виведення можна вважати надбудовою над старою С-системою. Практично всі програми, що були написані раніше з використанням старої бібліотеки, успішно компілюються тепер за наявності нової бібліотеки, не вимагаючи внесення будь-яких значних змін у самій програмі. По-друге, стара бібліотека С-сис­теми введення-виведення була визначена в глобальному просторі імен, а нова використовує простір імен std[71]. Оскільки С-бібліотека введення-виведення даних вже дещо застаріла, то у цьому навчальному посібнику описується тільки нова її версія. Водночас велика частина наведеного нижче матеріалу повною мірою стосується і старої бібліотеки.

19.2. Потоки у мові програмування C++

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

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

19.2.1. Файлові С++-потоки

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

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

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

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

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

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

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

19.2.2. Вбудовані С++-потоки

У мові програмування C++ міститься ряд вбудованих однобайтових (8-бітових) потоків (cin, cout, cerr і clog), які автоматично відкриваються, як тільки програма починає виконуватися. Як уже зазначалося вище, cin – це стандартний вхідний, а cout – стандартний вихідний потік. Потоки cerr і clog (вони призначені для виведення інформації про помилки) також пов'язані із стандартним виведенням даних. Різниця між ними полягає у тому, що потік clog є буферизованим, а потік cerr – ні. Це означає, що будь-які вихідні дані, послані у потік cerr, будуть негайно виведені на екран, а під час використання потоку clog дані спочатку записуються в буфер, а реальне їх виведення починається тільки тоді, коли буфер повністю заповнено. Зазвичай потоки cerr і clog використовуються для виведення інформації на екран про стан відлагодження програми або її помилки.