8.3 – Числовые преобразования

Добавлено 20 июня 2021 в 02:41

В предыдущем уроке (8.2 – Продвижение целочисленных типов и типов с плавающей запятой) мы рассмотрели числовые продвижения, которые представляют собой преобразование определенных более узких числовых типов в более широкие числовые типы (обычно int или double), которые можно эффективно обрабатывать.

C++ поддерживает другую категорию преобразований числовых типов, называемую числовыми преобразованиями, которая охватывает дополнительные преобразования типов, не подпадающие под действие правил числового продвижения.

Ключевые выводы


Любое преобразование типа, охватываемое правилами числового продвижения (8.2 – Продвижение целочисленных типов и типов с плавающей запятой), является числовым продвижением, а не числовым преобразованием.

Существует пять основных типов числовых преобразований.

  1. Преобразование одного целочисленного типа в любой другой целочисленный тип (за исключением целочисленных продвижений):
    short s = 3; // конвертируем int в short
    long l = 3;  // конвертируем int в long
    char ch = s; // конвертируем short в char
  2. Преобразование типа с плавающей запятой в любой другой тип с плавающей запятой (за исключением продвижений с плавающей запятой):
    float f = 3.0;        // конвертируем double в float
    long double ld = 3.0; // конвертируем double в long double
  3. Преобразование типа с плавающей запятой в любой целочисленный тип:
    int i = 3.5; // конвертируем double в int
  4. Преобразование целочисленного типа в любой тип с плавающей запятой:
    double d = 3; // конвертируем int в double
  5. Преобразование целочисленного типа или типа с плавающей запятой в bool:
    bool b1 = 3;   // конвертируем int в bool
    bool b2 = 3.0; // конвертируем double в bool

В качестве отступления...


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

Сужающие преобразования

В отличие от числового продвижения (которое всегда безопасно), числовое преобразование может (или не может) привести к потере данных или точности.

Некоторые числовые преобразования всегда безопасны (например, int в long или int в double). Другие числовые преобразования, такие как double в int, могут привести к потере данных (в зависимости от конкретного преобразуемого значения и/или диапазона базовых типов):

int i1 = 3.5; // 0.5 отбрасывается, что приводит к потере данных
int i2 = 3.0; // ok, будет преобразовано в значение 3, поэтому данные не будут потеряны

В C++ сужающее преобразование – это числовое преобразование, которое может привести к потере данных. К таким сужающим преобразованиям относятся:

  • из типа с плавающей запятой в целочисленный тип;
  • из более широкого типа с плавающей запятой в более узкий тип с плавающей запятой, если преобразовываемое значение не constexpr и не находится в диапазоне целевого типа (даже если оно не может быть представлено точно);
  • из целочисленного типа в тип с плавающей запятой, если только преобразуемое значение не constexpr и не находится в диапазоне целевого типа и не может быть преобразовано обратно в исходный тип без потери данных;
  • из более широкого целочисленного типа в более узкий целочисленный тип, если преобразовываемое значение не constexpr и после целочиисленного продвижения не будет соответствовать целевому типу;

Хорошая новость в том, что вам не нужно об этом помнить. Ваш компилятор обычно выдает предупреждение (или ошибку), когда он определяет, что требуется неявное сужающее преобразование.

Предупреждение

Компиляторы часто не предупреждают при преобразовании signed int в unsigned int или наоборот, даже если это сужающие преобразования. Будьте особенно осторожны с непреднамеренными преобразованиями между этими типами (особенно при передаче аргумента функции, принимающей параметр противоположного знака).

Например, при компиляции следующей программы:

int main()
{
    int i = 3.5;
}

Visual Studio выдает следующее предупреждение:

warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data

В общем, следует избегать сужающих преобразований, но есть ситуативные случаи, когда вам может потребоваться это сделать. В таких случаях вы должны сделать неявное сужающее преобразование явным, используя static_cast. Например:

void someFcn(int i)
{
}
 
int main()
{
    double d{ 5.0 };
    
    // плохо: сгенерирует предупреждение компилятора о сужающем преобразовании
    someFcn(d); 
    // хорошо: мы явно сообщаем компилятору, что это сужающее преобразование
    // ожидается, предупреждения не генерируются
    someFcn(static_cast<int>(d));
    
    return 0;
}

Лучшая практика

По возможности избегайте сужающего преобразования. Если вам действительно нужно выполнить его, используйте static_cast, чтобы сделать его явным преобразованием.

Инициализация с фигурными скобками запрещает сужающие преобразования

Сужающие преобразования строго запрещены при использовании инициализации с фигурными скобками (что является одной из основных причин, по которой эта форма инициализации предпочтительна):

int main()
{
    int i { 3.5 }; // не компилируется
}

Visual Studio выдает следующую ошибку:

error C2397: conversion from 'double' to 'int' requires a narrowing conversion

Еще о числовых преобразованиях

Конкретные правила преобразования чисел сложны и многочисленны, поэтому следует помнить о наиболее важных вещах.

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

int main()
{
    int i{ 30000 };
    char c = i; // chars имеют диапазон от -128 до 127
 
    std::cout << static_cast<int>(c);
 
    return 0;
}

В этом примере мы присвоили большое целое число переменной с типом char (диапазон значений от -128 до 127). Это вызывает переполнение char и приводит к неожиданному результату:

48

Преобразование большего целочисленного типа или типа с плавающей запятой в меньший тип из того же семейства обычно будет работать, пока значение попадает в диапазон меньшего типа. Например:

int i{ 2 };
short s = i; // конвертируем из int в short
std::cout << s << '\n';
 
double d{ 0.1234 };
float f = d;
std::cout << f << '\n';

Это дает ожидаемый результат:

2
0.1234

В случае значений с плавающей запятой может произойти некоторое округление из-за потери точности в меньшем типе. Например:

// значение doube 0.123456789 имеет 9 значащих цифр,
// но float может поддерживать только около 7
float f = 0.123456789;

// std::setprecision определена в заголовке iomanip
std::cout << std::setprecision(9) << f << '\n';

В этом случае мы видим потерю точности, потому что float не может содержать такой же точности, как double:

0.123456791

Преобразование из целого числа в число с плавающей запятой обычно работает до тех пор, пока значение попадает в диапазон типа с плавающей запятой. Например:

int i{ 10 };
float f = i;
std::cout << f;

Это дает ожидаемый результат:

10

Преобразование из числа с плавающей запятой в целое число работает до тех пор, пока значение попадает в диапазон целого числа, но любые дробные части теряются. Например:

int i = 3.5;
std::cout << i << '\n';

В этом примере дробная часть (.5) теряется, в итоге остается следующий результат:

3

Хотя правила числового преобразования могут показаться пугающими, на самом деле компилятор обычно предупреждает вас, если вы попытаетесь сделать что-то опасное (за исключением некоторых преобразований со знаком / без знака).

Теги

C++ / CppLearnCppДля начинающихНеявное преобразование типаОбучениеПриведение типовПрограммированиеСужающее преобразование типа

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.