Типы данных с плавающей точкойВ С++ существуют два типа данных с плавающей точкой: Как хранятся действительные числа в компьютере Для хранения действительных чисел в памяти компьютера отводится определённое количество бит. Действительное число хранится в виде знака (плюс или минус), мантиссы и экспоненты. Что такое мантисса и экспонента лучше объяснить на примере: масса Земли равна 5.972*1024 килограмм. Здесь 5.972 — мантисса, а 24 — экспонента. При выводе больших (или очень маленьких) чисел в программе на C++ можно увидеть на экране запись типа 5.972E23. Сначала выводится мантисса, затем — буква E, а затем — экспонента. Запись представлена в десятичной системе счисления. В таком же формате можно вводить большие или очень маленькие действительные числа. Этот формат называется экспоненциальной записью числа. Мы будем работать с типом double (с числами двойной точности), который занимает 8 байт. Один бит отводится под знак числа, 11 под экспоненту и 52 под мантиссу. С помощью 52 бит можно хранить числа длиной до 15-16 десятичных цифр. Таким образом, независимо от того, какая у числа экспонента, правильные значения будут иметь только первые 15 цифр. В примере с массой Земли точно заданы первые 4 цифры, таким образом, погрешность составляет 1020 килограмм. Это довольно большая погрешность. Чтобы масса Земли с точностью до первых четырёх знаков изменилась, на неё нужно дополнительно поселить миллиард миллиардов довольно упитанных людей. Таким образом, можно сказать, что числа в компьютере хранятся не с абсолютной, а с относительной погрешностью (то есть погрешность зависит от значения хранимого числа). То, что числа хранятся неточно, создаёт нам множество проблем. Итак, мы рассмотрели главные моменты, касающиеся основных типов данных в С++. Осталось только показать, откуда взялись все эти диапазоны принимаемых значений и размеры занимаемой памяти. А для этого разработаем программу, которая будет вычислять основные характеристики всех, выше рассмотренных, типов данных.
Данная программа выложена для того, чтобы Вы смогли просмотреть характеристики типов данных в своей системе. Не стоит разбираться в коде, так как в программе используются управляющие операторы, которые Вам, вероятнее всего, ещё не известны. Для поверхностного ознакомления с кодом программы, ниже поясню некоторые моменты. (b * 8 ) — 1;
|
Пример работы программы можно увидеть на рисунке 3. В первом столбце показаны основные типы данных в С++, во втором столбце размер памяти, отводимый под каждый тип данных и в третьем столбце — максимальное значение, которое может содержать соответствующий тип данных. Минимальное значение находится аналогично максимальному. В типах данных с приставкой unsigned
минимальное значение равно 0.
Рисунок 3 — Типы данных С++
Если, например, переменной типа short int
присвоить значение 33000, то произойдет переполнение разрядной сетки, так как максимальное значение в переменной типа short int
это 32767. То есть в переменной типа short int
сохранится какое-то другое значение, скорее всего будет отрицательным. Раз уж мы затронули тип данных int
, стоит отметить, что можно опускать ключевое слово int
и писать, например, просто short
. Компилятор будет интерпретировать такую запись как short int
. Тоже самое относится и к приставкам long
и unsigned
. Например:
1 2 3 4 5 |
|
C++ — язык со статической типизацией. У каждой переменной на этапе компиляции должен быть чётко определённый тип данных. Про каждый тип данных заранее известно, сколько места в памяти занимает переменная такого типа.
В этой главе мы познакомимся с некоторыми базовыми типами данных и с понятием области видимости переменных.
В С++ существует понятие области видимости (scope) переменной. Эта область ограничивается блоком кода, в котором переменная определена. Рассмотрим пример:
#include <iostream> int a = 1; // глобальная переменная int main() { int b = 2; // локальная переменная { int c = 3; // локальная переменная внутри блока std::cout << a << " " << b << " " << c << "\n"; // корректно } // Эта строчка не скомпилируется, // так как переменная c не определена в данной области: std::cout << c << "\n"; }
В этом примере есть три области:
a
;main
, в которой определена переменная b
;c
.В последней строке примера переменная c
недоступна, так как её область видимости уже закончилась. В случае коллизии имён компилятор всегда выбирает самую вложенную область видимости.
Рассмотрим пример:
#include <iostream> int main() { int x = 1; std::cout << x << "\n"; // напечатает 1 { int x = 2; // новая переменная, к предыдущему x не имеет отношения std::cout << x << "\n"; // напечатает 2 } std::cout << x << "\n"; // снова напечатает 1 }
Локальные переменные простых типов, таких как int
, не инициализируются по умолчанию нулём. Компилятор просто выделяет для них байты в стековой памяти, но при этом он не обязан как-либо их заполнять. Это один из принципов C++: мы не должны платить за то, что не используем.
Следующий фрагмент кода может напечатать всё что угодно:
#include <iostream> int main() { int x; std::cout << x << "\n"; // неопределённое поведение! int y; std::cin >> y; // а это допустимый сценарий }
Компиляторы g++
и clang++
обычно дают предупреждения о чтении неинициализированных переменных при использовании опции -Wall
или -Wuninitialized
:
$ clang++ -Wall program. = 0 1 warning generated.
Заметим, что std::string
является сложным типом и переменные такого типа всегда по умолчанию инициализируются пустой строкой. Поэтому нет необходимости писать std::string s = "";
. Пишите просто std::string s;
.
С типом int
мы уже знакомы. Рассмотрим другие фундаментальные типы данных в С++. Это так называемые интегральные типы и типы для вещественных чисел.
int main() { char c = '1'; // символ bool b = true; // логическая переменная, принимает значения false и true int i = 42; // целое число (занимает, как правило, 4 байта) short int i = 17; // короткое целое (занимает 2 байта) long li = 12321321312; // длинное целое (как правило, 8 байт) long long lli = 12321321312; // длинное целое (как правило, 16 байт) float f = 2.71828; // дробное число с плавающей запятой (4 байта) double d = 3. 141592; // дробное число двойной точности (8 байт) long double ld = 1e15; // длинное дробное (как правило, 16 байт) }
Обратите внимание, что символы, в отличие от строк (то есть массивов символов), записываются в апострофах, а не в кавычках. В примере выше мы записываем в переменную c
символ единицы. Фактически в памяти хранится ASCII-код этого символа, который равен 49.
Напомним, что каждый тип данных занимает заранее известное количество байтов памяти. Стандарт языка С++ не накладывает жёстких ограничений на размеры типов, они могут отличаться для разных платформ и компиляторов.
О том, что делать с этой особенностью, мы расскажем ниже. А пока отметим, что узнать размер переменной или типа на этапе компиляции можно с помощью оператора sizeof
.
Например, на 64-битной Linux-системе компилятор clang++
использует такие размеры для типов:
int main() { std::cout << "char: " << sizeof(char) << "\n"; // 1 std::cout << "bool: " << sizeof(bool) << "\n"; // 1 std::cout << "short int: " << sizeof(short int) << "\n"; // 2 (по стандарту >= 2) std::cout << "int: " << sizeof(int) << "\n"; // 4 (по стандарту >= 2) std::cout << "long int: " << sizeof(long int) << "\n"; // 8 (по стандарту >= 4) std::cout << "long long int: " << sizeof(long long) << "\n"; // 8 (по стандарту >= 8) std::cout << "float: " << sizeof(float) << "\n"; // 4 std::cout << "double: " << sizeof(double) << "\n"; // 8 std::cout << "long double: " << sizeof(long double) << "\n"; // 16 }
По умолчанию числовые типы – знаковые. 32 — 1 }
Минимальное и максимальное значение, помещающееся в данный числовой тип, можно получить так:
#include <iostream> #include <limits> // необходимо для numeric_limits int main() { // посчитаем для типа int: std::cout << "minimum value: " << std::numeric_limits<int>::min() << "\n" << "maximum value: " << std::numeric_limits<int>::max() << "\n"; }
Данный пример на 64-битной Linux-системе напечатает:
minimum value: -2147483648 maximum value: 2147483647
Приведённые выше примеры вывода оператора sizeof
верны для 64-битных архитектур, которые на сегодняшний день распространены повсеместно. Однако если бы мы скомпилировали и запустили такую программу на компьютере с 32-битной архитектурой, то получили бы другие результаты. Например, sizeof(long int)
стал бы равен 4, в то время как на современных компьютерах мы получили бы 8. Также бывают встраиваемые системы, под которые тоже можно писать на С++. Там битность архитектуры может быть ещё меньше, чем 32.
В заголовочном файле cstdint
стандартной библиотеки имеются целочисленные типы с фиксированным размером:
int8_t
/ uint8_t
int16_t
/ uint16_t
int32_t
/ uint32_t
int64_t
/ uint64_t
Число в имени типа означает количество бит, используемых для хранения в памяти. Например, int32_t
содержит 32 бита (4 байта) и часто соответствует типу int
. Если система не поддерживает какой-то тип, то программа с ним просто не скомпилируется.
Стандартные числовые типы имеют ограниченный размер и ограниченное множество допустимых значений. При арифметических операциях над числами таких типов может возникнуть переполнение — ситуация, когда результат операции не помещается в тип:
#include <iostream> int main() { unsigned int a = 123456; // на 64-битной платформе sizeof(a) == 4 // Произведение a * a не помещается в 4 байта, так как оно больше 2^32 std::cout << a * a << "\n"; }
В этом примере выражение a * a
будет иметь тот же тип, что и аргументы, поскольку результат не помещается в него целиком. То, что на самом деле будет вычислено, зависит от знаковости типа.
Беззнаковые типы можно спокойно переполнять: вычисления будут производиться по модулю соответствующей степени двойки. Другими словами, будут учтены только младшие биты результата:
int main() { unsigned int x = 0; // на 64-битной платформе sizeof(x) == 4 unsigned int y = x - 1; // 4294967295, то есть 2**32 - 1 unsigned int z = y + 1; // 0 }
Наоборот, для знаковых типов переполнение приводит к так называемому неопределённому поведению (UB, undefined behavior).
Такая ситуация не считается ошибкой компиляции (в самом деле, на стадии компиляции значения переменных могут быть ещё неизвестны). Но в этом случае стандарт С++ перестаёт что-либо гарантировать по поводу поведения программы. Компиляторы могут использовать такие случаи для оптимизации программ, полагаясь на то, что разработчики пишут код корректно и никогда не допускают неопределённого поведения. Далее нам встретятся и другие случаи неопределённого поведения.
Беззнаковые типы следует использовать, когда вы имеете дело с битовыми наборами. В остальных случаях предпочтительнее использовать знаковые типы.
Бинарные операции +
, -
и *
работают для чисел стандартным образом. Результат операции деления /
, применённой к целым числам, всегда округляется в сторону нуля. Таким образом, для положительных чисел операция /
возвращает неполное частное. Остаток от деления целых чисел можно получить с помощью операции %
.
int main() { int a = 7, b = 3; int q = a / b; // 2 int r = a % b; // 1 }
Если при делении нужно получить обычное частное, то один из аргументов нужно привести к вещественному типу (например, double
) с помощью оператора static_cast
:
int main() { int a = 6, b = 4; double q = static_cast<double>(a) / b; // 1. 5 }
Можно было бы написать чуть более кратко: double q = a * 1.0 / b;
. Тогда преобразование аргументов произошло бы неявно.
Арифметические операции над символами, а также сравнение символов друг с другом — это фактически операции над их ASCII-кодами:
#include <iostream> int main() { char c = 'A'; c += 25; // увеличиваем ASCII-код символа на 25 std::cout << c << "\n"; // Z }Таблица ASCII с шестнадцатеричными кодами символов. Слева указана старшая шестнадцатеричная цифра, справа — младшая. Цветом выделены так называемые управляющие символы, обычно не имеющие графического представления.
Операция +
применительно к строкам означает конкатенирование (то есть склейку). Это пример перегрузки операции: изначальному оператору сложения в стандартной библиотеке для строки придали новый смысл.
#include <string> int main() { std::string a = "Hello, "; std::string b = " world!"; std::string c = a + b; // Hello, world! }
Для каждой бинарной операции (например, +
) есть версия со знаком равенства (+=
) для случая, когда левый аргумент совпадает с переменной, которой присваивается результат:
int main() { int x = 5; x += 3; // x = x + 3 x *= x; // x = x * x }
Наконец, имеются операторы ++
и --
для увеличения или уменьшения переменной на единицу. Они бывают префиксные и постфиксные. Отличие состоит в значении выражения, которое будет вычисляться при применении такого оператора. Мы рассмотрим это позже, а пока привыкнем по умолчанию использовать префиксный оператор для обычных чисел:
int main() { int x = 5; ++x; // 6 --x; // снова 5 }
В языке C++ существуют три встроенных типа для записи дробных чисел: float
(4 байта), double
(8 байт) и long double
(16 или 8 байт, в зависимости от платформы). В большинстве случаев рекомендуется использовать тип double
. Тип float
разумно использовать там, где обрабатываются огромные массивы чисел, и возникает необходимость экономить память.
Как правило, хранение дробных чисел в С++ основано на стандарте IEEE 754. Число представляется в виде двоичной дроби в экспоненциальной записи: отдельно хранятся бит знака, порядок и мантисса.
Такое представление выгодно отличается от чисел с фиксированной точкой, где хранится фиксированное количество разрядов. Оно позволяет, хотя и с разной степенью точности, представлять числа, отличающиеся на порядки.
При работе с рациональными числами, знаменатель которых не является степенью двойки, неизбежно возникают погрешности представления. В следующей главе мы разберём как следует сравнивать такие числа.
Компилятор C++ умеет автоматически выводить тип переменной по значению, которое ей присваивается. Для этого вместо типа надо написать ключевое слово auto
:
int main() { auto x = 42; // int auto pi = 3.14159; // double }
Ключевое слово auto
позволяет сократить код и не выписывать сложные типы (нам встретятся дальше монстры вроде std::unordered_multimap<Key, Value>::const_iterator
). Важно подчеркнуть, что точный тип переменной всё равно становится известен в момент компиляции.
При использовании auto
со строками нужно быть осторожным. Важно знать, что конструкция auto s = "hello"
выведет низкоуровневый тип const char *
(указатель на неизменяемый набор символов в памяти), а не тип-обёртку std::string
.
Точные правила вывода типов похожи на правила вывода шаблонных параметров, с которыми мы познакомимся в главе про шаблоны.
/ Контент на английском языке, Теория программирования / Автор GameDevTraum
Название FLOAT происходит от системы числового представления « с плавающей запятой » и относится к типу примитивной переменной , которая характеризуется тем, что является числовой переменной, которая допускает десятичную часть , она может быть используется для представления положительных и отрицательных действительных чисел в пределах определенного диапазон и с определенной точностью , которая зависит от типа выбранной переменной с плавающей запятой.
Вот более подробная статья о том, что такое переменные в программировании и о различных типах переменных.
youtube.com/embed/l7Eq0lHzgD8″ title=»YouTube video player» frameborder=»0″ allow=»accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture» allowfullscreen=»»>Тип переменной | Количество битов | Точность | Наименьшее представимое значение | Наибольшее представимое значение |
float | 32 | ~6 a 9 digits | ±1.5 x 10 −45 | ±3. 4 x 10 38 |
double | 64 | ~15 a 17 digits | ±5.0 × 10 −324 | ±1.7 × 10 308 |
decimal | 128 | ~28 a 29 digits | ±1.0 x 10 -28 | ±7.9 x 10 28 |
Чтобы определить переменную с плавающей запятой, мы используем ключевое слово первого столбца, за которым следует имя переменной, например, следующим образом:
float aFloat;
двойное двойное;
десятичный aДесятичный;
Cuando ingresamos valores con decimales se utiliza el punto como separador y en el caso частности де лас переменных типо float, cuando se le asigna un valor hay que hacerlo se suele incluir la letra «f» , paraexplicitar que el valor que indicamos es de tipo float, por ejemplo:
Когда мы вводим значения с десятичными знаками, мы используем точку в качестве разделителя и в конкретном случае переменные типа float , когда мы присваиваем значение, мы обычно включаем букву «f» , чтобы явно указать, что значение, которое мы указываем, является значением с плавающей запятой, например:
aFloat = 3. 51f
В В предыдущем случае, поскольку мы присваиваем десятичное значение переменной с плавающей запятой, не будет проблем с тем, чтобы не включать «f», однако в других операциях, таких как выполнение методов, передающих значения с плавающей запятой в качестве параметров, может быть обязательным указать, что такие значение – число с плавающей запятой.
Переменные с плавающей запятой позволяют нам представлять данные, принадлежащие числовому набору действительных чисел, поэтому мы можем использовать их для представления чисел, имеющих такую природу, например вес, температура, объем, высота. Они также используются для создания более сложных структур данных, таких как массивы и массивы данных с плавающей запятой, классы программирования, такие как Vector2 и Vector3, или мы также можем определить наши собственные классы программирования и использовать числа с плавающей запятой в их внутренней структуре.
Редактировать
Твиттер LinkedIn Фейсбук Электронная почта
Числа с плавающей запятой используют формат IEEE (Институт инженеров по электротехнике и электронике). Значения одинарной точности с типом float имеют 4 байта, состоящие из знакового бита, 8-битной двоичной экспоненты с избыточным значением 127 и 23-битной мантиссы. Мантисса представляет собой число от 1,0 до 2,0. Поскольку старший бит мантиссы всегда равен 1, он не сохраняется в числе. Это представление дает диапазон приблизительно от 3,4E-38 до 3,4E+38 для типа float.
Вы можете объявить переменные как float или double, в зависимости от потребностей вашего приложения. Принципиальные различия между этими двумя типами заключаются в значимости, которую они могут представлять, требуемой памяти и их диапазоне. В следующей таблице показана взаимосвязь между значимостью и требованиями к хранению.
Тип | Значащие цифры | Количество байтов |
---|---|---|
поплавок | 6 — 7 | 4 |
двойной | 15 — 16 | 8 |
Переменные с плавающей запятой представлены мантиссом, которая содержит значение числа, и показателем степени, который содержит порядок величины числа.
В следующей таблице показано количество битов, выделенных для мантиссы, и показатель степени для каждого типа с плавающей запятой. Старшим битом любого float или double всегда является бит знака. Если это 1, число считается отрицательным; в противном случае оно считается положительным числом.
Тип | Длина экспоненты | Длина мантиссы |
---|---|---|
поплавок | 8 бит | 23 бита |
двойной | 11 бит | 52 бита |
Поскольку показатели степени хранятся в беззнаковой форме, показатель степени смещен на половину своего возможного значения. Для типа float смещение равно 127; для типа double это 1023. Фактическое значение экспоненты можно вычислить, вычитая значение смещения из значения экспоненты.
Мантисса хранится в виде двоичной дроби, большей или равной 1 и меньшей 2. Для типов float и double в мантиссе в самом старшем разряде подразумевается ведущая 1, поэтому фактически мантисса равна 24. и 53 бита соответственно, хотя самый старший бит никогда не сохраняется в памяти.
Вместо только что описанного метода хранения пакет с плавающей запятой может хранить двоичные числа с плавающей запятой как денормализованные числа. «Денормализованные числа» — это ненулевые числа с плавающей запятой с зарезервированными значениями экспоненты, в которых старший бит мантиссы равен 0. Используя денормализованный формат, диапазон числа с плавающей запятой может быть расширен за счет точности. Вы не можете контролировать, представлено ли число с плавающей запятой в нормализованной или денормализованной форме; пакет с плавающей запятой определяет представление. Пакет с плавающей запятой никогда не использует денормализованную форму, если показатель степени не становится меньше минимума, который может быть представлен в нормализованной форме.
В следующей таблице показаны минимальные и максимальные значения, которые вы можете хранить в переменных каждого типа с плавающей запятой. Значения, перечисленные в этой таблице, относятся только к нормализованным числам с плавающей запятой; денормализованные числа с плавающей запятой имеют меньшее минимальное значение. Обратите внимание, что числа, хранящиеся в 80 90 229 x 90 230 87 регистрах, всегда представлены в 80-битной нормализованной форме; числа могут быть представлены в денормализованной форме только при сохранении в 32-битных или 64-битных переменных с плавающей запятой (переменные типа float и типа long).
Тип | Минимальное значение | Максимальное значение |
---|---|---|
поплавок | 1.175494351 Э — 38 | 3.402823466 Е + 38 |
двойной | 2.2250738585072014 Э — 308 | 1.7976931348623158 Е + 308 |
Если точность важнее хранения, рассмотрите возможность использования типа float для переменных с плавающей запятой. И наоборот, если точность является наиболее важным критерием, используйте тип double.
Переменные с плавающей запятой могут быть преобразованы в более значимый тип (из типа float в тип double). Продвижение часто происходит, когда вы выполняете арифметические действия над переменными с плавающей запятой. Эта арифметика всегда выполняется с такой же высокой степенью точности, как и переменная с наивысшей степенью точности. Например, рассмотрим следующие объявления типов:
float f_short; двойной f_long; длинный двойной f_longer; f_short = f_short * f_long;
В предыдущем примере переменная f_short
преобразуется в тип double и умножается на f_long
; затем результат округляется до типа float перед присвоением f_short
.
В следующем примере (в котором используются объявления из предыдущего примера) арифметические операции над переменными выполняются с точностью до числа с плавающей запятой (32 бита); затем результат преобразуется в тип double:
f_longer = f_short * f_short;