Приведение типов данных
Итак, мы знаем, что в памяти компьютера каждый тип данных хранится по-разному.
Типы данных занимают разное количество памяти и могут распознаваться компьютером совершенно по-разному.
Но всё же зачастую нам нужно преобразовывать данные из одного типа в другой.
Такое преобразование называется приведением типов, или же type casting.
Явное приведенеи типов
Мы можем напрямую попросить компилятор преобразовать значение из одного типа в другой.
C-style casting
Так как C++ является продолжением языка C, в целях совместимости этих двух языков в C++ остаётся много инструментов из C.
Одним из них является C-style casting, или же преобразование типов в стиле C.
Записывается оно следующим образом:
int a = 4;
std::cout << (double)a << std::endl;
внимание!
Использовать C-style casting - плохая практика
Но использовать такое преобразование, кроме того, что это достаточно плохо читается в коде, не рекомендуется по нескольким причинам:
- При таком преобразовании компилятор не будет проверять, насколько такое преобразование безопасно. Поэтому мы можем случайно спровоцировать потерю данных, неопределённое поведение нашей программы или даже аварийное завершение.
- Преобразование в стиле С может даже снимать константность с переменных
Статическое приведение типов - static_cast
Такое приведение уже избавлено от недостатков C-style casting. Его мы и будем использовать для явного приведеня типов.
Записывается оно при помощи static_cast:
int a = 4;
std::cout << static_cast<double>(a) << std::endl;
В угловых скобках указывается тип, к которому мы хотим преобразовать данные, а в круглых - сами преобразуемые данные.
Другие C++-преобразования
В следующих уроках мы рассмотрим подробнее и другие преобразования:
dynamic_castconst_castreinterpret_cast
На данный момент мы разобрали ещё недостаточно материала, так что пока отставим эти темы на потом.
Неявное приведение типов
Также в C++, как и в C, часто работает неявное приведение типов. В таких случаях значение одного типа данных без наших инструкций, по решению компилятора преобразуется в значение другого типа.
Нежелаемое неявное приведение типов в программе может привести к очень опасным последствиям, вплоть до взрыва космических ракет, управляемых программой на C/C++.
Поэтому нужно очень чётко понимать, когда срабатывает неявное приведение типов.
Общий случай
Вообще, все ситуации с неявным приведением можно свести к общему правилу: когда компилятор видит, что один тип данных используется в контексте другого типа данных, он будет пытаться привести второй тип данных к первому (если мы ему это, конечно же, не запретили).
Если же компилятору нужно выбрать, к какому типу нужно привести значение, он всегда будет стараться подобрать тип с наименьшей потерей информации. Конкретный пример посмотрим далее для оператора сложения +.
Операторы
Когда мы используем стандартные операторы, так же стоит понимать, что конкретные операции в C++ определены только для конкретных типов.
Например, сложение в C++ через + определено только для:
- двух целых чисел
- двух вещественных чисел
Соответственно, при вызове следующей операции:
double a = 5.2 + 4;
...оператор + без преобразований не сможет вычислить эту сумму.
Ему придётся либо 5.2 привести к инту, либо 4 привести к float. Что же он сделает?
Конечно же, компилятор видит, что если он приведёт 5.2 к инту, произойдёт потеря информации - мы потеряем дробную часть числа. А если он приведёт 4 к float, ничего не потеряется - поэтому в данному случае произойдёт именно преобразование 4 к float, после чего будет вычислен результат сложения двух вещественных чисел.
Ещё один очень наглядный и базовый пример - это деление.
Как вы думаете, что будет выведено на экран при исполнении этого кода?
std::cout << 5/2 << std::endl;
И 2.5 - это неправильный ответ! На самом деле, на экран выведется число 2. Давайте разберёмся, почему.
У нас есть оператор /, который принимает два аргумента.
В данном случае и левый, и правый операторы - это целые числа. На основе входных данных (два целых числа), оператор считает, что он также должен вернуть целое число. Поэтому, 5 делится на два с учётом того, что результат должен быть целым числом. Из-за этого дробная часть просто выкидывается, и на выходе мы получаем число 2.
Как исправить этот код? Чётко сказать компилятору, какой тип мы хотим получить. Например, задать один из аргументов как число с плавающей точкой:
std::cout << 5.0 / 2 << std::endl;
...или так:
std::cout << static_cast<float>(5)/2 << std::endl;
В данном случае оператор получает одно число с плавающей точкой и одно целое, и на основе этой информации принимает решение возвращать тоже число с плавающей точкой, чтобы избежать потери данных.
Если быть точнее, оператор / определён только для работы либо с двумя интами, либо с двумя вещественными числами. Когда мы задаём одно число как вещественное, компилятор преобразует второе число в вещественное и правильно вычисляет результат (по аналогии с только что рассмотренным примером сложения).
замечание
Если применить static_cast ко всему выражению:
std::cout << static_cast<float>(5/2) << std::endl;
...у нас всё равно выведется 2.
Потому что в данном случае оператор / всё равно примет на вход 2 целых числа
и точно также вернёт значение int, и уже это готовое целое число будет формально
преобразовано к типу float.
Вызов функций
Предположим, у нас есть абстрактная функция для сложения двух чисел с плавающей точкой:
double double_sum(double a, double b)
{
return a + b;
}
...и позже, где-то в коде, нам захотелось вызвать эту функцию для двух целых чисел:
int height_table, height_vase;
// ...
std::cout << double_sum(height_table, height_vase);
В этом случае тоже будет произведено неявное преобразование целых чисел в вещественные.
Инициализация
Когда мы инициализируем переменную через оператор =, тоже может возникнуть неявное преобразование.
Например, следующий код:
int a = 4.0;
В этом случае, при инициализации целого числа вещественное число справа неявно преобразуется в int.
Чтобы избежать неявного преобразования типов при инициализации, можно использовать универсальную инициализацию:
int a {4.0};
Такой код компилятор уже откажется компилировать и резонно укажет нам на различие типа переменной и присваеваемого значения.
Условие if
Когда мы пишем условный оператор if, в круглых скобках мы указываем условие, при котором должно быть выполнено тело if (команды в фигурных скобках).
Это условие, как нетрудно догадаться - это на самом деле какое-то значение типа bool. Если оно равняется true, то тело выполняется, если false, то нет.
И если передать в круглые скобки if какое-то не булевое значение, произойдёт неявное преобразование типа в bool:
int a; std::cin >> a;
if (a)
{
std::cout << "a is null!\n" << std::endl;
}
Здесь a - это целое число. if принимает только булевые значения, поэтому компилятор выполняет неявное преобразование из int в bool.
Вообще, преобразования каких-либо типов в bool работают очень просто: если значение не ноль - результирующий bool будет true, иначе - false.
Так и в нашем примере - если a == 0, то тело выполнется, иначе нет.
Опасное и безопасное приведение типов
Как мы видим, типы в C++ можно преобразовывать и изменять совершенно по всякому. Но стоит понимать, что приведение типов допустимо далеко не во всех ситуациях.
Приведение типов считается безопасным, если оно не может потенциально привести к потере данных.
Например, преобразование int в long int - безопасное, потому что число, хранящееся в инте, со стопроцентной гарантией поместится в long int.
В целом, это правило можно немного расширить: всякое преобразование из типа данных, который занимает меньшие объём памяти, к типу данных, который занимает больший объём памяти, является безопасным. Некоторые примеры:
charвshort int,int,long int,double, ...intвdouble,long long int, ...doubleвlong long int
Если же преобразование типов может привести к потере данных, то такое преобразование называется опасным.
Например, приведение типа int к типу char. Если в int-е хранится число, большее 255, оно просто не поместится в char, и в итоговой переменной мы получим "оборванное значение".
Легко можно это визуализировать, представив себе коробки с песком. Если насыпать в большую коробку чуть-чуть песка - ничего плохого не случится, несмотря на то, что часть коробки будет не занята - это безопасное приведение типов.
Но если мы попытаемся в маленькую коробку засыпать камаз песка - часть песка высыпется из коробки и будет утеряна - это будет небезопасное приведение типов.
Как это выглядит на уровне памяти?
Пусть у нас есть переменная int a, в которой лежит значение 300. В памяти компьютера это будет выглядеть так:
00000000000000000000000100101100
(32 знака, потому что int, обычно, занимает 4 байта).
Дальше мы хотим это значение привести к типу char. Что сделает компьютер? Правильно, он просто обрежет наше число:
000000000000000000000001[00101100]
char занимает 1 байт, поэтому компьютер "обрезает" от числа последние 8 бит.
Но в итоговый char не попала одна единичка из исходного числа, потому что она просто не влезла в границы char-а!
В итоге, мы получим в char значение 44 вместо 300.
Самым наглядным примером небезопасного преобразования типов, которое привело к необратимым последствиям, является крушение ракеты Airane 5.
В этом случае в программе 64-битное вещественное число преобразовывалось к 16-битному целому числу, из-за чего ракета, стоившая сотни миллионов долларов, взорвалась при старте. Подробнее на эту тему можно почитать прекрасную статью с хабра, её я оставлю в источниках.
Мораль
Всегда следите за преобразованием типов в своей программе, используйте статическое приведение типов и не забывайте отслеживать в своей программе неявные (и явные тоже) небезопасные преобразования.