Статическая типизация и преобразования типов

Последнее обновление: 13.02.2023

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

Ряд преобразований компилятор может производить неявно, то есть автоматически. Например:

#include <iostream>

int main()
{
    unsigned int age{25};
    std::cout << "age = " << age << std::endl;
}

Здесь переменная age представляет тип unsigned int и условно хранит возраст. Эта переменная инициализируется числом 25, а все целочисленные литералы без суффиксов по умолчанию представляют тип int (signed int). Но компилятор знает как преобразовать значение 25 к типу unsigned int, и каких-то проблем в данном случае не будет.

Но посмотрим на другой пример:

#include <iostream>

int main()
{
    unsigned int age{-25};
    std::cout << "age = " << age << std::endl;
}

Здесь переменной age уже присваивается число -25 - отрицательное, в то время как тип переменной - unsigned int предполагает лишь использование положительных чисел. И в этом случае мы столкнемся с ошибкой компиляции. Например, вывод компилятора g++:

error: narrowing conversion of '-25' from 'int' to 'unsigned int' [-Wnarrowing]

Примеры неявных преобразований

Рассмотрим, как выполняются некоторые базовые преобразования:

  • Переменной типа bool присваивается значение другого типа. В этом случае переменная получает false, если значение равно 0. Во всех остальных случаях переменная получает true.

    bool a = 1;		// true
    bool b = 0;		// false
    bool c = 'g'; // true
    bool d = 3.4; 	// true
    
  • Числовой или символьной переменной присваивается значение типа bool. В этом случае переменная получает 1, если значение равно true, либо получает 0, если присваиваемое значение равно false.

    int c = true; 		// 1
    double d = false;	// 0
    
  • Целочисленной переменной присваивается дробное число. В этом случае дробная часть после запятой отбрасывается.

    int a = 3.4; 		// 3
    int b = 3.6;		// 3
    
  • Переменной, которая представляет тип с плавающей точкой, присваивается целое число. В этом случае, если целое число содержит больше битов, чем может вместить тип переменной, то часть информации усекается.

    float a = 35005; 				// 35005
    double b = 3500500000033;		// 3.5005e+012
    
  • Переменной беззнакового типа (unsigned) присваивается значение не из его диапазона. В этом случае результатом будет остаток от деления по модулю. Например, тип unsigned char может хранить значения от 0 до 255. Если присвоить ему значение вне этого диапазона, то компилятор присвоит ему остаток от деления по модулю 256 (так как тип unsigned char может хранить 256 значений). Так, при присвоении значения -5 переменная типа unsigned char получит значение 251

    unsigned char a = -5; 			// 251
    unsigned short b = -3500;		// 62036
    unsigned int c = -50000000;		// 4244967296
    
  • Переменной знакового типа (signed) присваивается значение не из его диапазона. В этом случае результат не детерминирован. Программа может выдавать адекватный результат, а может работать некорректно.

Преобразования в арифметических операциях

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

  1. long double

  2. double

  3. float

  4. unsigned long long

  5. long long

  6. unsigned long

  7. long

  8. unsigned int

  9. int

То есть, если в операции участвует число типа float и типа long double, то компилятор автоматически преобразует операнд типа float в тип long double (который в соответствии с вышеуказанным списком имеет более высокий приоритет).

Операнды типов char, signed char, unsigned char, short и unsigned short всегда при операциях преобразуются как минимум в тип int

Например, программист заработал за 8 часовой рабочий день 100,2$, рассчитаем его заработок за час:

#include <iostream>

int main()
{
    double sum {100.2};
    int hours {8};
    double revenuePerHour {sum / hours};
    std::cout << "Revenue per hour = " << revenuePerHour << std::endl;
}

Здесь переменная hours, которая представляет тип int и хранит количество часов, будет преобразована к "более приоритетному" типу double.

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

#include <iostream>

int main()
{
    int n {5};
    unsigned int x {8};
    std::cout << "result = " << n - x << std::endl;	// result = 4294967293
}

Здесь в операции n - x число n будет преобразовываться к более приоритетному типу - unsigned int. Формально эта операция возвращает 5 - 8 = -3. Но в нашем случае оба операнда и соответственно результат представляют тип unsigned int, поэтому в итоге результат равен 4294967293.

Опасные и безопасные преобразования

Те преобразования, при которых не происходит потеря информации, являются безопасными. Как правило, это преобразования от типа с меньшей разрядностью к типу с большей разрядностью. В частности, это следующие цепочки преобразований:

bool -> char -> short -> int -> double -> long double

bool -> char -> short -> int -> long -> long long

unsigned char -> unsigned short -> unsigned int -> unsigned long

float -> double -> long double

Примеры безопасных преобразований:

short a = 'g'; // преобразование из char в short
int b = 10;
double c = b; // преобразование из int в double
float d = 3.4;
double e = d; // преобразование из float в double
double f = 35; // преобразование из int в double

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

unsigned int a = -25;           // 4294967271
unsigned short b = -3500;		// 62036 

В данном случае переменным a и b присваивается значения, которые выходят за пределы диапазона допустимых значений для данных типов.

И в подобных примерах многое зависит от компилятора. В ряде случаев компиляторы при компиляции выдают предупреждение, тем не менее программа может быть успешно скомпилирована. В других случаях компиляторы не выдают никакого предупреждения. Собственно в этом и заключается опасность, что программа успешно компилируется, но тем не менее существует риск потери точности данных. Значение переменной - это всего лишь набор битов в памяти, которые интерпретируются в соответствии с определенным типом. И для разных типов один и тот же набор битов может интерпретироваться по разному. Поэтому важно учитывать диапазоны значений для того или иного типа при присвоении переменной значения.

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

unsigned int a {-25};           // ! Ошибка 
unsigned short b {-3500};		// ! Ошибка 

В этом случае компилятор сгенерирует ошибку, и программа не скомпилируется.

Явные преобразования типов

Для выполнения явных преобразований типов (explicit type conversion) применяется оператор static_cast

static_cast<type>(value)

Данный оператор преобразует значение в круглых скобках - value к типу, который указан в угловых скобках - type. Слово static в названии оператора отражает тот факт, что приведение проверяется статически, то есть во время компиляции.

Применение оператора static_cast указывает компилятору, что мы уверены, что в этом месте надо применить преобразование, поэтому даже при инициализации в фигурных скобках компилятор не сгенерирует ошибку. Например, программист заработал за 8 часовой рабочий день 100,2$, рассчитаем его заработок за час, но в виде значения unsigned int:

#include <iostream>

int main()
{
    double sum {100.2};
    unsigned int hours {8};
    unsigned int revenuePerHour { static_cast<unsigned int>(sum/hours) };  // revenuePerHour = 12
    std::cout << "Revenue per hour = " << revenuePerHour<< std::endl;
}

Здесь выражение static_cast<unsigned int>(sum/hours) вычисляет значение выражения sum/hours (а оно будет представлять тип double), и затем преобразует его в тип unsigned int

Стоит отметить, что раньше во времена динозавров в С++ применялась операция преобразования, унаследованная от языка Си:

(тип) значение

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

#include <iostream>

int main()
{
    double sum {100.2};
    unsigned int hours {8};
    unsigned int revenuePerHour { (unsigned int)sum/hours};		// revenuePerHour = 12
    std::cout << "Revenue per hour = " << revenuePerHour<< std::endl;
}

Результат будет тот же. Однако в современном C++ эту операцию практически вытеснил оператор static_cast.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850