Зауваження. Присвоєння x=x інколи вважають синонімом порожнього оператора. Чи завжди воно безпечне? Для фундаментальних типів — так. Проте з оглядом на майбутнє зауважимо, що у разі програмованих типів обчислення лівобічного значення може призвести до руйнування правобічного (див. підрозділ 4.6.3). 2.3.3. Перетворення типів Тепер звернімося до проблеми, якої за строгої типізації не мало б бути, а саме до перетворення типів. Воно виникає, зокрема, у мішаних арифметичних виразах, що водночас містять об’єкти різних числових типів. Навіть у мовах програмування зі скромним набором числових типів (один цілий і один дійсний тип) виникає проблема обчислення мішаних виразів вигляду k*x, де k — ціле число, а x — дійсне. Оскільки результат має бути дійсний, то доцільно виконати перетворення величини k до дійсного типу та застосувати операцію множення дійсних чисел. До того ж навряд чи можна очікувати, що в наборі команд комп’ютера є арифметичні операції для всіх можливих комбінацій арифметичних типів. Тому для виконання будь-якої операції над числовими даними різних типів доводиться здійснювати потрібні зведення типів. Так, щоб додати коротке ціле число до довгого, його перетворюють на довге. Щоб помножити ціле число на дійсне, його слід перетворити на дійсне. Як виконати додавання короткого та довгого дійсних чисел? Спочатку необхідно зробити з короткого дійсного числа довге, потім визначити тип результату залежно від типу об’єкта, що його зберігатиме. Так у програмі виникають неявні перетворення типів (implicit type conversion), про окремі з яких компілятор може попереджати, водночас залишаючи інші непоміченими. Строга типізація, узагалі кажучи, мала б заборонити використання мішаних виразів і, відповідно, неявних перетворень. Однак традиція перетворень так укоренилися, що типізації довелося поступитися. Із перетвореннями типів ми знову зустрінемося в об’єктному програмуванні. У мові C++ діє концепція розширення типу (promotion), згідно з якою в разі змішування типів простіші перетворюються на складніші, які забезпечують ширший діапазон значень. У разі одночасного використання аргументів різних типів у виразах компілятор, де це потрібно, автоматично перетворює типи. Так, якщо арифметичні операції виконують над інтегральними типами (наприклад, char і short), вони розширюються до цілого типу int; коротші дійсні типи перетворюються на відповідні довші. Якщо йдеться про виконання присвоєння, то обчислений у правій частині результат перед зберіганням набуде типу лівобічного значення value. Окрім неявних можливі також явні перетворення типів (type cast) за допомогою кількох особливих операцій перетворення. Зокрема, мова C++ успадкувала від C дві «старомодні» операції: вирази int(monday) та (int)monday перетворюють константу monday типу week на значення цілого типу. Обернене перетворення — (week)i чи week (i). Зауваження. Перетворення типів у програмах, узагалі кажучи, небажані — передусім тому, що вони не завжди коректні. Хотілося б принаймні мати можливість контролювати всі перетворення, які відбуваються в програмі. При цьому особливо небезпечними вважаються «старомодні» перетворення. Проблема в тому, що вони замасковані: немає іншого ефективного способу виявляти місця, де вони виконуються, окрім як перечитати всі тексти. Власні перетворення мова С++ виконує більш диференційовано за допомогою операцій const_cast, dynamic_cast, reinterpret_cast і static_cast, кожна з яких має спеціальне призначення. Останнє перетворення, наприклад static_cast (monday), діє так, як аналогічне перетворення в мові C. Його статичність означає, що всі потрібні перевірки виконуються на етапі компіляції. Це найнадійніше з перетворень. Призначення інших перетворень розглянемо пізніше. Перевага явного позначення для операцій перетворення полягає в тому, що їх можна легко знайти в програмі, виконавши простий пошук за словом «_cast». 2.4. Указники Програмування роботи з пам’яттю — найважливіша і разом з тим найнебезпечніша з погляду надійності частина C/C++-програм. Якщо до розглянутих раніше сталих і змінних можна застосувати термін пряме адресування, оскільки імена адресують значення безпосередньо (direct addressing), і відповідальність за зв’язок змінної з її адресою несе компілятор, то указники(pointers) роблять це непрямо (indirect addressing), а за коректність адреси відповідає сама програма. Якщо проаналізувати можливі джерела помилок при застосуванні прямої адресації порівняно з непрямою, то побачимо, що до помилок, викликаних неініціалізованими змінними чи указниками, будуть додані помилки обчислення адрес. Володіння технікою програмування указників — своєрідний «вищий пілотаж» у процедурному програмуванні. 2.4.1. Указник як засіб непрямого адресування Щоб указник мав значення, попередньо потрібно надати йому це значення, зв’язавши указник з адресою певного місця пам’яті. Якщо воно невідоме на момент визначення указника, то слід ініціалізувати його нулем. На рис. 2.1 наведено приклад визначення й ініціалізації указника, значення якого — нульова адреса. Говорять, що такий указник на жодне місце в пам’яті не вказує, а тому його значення невизначене. Невизначений указник певною мірою безпечний, оскільки рівність нулеві можна перевірити. Він стає небезпечним (призводить до аварійного завершення програми) у разі спроби звернутися до значення, що міститься за нульовою адресою. Невизначений указник не слід плутати із «засміченим» (теж у певному розумінні невизначеним або, точніше кажучи, визначеним некоректно). Якщо звичайну змінну було визначено без ініціалізації, наприклад так: double х; чого за правилами гарного тону слід уникати, то для неї відведено місце, до якого ще не потрапило змістовне значення: відповідна область пам’яті просто містить «сміття». Маємо коректне лівобічне та невизначене правобічне значення змінної. Так само незаданим або, інакше кажучи, «засміченим» буде значення неініціалізованого указника px. Однак у такому разі воно набагато небезпечніше, тому що може виявитись адресою області пам’яті, не призначеної чи навіть забороненої для використання за допомогою цього указника. Некоректним у такому разі стає лівобічне значення, а питання коректності правобічного за цієї умови вже навіть не виникає: наслідки можуть стати непередбачуваними (рис. 2.2). Указники, що ведуть «у нікуди», називають завислими (dangling pointer): double* px; Зауваження. Розглядаючи техніку роботи з указниками, можна провести аналогію між структурами керування та структурами даних. Як уже було сказано під час вивчення парадигм, структуроване програмування повністю витіснило з практики вживання оператора переходу goto. Так само, як оператор переходу призводить до важко контрольованих передавань керування в програмі, використання указників може спричинити виникнення несистематизованих зв’язків між даними. Тому під час роботи з пам’яттю потрібно скрізь, де це можливо, надавати перевагу безпечнішим рішенням, наприклад відсилкам (підрозділ 2.5), особливо якщо мова йде про передавання параметрів. Проте це виходить за межі можливостей мови C. Тому, дотримуючись її правил, доводиться використовувати звичайні указники, особливо для обміну значеннями параметрів. При цьому варто дотримуватися дисципліни програмування, яка б виключала виникнення некоректних значень указників. Від багатьох помилок убезпечують найпростіші правила: указник потрібно ініціалізувати в момент створення (невизначений указник ініціалізується нулем) і знову робити невизначеним у разі видалення значення, на яке він указував. Далі (у підрозділі 5.5) будуть запропоновані безпечніші рішення, що ґрунтуються на використанні вдосконалених безпечних інтелектуальних указників. Указники, подібно до будь-яких інших змінних, мають значення, але це лише адреси, що вказують на самі дані. У попередньому прикладі указник px містить «сміття», а тому вказує невідомо на що. Важко уявити, до яких наслідків може призвести спроба записати дані за такою адресою. У кращому випадку це спричинить вихід за межі пам’яті, в гіршому — потрапляння до не призначеної для програми області й, можливо, ненавмисного псування життєво важливих даних.