Типы данных с плавающей точкойВ С++ существуют два типа данных с плавающей точкой: Как хранятся действительные числа в компьютере Для хранения действительных чисел в памяти компьютера отводится определённое количество бит. При выводе больших (или очень маленьких) чисел в программе на C++ можно увидеть на экране запись типа 5.972E23. Сначала выводится мантисса, затем — буква E, а затем — экспонента. Запись представлена в десятичной системе счисления. В таком же формате можно вводить большие или очень маленькие действительные числа. Этот формат называется экспоненциальной записью числа. Мы будем работать с типом double (с числами двойной точности), который занимает 8 байт. Один бит отводится под знак числа, 11 под экспоненту и 52 под мантиссу. С помощью 52 бит можно хранить числа длиной до 15-16 десятичных цифр. Таким образом, независимо от того, какая у числа экспонента, правильные значения будут иметь только первые 15 цифр. В примере с массой Земли точно заданы первые 4 цифры, таким образом, погрешность составляет 1020 килограмм. Таким образом, можно сказать, что числа в компьютере хранятся не с абсолютной, а с относительной погрешностью (то есть погрешность зависит от значения хранимого числа). То, что числа хранятся неточно, создаёт нам множество проблем. Итак, мы рассмотрели главные моменты, касающиеся основных типов данных в С++. Осталось только показать, откуда взялись все эти диапазоны принимаемых значений и размеры занимаемой памяти. А для этого разработаем программу, которая будет вычислять основные характеристики всех, выше рассмотренных, типов данных.
Данная программа выложена для того, чтобы Вы смогли просмотреть характеристики типов данных в своей системе. Не стоит разбираться в коде, так как в программе используются управляющие операторы, которые Вам, вероятнее всего, ещё не известны. Для поверхностного ознакомления с кодом программы, ниже поясню некоторые моменты.
|
Пример работы программы можно увидеть на рисунке 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 происходит от системы числового представления « с плавающей запятой » и относится к типу примитивной переменной , которая характеризуется тем, что является числовой переменной, которая допускает десятичную часть , она может быть используется для представления положительных и отрицательных действительных чисел в пределах определенного диапазон и с определенной точностью , которая зависит от типа выбранной переменной с плавающей запятой.
Вот более подробная статья о том, что такое переменные в программировании и о различных типах переменных.