§5 Действительный тип. Переполнение типа. Точность вычислений. Преобразования типов

Действительные типы

Действительные (вещественные) типы или типы с плавающей точкой представлены типами одинарной, двойной и расширенной точности: float, double и long double соответственно:

Types        Size                           MIN                           MAX
float           4                 1.1754944e-38                 3.4028235e+38
double          8        2.225073858507201e-308        1.797693134862316e+308
long double    12   3.3621031431120935063e-4932    1.189731495357231765e+4932

Вещественные типы могут быть представлены следующим литералами:

double mu = 1.91315;	 	 
double me = .5110034;	 	 
double d = 2.;	 	 
double c = 3E8;	 	 
double G = 6.6720e-11;	 	 

(Для этого послужили значения некоторых известных физических констант).
Как видно из примера поддерживаются две формы записи: с фиксированной точкой (1, 2 и третья строка) и экспоненциальная (иначе научный формат или число с плавающей точкой). Обратите внимание на то, что для обеих форм нулевую дробную или целую часть числа (для экспоненциальной формы – в мантиссе) можно опустить (но не обе одновременно!). Также можно опустить и саму точку, оставив лишь целую часть числа. Но для улучшения представления кода точку всё же опускать не следует. Для представления отрицательной вещественной константы используется оператор “унарный минус”.
Типом по умолчанию для вещественных типов является тип double. Для явного указания типа в выражениях следует использовать суффиксы (см. п. 3), но только для литералов с фиксированной точкой.
Представление вещественных чисел существенно отличается от представления целых чисел. Вещественное число представлено в памяти в виде двух частей: мантиссы и порядка. Мантисса – это число >= 1, но < 2, поэтому старшая цифра мантиссы, равная 1, никогда не хранится. Например, для хранения числа типа float выделяется 32 бита, из них: 1 разряд выделяется для хранения знака мантиссы, 8 разрядов для хранения порядка числа и 23 разряда для хранения мантиссы.

Точность вычислений

Размеры мантиссы (дробной части) чисел действительных типов float, double и long double, в количестве десятичных разрядов, соответственно равны: 7, 15 и 19. Это предел точности этих типов, сверх которой будет накапливаться ошибка. Отсюда следует, что наибольшей точностью вычислений обладает тип long double.
Науке известны как слишком большие величины, так и слишком маленькие: объем Метагалактики  3,5\cdot10^{30} m^3 , радиус протона  0,8768\cdot10^{-15} m , масса электрона  9.10938291\cdot10^{-31} kg , масса Солнца 1.98892\cdot10^{30} kg , а Галактики, и того больше – 6\cdot10^{42} kg! Очевидно, что производя вычисления с подобными числами мы будем терять точность вычислений, так как в мантиссе представлено лишь небольшое число десятичных разрядов по сравнению с порядком.
Если необходимо соблюсти точность вычислений при работе с целыми числами, то не нужно использовать для этих целей любой из вещественных типов! Необходимо выбирать подходящий по размеру целый тип. Если же размер числа больше любого целого типа, то прибегать к программированию больших чисел с помощью алгоритмов длинной арифметики с массивами.
Заметим, что для точных вычислений с большими числами (как с целыми, так и с действительными) для языка С++ существуют несколько свободных библиотек (например GMP и MPIR). Но лучшим выбором будет использование библиотеки Boost.Multiprecision. Эта библиотека входит в набор известных библиотек написанных для языка C++ в рамках проекта Boost (а разработчики Boost, как известно, активно сотрудничают с комитетом по разработке C++), поэтому она хорошо интегрирована в систему и может работать с фундаментальными типами, подчиняясь, при этом, всем правилам языка. Для ускорения работы предусмотрен механизм определения длины контейнера для хранения “длинного” числа до их использования. Если выделенная память будет исчерпана, то увеличение произойдет автоматически. Поскольку это может привести к потере производительности о длине объекта нужно побеспокоиться заранее. Впрочем, работу с длинными числами можно полностью возложить на автоматику, если низкая производительность программы не критична.
Приведем небольшой пример для иллюстрации.

Программа 9.5.1
#include <boost/multiprecision/cpp_int.hpp>	 	 
#include <iostream>
using namespace boost::multiprecision;	 	 
using namespace std;	 	 
int main() {	 	 
	uint256_t num1, num2; // max 115792089237316195423570985008687907853269984665640564039457584007913129639935	 	 
	cout << "num1 = "; cin >> num1;	 	 
	cout << "num2 = "; cin >> num2;	 	 
	cout << "num1 + num2 = " << num1 + num2 << endl;	 	 
 return 0;	 	 
}	 	 
num1 = 555555555555555555555555555555555555555555555	 	 
num2 = 555555555555555555555555555555555555555555555	 	 
num1 + num2 = 1111111111111111111111111111111111111111111110	 	 

Библиотека математических функций cmath

