З попередніх уроків ми вже знаємо, що значення змінної зберігається у вигляді послідовності біт, а тип змінної вказує компілятору, як інтерпретувати ці біти в відповідні значення.
Конвертація типів
Різні типи даних можуть представляти одне значення по-різному, наприклад, значення 4
типу int і значення 4.0
типу float зберігаються як абсолютно різні двійкові шаблони.
І як ви думаєте, що станеться, якщо зробити наступне:
1 |
float f = 4; // ініціалізація змінної типу з плаваючою крапкою цілим числом 4 |
Тут компілятор не зможе просто скопіювати біти зі значення 4
типу int і перемістити їх в змінну f
типу float. Замість цього йому потрібно буде перетворити ціле число 4
в число типу з плаваючою крапкою, яке потім можна буде присвоїти змінній f
.
Процес конвертації значення з одного типу даних в інший називається конвертацією/приведенням типу. Конвертація типу може виконуватися в наступних випадках:
Присвоювання або ініціалізація змінної значенням іншого типу даних:
1 2 |
double k(4); // ініціалізація змінної типу double цілим числом 4 k = 7; // присвоюємо змінній типу double ціле число 7 |
Передача значення в функцію, де тип параметру — інший:
1 2 3 4 5 |
void doSomething(long l) { } doSomething(4); // передача числа 4 (тип int) в функцію з параметром типу long |
Повернення з функції, де тип значення, що повертається, — інший:
1 2 3 4 |
float doSomething() { return 4.0; // передача значення 4.0 (тип double) з функції, яка повертає float } |
Використання арифметичного оператору з операндами різних типів:
1 |
double division = 5.0 / 4; // операція ділення зі значеннями типів double та int |
У всіх цих випадках (і в багатьох інших) C++ буде використовувати приведення типів.
Є два основних способи конвертації типів:
Неявна конвертація типів, коли компілятор автоматично конвертує один фундаментальний тип даних в інший.
Явна конвертація типів, коли розробник використовує один з операторів конвертації для виконання конвертації об’єкта одного типу даних в інший.
Неявна конвертація типів
Неявна конвертація типу (або ще “автоматичне приведення типу“) виконується всякий раз, коли потрібен один фундаментальний тип даних, але надається інший, і користувач не вказує компілятору, як виконати конвертацію (не використовує явну конвертацію типів через оператори конвертації).
Є два основних типи неявної конвертації типів даних:
числове розширення;
числова конверсія.
Числове розширення
Коли значення з одного типу даних конвертується в інший тип даних, який є більшим (за розміром і по діапазону значень), то це називається числовим розширенням. Наприклад, int може бути розширений в long, а float може бути розширений в double:
1 2 |
long l(65); // розширюємо значення типу int (65) в тип long double d(0.11f); // розширюємо значення типу float (0.11) в тип double |
В C++ є два типи розширень:
Інтегральне розширення (або ще “цілочисельне розширення”). Включає в себе конвертацію цілочисельних типів, менших, ніж int (bool, char, unsigned char, signed char, unsigned short, signed short) в int (якщо це можливо) або в unsigned int.
Розширення типу з плаваючою крапкою. Конвертація з float в double.
Інтегральне розширення і розширення типу з плаваючою крапкою використовуються для перетворення “менших за розміром” типів даних в типи int/unsigned int чи double (вони найбільш ефективні для виконання різних операцій).
Важливо: Числові розширення завжди безпечні і не приводять до втрати даних.
Числові конверсії
Коли ми конвертуємо значення з більшого типу даних в аналогічний, але менший тип даних, або конвертація відбувається між різними типами даних, то це називається числовою конверсією, наприклад:
1 2 |
double d = 4; // конвертуємо 4 (тип int) в double short s = 3; // конвертуємо 3 (тип int) в short |
На відміну від розширень, які є завжди безпечними, конверсії можуть (але не завжди) привести до втрати даних. Тому, в будь-якій програмі, де виконується неявна числова конверсія, компілятор буде видавати попередження.
Є багато правил щодо виконання числової конверсії, але ми розглянемо тільки основні.
У всіх випадках, коли відбувається конвертація значення з одного типу даних в інший, але який не має достатній діапазон для зберігання конвертованого значення, — результати будуть неочікувані. Тому так робити не рекомендується. Наприклад, розглянемо наступну програму:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int i = 30000; char c = i; std::cout << static_cast<int>(c); return 0; } |
У програмі вище ми присвоїли величезне цілочисельне значення типу int змінній типу char (діапазон якого складає від -128 до 127). Це призведе до переповнення і наступного результату:
48
Однак, якщо число підходить за діапазоном, то конвертація пройде успішно. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int i = 3; short s = i; // конвертуємо значення типу int в тип short std::cout << s << std::endl; double d = 0.1234; float f = d; std::cout << f << std::endl; return 0; } |
Тут ми отримаємо очікуваний результат:
3
0.1234
У випадках зі значеннями типу з плаваючою крапкою можуть статися округлення через гіршу точність, яка є в менших типах. Наприклад:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> #include <iomanip> // для std::setprecision() int main() { float f = 0.123456789; // значення типу double - 0.123456789 має 9 значущих цифр, але float може зберігати тільки 7 std::cout << std::setprecision(9) << f; // std::setprecision визначений в заголовковому файлі iomanip return 0; } |
В цьому випадку ми спостерігаємо втрату в точності, так як точність в float менша, ніж в double:
0.123456791
Конвертація з типу int в тип float успішна до тих пір, поки значення підходять по діапазону, наприклад:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int i = 10; float f = i; std::cout << f; return 0; } |
Результат:
10
Аналогічно, конвертація з float в int успішна до тих пір, поки значення підходять по діапазону. Але слід пам’ятати, що будь-який дріб відкидається, наприклад:
1 2 3 4 5 6 7 8 9 |
#include <iostream> int main() { int i = 4.6; std::cout << i; return 0; } |
Дрібна частина значення (.6
) ігнорується, тому результат:
Обробка арифметичних виразів
При обробці виразів компілятор розбиває кожен вираз на окремі підвирази. Арифметичні оператори вимагають, щоб їх операнди були одного типу даних. Щоб це гарантувати, компілятор використовує наступні правила:
Якщо операндом є ціле число менше (за розміром/по діапазону) типу int, то воно піддається інтегральному розширенню в тип int або в unsigned int.
Якщо операнди різних типів даних, то компілятор обчислює операнд з найвищим пріоритетом і неявно конвертує тип іншого операнду в відповідність до типу першого.
Пріоритет типів операндів:
long double (найвищий);
double;
float;
unsigned long long;
long long;
unsigned long;
long;
unsigned int;
int (найнижчий).
Ми можемо використовувати оператор typeid (який знаходиться в заголовку typeinfo), щоб дізнатися вирішальний тип в виразі.
В наступному прикладі у нас є 2 змінні типу short:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> #include <typeinfo> // для typeid() int main() { short x(3); short y(6); std::cout << typeid(x + y).name() << " " << x + y << std::endl; // вираховуємо вирішальний тип даних в виразі x + y return 0; } |
Оскільки значеннями змінних типу short є цілі числа і тип short менше (за розміром/по діапазону) типу int, то він піддається інтегральному розширенню в тип int. Результатом додавання двох int-ів буде тип int:
int 9
Розглянемо інший випадок:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> #include <typeinfo> // для typeid() int main() { double a(3.0); short b(2); std::cout << typeid(a + b).name() << " " << a + b << std::endl; // вираховуємо вирішальний тип даних в виразі a + b return 0; } |
Тут short піддається інтегральному розширенню в int. Однак, int і double як і раніше не збігаються. Оскільки double знаходиться вище в ієрархії типів, то ціле число 2
перетворюється в 2.0
(тип double), і два double-а дорівнюють double:
double 5
З цією ієрархією іноді можуть виникати цікаві ситуації, наприклад:
1 2 3 4 5 6 7 8 |
#include <iostream> int main() { std::cout << 5u - 10; // 5u означає значення 5 типу unsigned int return 0; } |
Очікується, що результатом виразу 5u − 10
буде -5
, оскільки 5 − 10 = -5
. Але результат:
4294967291
Тут значення signed int (10
) піддається розширенню в unsigned int (який має більш високий пріоритет), і вираз обчислюється як unsigned int. А оскільки unsigned — це тільки додатні числа, то відбувається переповнення, і ми маємо, що маємо.
Це одна з тих вагомих причин, чому слід уникати використання типу unsigned int взагалі.