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

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

Действительные (вещественные) типы представлены типами одинарной, двойной и расширенной точности: 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

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

  • Действительное число с фиксированной точкой. Например: 0.12, .5, 10.
  • Действительное число в экспоненциальном (научном) формате с разделителем мантиссы и порядка – e|E. Например: 3E8, 6.6720e-11, 2.E+20
  • Действительное число в экспоненциальном формате с разделителем мантиссы в виде шестнадцатеричного числа и порядка в виде десятичного числа – p|P. Например: 0xC.68p+2, 0x1.P-126, 0xa.bp10

Обратите внимание на то, что для этих форм нулевую дробную или целую часть числа (для экспоненциальной формы – в мантиссе) можно опустить (но не обе одновременно!). Также можно опустить и саму точку, оставив лишь целую часть числа. Но для улучшения представления кода точку всё же опускать не следует. Для представления отрицательной вещественной константы используется оператор “унарный минус”.
Типом по умолчанию для вещественных типов является тип double. Для явного указания типа в выражениях следует использовать суффиксы.
Представление вещественных чисел существенно отличается от представления целых чисел. Вещественное число представлено в памяти в виде двух частей: мантиссы и порядка. Мантисса – это число >= 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++), поэтому она хорошо интегрирована в систему и может работать с фундаментальными типами, подчиняясь, при этом, всем правилам языка. Для ускорения работы предусмотрен механизм определения длины контейнера для хранения “длинного” числа до их использования. Если выделенная память будет исчерпана, то увеличение произойдет автоматически. Поскольку это может привести к потере производительности о длине объекта нужно побеспокоиться заранее. Впрочем, работу с длинными числами можно полностью возложить на автоматику, если низкая производительность программы не критична.
Приведем небольшой пример для иллюстрации.

Программа cpp-6.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	 	 

Числовая библиотека std

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

Numerics library
Заголовок Описание
cmath Общие математические функции
random Генераторы случайных чисел и распределения
numeric Числовые операции над значениями в диапазонах
ratio Рациональная арифметика во время компиляции
numbers Математические константы

Некоторые часто используемые функции перечислены в таблице ниже.

Некоторые математические функции числовой библиотеки C++
Функция Описание
cmath
fabs Модуль действительного числа
fmod Остаток от делания действительных чисел
fmax/fmim Максимальное/минимальное из двух действительных чисел
exp ex
exp2 2x
log Натуральный логарифм
log10 Десятичный логарифм
log2 Логарифм по основанию 2
pow ab
sqrt Квадратный корень
hypot Гиротенуза
sin Синус
cos Косинус
tan Тангенс
asin Арксинус
acos Арккосинус
atan Арктангенс
atan2 Угол phi в полярных координатах
ceil Округление до ближайшего целого не меньше заданного значения
floor Округление до ближайшего целого не больше заданного значения
tranc Отсечение дробной части
round Округление по правилам математики
numeric
gcd Наибольший общий делитель двух целых чисел
lcm Наименьшее общее кратное двух целых чисел

Функции cmath используются с префиксами l и f.
Помимо функций, cmath содержит несколько важных математических констант:

Некоторые константы cmath
Имя Описание
M_E e
M_PI pi
M_LN2 ln(2)
M_LN10 ln(10)
M_LOG2E log2(e)
M_LOG10E log10(e)

Решение задач линейной структуры

Задачи в которых все действия выполняются последовательно, т. е. инструкции следуют одна за другой (алгоритмическая структура следование), относятся к задачам линейной структуры. Алгоритмы таких задач состоят из следующих частей:

  • Описание переменных и констант
  • Реализация диалога с компьютером
  • Инструкции реализации задачи
  • Вывод результатов

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

Программа cpp-6.2
#include <iostream>
#include <iomanip>
#include <cmath> // для sqrt
using namespace std;

int main() {
	double a, b; // вещественный тип данных
	cout << "a = "; cin >> a; // реализация диалога
	cout << "b = "; cin >> b;
	// гипотенуза
	double c = sqrt(a * a + b * b); // или hypot(a, b);
	double S = 0.5 * a * b; // площадь
	double P = a + b + c; // периметр
	double R = c / 2; // радиусы: описанной окружности
	double r = (a + b - c) / 2; // и вписанной окружности
	cout << setprecision(3) // количество дробных знаков
		 << fixed // формат с фиксированной точкой
		 << "c = " << setw(7) << c << "\n"
		 << "S = " << setw(7) << S << "\n"
		 << "P = " << setw(7) << P << "\n"
		 << "R = " << setw(7) << R << "\n"
		 << "r = " << setw(7) << r << endl;
	return 0;
}

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

a = 17
b = 19
c =  25.495
S = 161.500
P =  61.495
R =  12.748
r =   5.252

Следуюшая задача. В треугольнике известны три стороны 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)
Программа cpp-6.3
//==============================================================//
// Нахождение углов треугольника по трем сторонам               //
//==============================================================//
#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.

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

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

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

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

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

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

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

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

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

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

  1. чтобы получить предсказуемый результат (без потери точности) необходимо типу с большим диапазоном присваивать тип с меньшим диапазоном, с учетом знака;
  2. избегать в присваиваниях знаковому типу значение беззнакового типа и наоборот.
Программа cpp-6.4
#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:

Программа cpp-6.5
#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 имеются специальные функции осуществляющие контроль переполнения на этапе выполнения операций сложения, вычитания и умножения для всех целых базовых типов.
Приведем пример программы для операции сложения.

Программа cpp-6.6
#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() имеет три аргумента: первые два – это операнды операции сложения, а третий – указатель на переменную в которой сохраняется результат операции. (См. также иные способы здесь)
В программе ниже мы наглядно (по выводимому размеру) увидим в какой тип преобразуется выражение по результату вычислений.

Программа cpp-6.7
#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	 	 

Для того, чтобы определить размер выводимого типа в программе cpp-6.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;
Примеры решения задач
Вопросы
Темы сообщений
Задания А
Задания Б
Задания С
Ссылки
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 1,00 из 5)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Print Friendly, PDF & Email

Обсуждение закрыто.