В стандартной библиотеке C++ имеется большой набор математических функций, которые становятся доступными, если в программу включить заголовочный файл cmath. Некоторые часто используемые функции перечислены в методичке “Язык программирования C++. Краткий справочник” (Таб. 10). Обратите внимание, что функции, принимающие или возвращающие угол (тригонометрические), работают с радианами (напомним, что  \alpha[rad] = \alpha[\textdegree]\times(\pi/180) ).
Составим программу использующую функции библиотеки cmath.
В треугольнике известны три стороны a, b и c; найти (в градусах) углы этого треугольника, используя формулы:
 cos(A)=\frac{b^2+c^2-a^2}{2 a b},\qquad sin(B)=\frac{b sin(A)}{a},\qquad C=180\textdegree-(A+B)

Примечание: тригонометрические функции возвращают значение угла в радианах. Формула для перевода рад <-> град:
 \alpha[\textdegree] = \alpha[rad]\times(180/\pi)
Программа 9.5.2
//==============================================================//
// Нахождение углов треугольника по трем сторонам               //
//==============================================================//
#include <iostream>
#include <cmath>
using namespace std;

int main() {
	double a, b, c, A, B, C;
	const double RadToGre = 180 / M_PI;

	cout << "a = "; cin >> a;
	cout << "b = "; cin >> b;
	cout << "c = "; cin >> c;

	A = acos((b * b + c * c - a* a) / (2 * b * c));
	B = asin(b * sin(A) / a);
	C = 180 - (A + B) * RadToGre;
	A *= RadToGre;
	B *= RadToGre;

	cout << "A = " << A << endl;
	cout << "B = " << B << endl;
	cout << "C = " << C << endl;

	return 0;
}
a = 2.3
b = 5
c = 4.7
A = 27.2069
B = 83.6799
C = 69.1133

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

Константы

Если перед именем переменной используется ключевое слово const – то это означает, что описана константа. Const называется квалификатором (qualifier), поскольку он уточняет, что означает это объявление. Константа инициализируется значением в момент описания. Попытка присвоить ей новое значение приведет к ошибке. Существует практика давать имена константам с заглавной буквы. Квалификатор const принято использовать до спецификатора типа:

[квалификатор const][спецификатор типа][Name_const] = [значение];

Например:

const int Buff = 255;
const double Alpha = 0.0072973506;

Разумеется, другие способы инициализации, перечисленные выше для переменных, будут справедливы и для констант. Однако, в отличие от переменных, константы не должны участвовать в выражениях, которые потенциально могут привести к их изменению.

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

С++ дает возможность преобразовать объекты одного типа в объекты другого типа. Различают два вида преобразования:

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

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

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

К неявным преобразованиям (implicit conversion) относят:

  1. Преобразования при инициализации и присваивании
  2. Преобразования при передаче аргументов
  3. Арифметические преобразования (arithmetic conversion)
Преобразования при инициализации и присваивании

Правила преобразований:

  • Если перемененной целочисленного типа с большим диапазоном присваивается значение типа с меньшим, то значение не будет изменено. Аналогично для действительных типов.
  • Если переменной float присваивается тип long, то значение усекается, а точность теряется.
  • Если переменной int присваивается значение переменной double, то результат: либо усечение дробной части, либо неопределенный.
  • Если целой переменной знакового типа присваивается значение не из диапазона результат неопределен.
  • Если переменной беззнакового типа присваивается значение не из диапазона, то результатом будет остаток от деления по модулю значения, которое может содержать тип назначения.

Отсюда вытекает правило:

  1. чтобы получить предсказуемый результат (без потери точности) необходимо типу с большим диапазоном присваивать тип с меньшим диапазоном, с учетом знака;
  2. избегать в присваиваниях знаковому типу значение беззнакового типа и наоборот.
Программа 9.5.3
#include <iostream>
using namespace std;

int main() {
	cout << "----------------------------------------------\n";
	unsigned short a;
	a = -100000; cout << a << endl;
	a =  70000;  cout << a << endl; // 70000 % 65536;
	cout << "----------------------------------------------\n";
	int b;
	b = 12.123;	 cout << b << endl; // отсекается дробная часть
	b = -0.5e+6; cout << b << endl; // Ok!
	b = -1e+26;  cout << b << endl; // min_int -2147483648
	b = 6e15;    cout << b << endl; // max_int 2147483647
	cout << "----------------------------------------------\n";
	unsigned c;
	c = -2e-20;  cout << c << endl; // 0!
	c = 18446744073709551615; cout << c << endl; // max_unsigned
	c = '\x07\x07'; cout << c << endl; // Ок! (707 - 16-ричное)
	cout << "----------------------------------------------\n";
	float d;
	d = 18446744073709551615; cout << d << endl; // потеря точности
	d = 1.7976931348623e+308; cout << d << endl; // бесконечность
	d = 0B11110001111; cout << d << endl; // Ok! (1935 - двоичное)
	cout << "----------------------------------------------\n";
	return 0;
}

Вывод программы:

----------------------------------------------
31072
4464
----------------------------------------------
12
-500000
-2147483648
2147483647
----------------------------------------------
0
4294967295
1799
----------------------------------------------
1.84467e+19
inf
1935
----------------------------------------------
Арифметические преобразования

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

  • В выражениях с младшими целочисленными типами (bool, char и short) итоговый результат преобразуется в int, если результат соответствует диапазону int, иначе преобразуется в unsigned int;
  • если один из операндов имеет тип float, double или long double, то и другой операнд преобразуется в тип float, double или long double, соответственно;
  • если оба операнда имеют одинаковый знак (то есть: либо знакового, либо беззнакового типа), то операнд с младшим типом преобразуется к старшему типу;
  • если операнды в выражении и знакового, и беззнакового типа:
    • если беззнаковый тип операнда старше, чем операнд со знаком, то последний преобразуется в тип беззнакового операнда;
    • иначе, если тип со знаком может представить весь диапазон беззнакового типа, беззнаковый операнд преобразуется к типу операнда со знаком;
    • иначе операнды преобразуются в беззнаковую версию типа со знаком

Проиллюстрируем на примере, что int + unsigned => long:

Программа 9.5.4
#include <iostream>	 	 
using namespace std;	 	 
int main() {	 	 
	int d = 2147483647;	 	 
	unsigned e = 4294967295;	 	 
	cout << d + e << endl;	 	 
	cout << long(d) + e << endl;	 	 
	return 0;	 	 
}	 	 

Результат:

2147483646	 	 
6442450942	 	 

Программа должна сложить два числа, однако значения чисел являются граничными для соответствующих типов (int и unsigned int). В результате в первой строке ответ неверный! Во второй строке предусмотрен такой исход. Создаётся копия переменной, которая приводится к типу, который имеет большую ширину, то есть long (способного вместить все возможные значения этих типов). Такое преобразование типов называется явным. Результат – правильный, но обратите внимание, что программа продолжила работу как ни в чем не бывало! (Т. е. программа не посчитала это за ошибку).
Объясняется такой результат способом представления целых чисел со знаком и без знака в памяти компьютера. На схемах ниже показано как будут изменяться значения для переменных обоих типов с одинаковой шириной (int) после переполнения:
int-sig
int-uns
Существуют несколько способов контроля переполнения не являющихся частью стандарта. В реализации для GCC имеются специальные функции осуществляющие контроль переполнения на этапе выполнения операций сложения, вычитания и умножения для всех целых базовых типов.
Приведем пример программы для операции сложения.

Программа 9.5.5
#include <iostream>
#include <limits>
using namespace std;

int main() {
	int x = numeric_limits<int>::max() - 10;
	for (int i = 0; i < 10; i++) {
		int res;
		if (__builtin_add_overflow(x, i, &res)) {
			cout << "Произошло переполнение!" << endl;
		} else {
			x = res;
			cout << x << endl;
		}
	}
	return 0;
}
2147483637
2147483638
2147483640
2147483643
2147483647
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!

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

Программа 9.5.6
#include <iostream>	 	 
using namespace std;	 	 
int main() {	 	 
	int a = 2000;	 	 
	long b = 100;	 	 
	float c = 7.0;	 	 
	double d = 4.5;	 	 
	cout << sizeof(b + a) << " -> " << b + a << endl;	 	 
	cout << sizeof(c * d) << " -> " << c * d << endl;	 	 
	cout << sizeof(a / int(d)) << " -> " << a / int(d) << endl;	 	 
	cout << sizeof(b / c) << " -> " << b / c << endl;	 	 
	return 0;	 	 
}	 	 

Результат работы программы:

8 -> 2100	 	 
8 -> 31.5	 	 
4 -> 500	 	 
4 -> 14.2857	 	 

Для того, чтобы определить размер выводимого типа в программе 9.5.6 используется операция sizeof. Эта операция возвращает размер в байтах любого объекта.

Явное преобразование

В C++ для явного преобразования типов используется приведение типов (cast) в старом стиле и с помощью операции static_cast.

Приведение типов в старом стиле

Приведение типов в старом стиле использует две формы:

  1. type(expression) – в стиле C++
  2. (type)expression – в стиле C

где type — тип к которому приводится выражение expression.
И та, и другая форма является устаревшей и небезопасной. Она существует до сих пор по причине совместимости с языком С. В С++ рекомендуется использовать новый формат преобразования типов.

Операция static_cast

Операция static_cast является рекомендуемой формой явного приведения типов в C++. Она позволяет предупреждать некорректную попытку преобразования типа. Формат операции таков:

static_cast<type>(expression)

Например, в программе 5.4 строку с преобразованием можно было бы заменить следующей строкой:

cout << static_cast<long>(d) + e << endl;

Спецификатор auto

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

auto c = 2 * 3.14 / 3, d = 0.15 * с;

Применять auto для описания переменных, тип которых и так очевиден - не имеет смысла, но может сделать код более лаконичным и изящным:

auto r = 1000000ull;
// вместо
// unsigned long long r = 1000000;

Действительно полезное значение auto приобретает при работе с составными типами.

Print Friendly, PDF & Email

Comments are closed.