Внутреннее представление простых типов

В современных компьютерных архитектурах имеется две основных формы представления данных:

· с фиксированной точкой (в русском переводе иногда употребляют термин "с фиксированной запятой", т. к. речь идет о разделителе, отделяющем целую часть числа от дробной части);

· с плавающей точкой (запятой).

Первый способ употребляется для хранения всех целочисленных типов, второй способ используется для хранения вещественных чисел.

В представлении с фиксированной точкой целое число хранится в двоичной системе счисления, занимая столько байт памяти, сколько определено для его типа.

Например, целое число 19 в двоичной форме с фиксированной точкой будет записано как 10011 (разложение по степеням двойки: 19=1×24+0×23+0×22+1×21+1×20). В типах signed один крайний бит отводится под знак (0 для положительного числа, 1 для отрицательного). Остальные незанятые биты заполняются нулями.

Зная размер целого типа данных, несложно определить диапазон его допустимых значений: если тип занимает k бит, то количество различных значений равно 2k. При этом для знаковых типов количество отрицательных значений равно количеству неотрицательных – например, для типа int, который обычно занимает 32 бита, диапазон значений получается от -231 до 231-1, то есть от -2147483648 до 2147483647 (на практике обычно достаточно помнить, что диапазон составляет от минус двух миллиардов с небольшим до плюс двух миллиардов с небольшим). Впрочем, точные значения границ диапазонов всегда можно получить с помощью констант, определенных в заголовочном файле climits, например, INT_MIN и INT_MAX.

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

Например:

вещественное число 15,375 в двоичной форме с плавающей запятой можно представить как 1,111011×211, где 1,111011 — мантисса, 11 — порядок.

В ячейке памяти, отводимой под число с плавающей точкой, в действительности хранится не одно, а два значения — мантисса и порядок.

Сравнение способов представления с фиксированной и плавающей точкой. Рассмотрим следствия, которые вытекают из рассмотренных способов хранения данных.

· Целочисленные типы — это интервал целых чисел. Операции над ними определены лишь тогда, когда исходные данные и результат лежат в этом интервале. Иначе возникает ситуация, называемая переполнением. При целочисленном переполнении, как правило, не возникает ошибка выполнения программы, однако результат становится неверным. За исключением переполнения все операции над аргументами с фиксированной точкой выполняются точно (без погрешности).

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

· Вещественные числа, как и целые, представляются конечным множеством значений (хотя количество различных значений вещественных чисел так велико, что может показаться бесконечным).

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

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

· Следует быть осторожным при выполнении операций между числами, порядки которых сильно отличаются, например, между очень большими и очень маленькими числами. Возможны ситуации, когда мантисса не сможет обеспечить требуемую точность. Например, при сложении чисел 10000000 и 0.00000001 типа float разрядности мантиссы не хватит, чтобы представить число 10000000.00000001, и результат останется равным 10000000.

· Стоит запомнить, что наиболее часто используемый тип double способен хранить 15 значащих десятичных цифр (хотя вследствие погрешностей вычислений точность последних цифр не всегда гарантируется).

Для уменьшения влияния ошибки округления при выполнении арифметических операций с вещественными числами необходимо иметь в виду следующее. Если складывается много чисел, то их необходимо разбить на группы чисел, близких по абсолютному значению, произвести суммирование в группах, начиная с меньшего числа, после чего полученные суммы сложить, опять-таки начиная с меньшей. Таким образом, при работе с вещественными числами при перестановке мест слагаемых сумма может измениться. Похожие рекомендации можно дать и для других арифметических операций.

1.2.4 Ключевое слово typedef. Тип size_t

В заключение данного раздела отметим, что С++ позволяет определять новые имена для уже существующих типов при помощи инструкции

typedef тип новое_имя;

Новое имя определяется в качестве дополнения к существующему имени типа, а не для его замены.

Например, с помощью следующей инструкции можно создать новое, более короткое, имя для типа long long: typedef long long big;

Таким способом можно повысить переносимость и читаемость программного кода.

Например, в стандартной библиотеке С++ с помощью инструкции typedef определён тип size_t – синоним для беззнакового целого числа, являющегося результатом операции sizeof. Это обозначение типа часто используется в различных функциях стандартной библиотеки, его можно использовать в программном коде на С++, но, конечно, там, где это уместно.

Константы и переменные

Программа на С++ манипулирует данными, используя для этого константы и переменные.

Константы,в свою очередь, делятся на именованные и литеральные (литералы). Именованные константы отличаются от переменных только тем, что компилятор строго пресекает все попытки изменить их значения. Литералы требуют отдельных пояснений.

Литералы

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


1. Числовые константы:

* вещественные: 3.14, -2.5

* целые:

десятичные числа 15, 100

восьмеричные числа – запись числа начинается с нуля: 00, 01, 02 …

шестнадцатеричные – начинаются с 0x: 0x10, 0xFF

Тип константы определяется компилятором по её записи. Однако, тип можно задать явно при помощи суффиксов:

l,L – long int, например, 35L

uh, Uh, UH, hu, Hu, hU – unsigned short, например, 227UH

f,F – float, например, 1.5F

l,L – long double, например, 1.5L

2. Символьные константы:

* клавиатурные: ‘a’,’b’,’c’ – клавиатурный символ задаётся в апострофах

* кодовые – для задания некоторых управляющих и разделительных символов:

‘\n’ – перевод строки (код 13)

‘\r’ – возврат каретки (код 10)

‘\\’ – обратная косая черта (удваивается)

‘\’’ – апостроф

и др.

* кодовые числовые – для задания любых ASCII-символов, имеют вид:

‘\xHH’, ‘\XHH’, ‘\0OOO’ , где H, O – шестнадцатеричная и восьмеричная цифры соответственно.

3. Строковые константы:

это последовательности символов, ограниченные двойными кавычками:

“This string constant”

Для задания управляющих и разделительных символов внутри строки используются символьные кодовые и числовые константы без апострофов. Например:
”first\nsecond” – после first вставляется символ перевода строки (т.е. при выводе такой константы second будет выведен в новой строке).

4. Логические константы: true и false.

Переменные

К ним же отнесём и именованные константы.

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

Область видимости определяет часть тела программы, в которой может быть использована ("видна") данная переменная. Началом области видимости всегда является оператор описания переменной, а концом – либо окончание того блока, в котором описана переменная, либо конец файла. Возможно использование одной и той же переменной и в разных файлах, относящихся к одному проекту.

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

Время жизни– это интервал времени выполнения программы, в течение которого данная переменная существует, т.е. привязана к конкретной ячейке памяти. Время жизни переменной, как и область видимости, может быть локальным или глобальным. Переменная с глобальным временем жизни существует в течение всего времени выполнения программы. Переменная с локальным временем жизнисуществует только при выполнении блока, в котором она описана. При каждом входе в блок для такой переменной распределяется новая память, которая освобождается при выходе из блока.

Программист может управлять областью видимости и временем жизни переменных двумя путями:

· изменением места описания переменной в программе;

· использованием спецификаторов класса памяти в описании переменной.

Описание переменных

Оператор описания переменных и именованных констант имеет вид:

[спецификатор_класса_памяти][const][тип] имя[=значение][, имя[=знач.]]…;

Обратим внимание, что только имя является обязательным – даже тип в определённой конфигурации описания указывать не нужно. Для присвоения начального значения (инициализации переменной) вместо знака “=” можно использовать и другой синтаксис (более современный):

имя_переменной (значение), например, int x(5);.

Спецификатор класса памяти – дополнительные указания компилятору, как следует выделять память под переменную. Он может принимать значения: auto, extern, register, static. Если спецификатор в описании явно не указан, память для переменной выделяется компилятором, исходя из того места в программе, где это описание находится, и типа данных.

Ключевое слово const используется при описании именованных констант. В этом случае обязательно нужно задать значение константы. Если переменная явно не проинициализирована, то глобальные и статические (static) переменные получат нулевые значения. Локальные переменные (описанные в теле функций, в том числе, и в функции main) не инициализируются ничем и содержат мусор.

Например:

char a=’a’,b=’b’;

double x,y;

const double Y(1E-5);

const float PI = 3.1415926;

Спецификаторы класса памяти. Кратко поясним назначение каждого из спецификаторов.

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

Например: auto pi=3.1415926;

Спецификатор register предписывает компилятору распределить память для переменной в каком-либо регистре процессора или кэш-памяти, если это возможно, для повышения скорости доступа к этой переменной. Формально спецификатор register представляет собой лишь запрос, который компилятор вправе проигнорировать. Переменная с таким спецификатором должна иметь тип int и быть локальной.

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

// Пример 1.2 Использование спецификатора static

void fun(int x) {

static int i=x; cout << i << “ “;

}

int main(){

fun(1); fun(2);… return 0;

}

Результат этого, в общем то, бесполезного, примера будет неожиданным – 1 1. Дело в том, что инициализация статической переменной выполняется только один раз – при первом исполнении блока, в котором она описана. Если переменная со спецификатором static явным образом не инициализирована, она инициализируется нулём.

Спецификатор static можно применять и при описании глобальных переменных, в этом случае переменная будет доступна только в том файле, в котором она описана. Такое употребление static не рекомендуется – лучшим способом будет использование пространства имён.

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

Файл a.cpp:

int x;

Файл b.cpp:

extern int x;

int main(){

x=5; …

}

Обратим внимание – если при объявлении переменной указан класс памяти register или static, то тип можно не указать – по умолчанию будет int.

1.4. Выражения. Преобразование типов

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

Операнды и операции

Выражение состоит из операндов, знаков операций и, при необходимости, круглых скобок для изменения порядка выполнения операций. Операнды, в свою очередь, тоже являются выражениями, в частном случае, операндом может быть переменная, константа или результат функции (определенной в программе или в стандартной библиотеке С++). Функции с типом void не могут быть операндами в выражении, для их вызова используется отдельный оператор-выражение.

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

Арифметические операции:

+ (плюс – унарный и бинарный)

- (минус – унарный и бинарный)

* (умножение)

/ (деление)

Следует заметить, что целое деление дает целый результат, при этом дробная часть усекается: 8 / 3 есть 2. Над целыми может выполняться операция % получения остатка: 11 % 4 равно 3, а 1 % 4 равно 1 (1 / 4 равно 0).

Логические операции:

&& (логическое И)

|| (логическое ИЛИ)

! (логическое НЕ – унарная операция)

В языке C++ логические операции могут быть применены ко всем целочисленным типам, а не только к типу bool. При этом все нулевые значения трактуются как false, а значения, отличные от нуля, – true.

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

& (поразрядная операция И). Например, 3 & 5 = 1 (в двоичной системе 011 & 101 = 001).

| (поразрядная ИЛИ). Например, 3 | 5 = 7.

^ (исключающее ИЛИ). Например, 3 ^ 5 = 6.

<< (сдвиг операнда влево на заданное число разрядов).
Например, 3 << 2 = 12.

>> (сдвиг операнда вправо на заданное число разрядов).
Например, 3 >> 1 = 1.

Операции сравнения:

== (два подряд идущих знака равно – сравнение на равенство)

!= (не равно)

< (меньше)

> (больше)

<= (меньше или равно)

>= (больше или равно)

В C++ есть операция присваивания =, а не оператор присваивания, как в некоторых других языках, например, Pascal. Операция не только присваивает значение справа левому операнду, но и возвращает это значение. Таким образом, присваивание может встречаться в неожиданном контексте; например, x=3*(a=2*x); - в результате новое значение получит не только переменная x, но и переменная a (однако, не рекомендуется злоупотреблять подобными конструкциями). Выражение a=b=c; означает присвоение c сначала переменной b, а затем переменной a.

Другим свойством операции присваивания является то, что она может совмещаться с большинством бинарных операций – получаются составные операции: +=, -=, *=, /=, %=

Например, выражение x+=2 равносильно x=x+2.

Операция запятая (‘,’) – операция последовательного вычисления. Вычисляет значение левого и правого операнда, при этом возвращает значение правого операнда. Например:

c=(a=8,9);

В a будет помешено значение 8, в c – значение 9 (не рекомендуем использовать подобные конструкции). Заметим, что если вы имели в виду число 8.9, но случайно поставили запятую вместо точки, то сообщения об ошибке не будет выведено.

Операции -- и ++ - декремент и инкремент – уменьшают или увеличивают значение аргумента на 1. Могут быть записаны как перед операндом (префиксная форма), так и после операнда (постфиксная форма). Если знак операции записан перед операндом, то изменение операнда происходит до его использования в выражении, если после операнда – после его использования. Например:

x=3; a=++x; //a=4, x=4

x=3; a=x++; //a=3, x=4

Ещё пример:

x=3; a=++x+ ++x; //a=10, x=5

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

Операция sizeof(объект) – возвращает размер в байтах того объекта, к которому применяется (объектом может быть тип или переменная ).

В большинстве программ на C++ широко применяются указатели. Указатель – это переменная, содержащая адрес другой переменной. Унарная операция * разыменовывает указатель, т.е. *p есть объект, на который указывает p. Эта операция также называется косвенной адресацией. Например, если имеется объявление char* p, то *p есть символ, на который указывает p. Операция, противоположная разыменованию – получение адреса (&).Об указателях в C++ мы ещё поговорим подробнее.

В языке C++ имеется одна тернарная операция (с тремя аргументами) – условная операция, которая имеет следующий формат:

операнд1 ? операнд2 : операнд3

Операнд1 в данной операции задаёт условие. Если имеет значение true, то результатом операции будет являться значение операнда 2, иначе – операнда 3. Например, x=(a>0)?1:-1; - переменная x получит значение 1 или -1 в зависимости от значения переменной a. В данном примере скобки, в которые заключается условие, не являются обязательными, но с ними текст получается более наглядным.

Приоритет операций

Приоритет операций – очерёдность выполнения операций при условии, что в выражении нет круглых скобок. Приоритеты перечисленных выше операций представим в виде списка, в котором операции представлены в порядке убывания приоритетов:

· постфиксные ++ и --, вызов функции, обращение к элементу массива

· унарные + и -, префиксные ++ и --, !, sizeof, * и & (разыменование и адрес)

· *, /, %

· +, - бинарные

· <<, >>

· <, >, <=, >=

· =, !=

· &

· ^

· |

· &&

· ||

· =,+=, -= и другие составные операции

· Операция "," - у неё самый низкий приоритет

Если в выражении имеется несколько операций с одинаковыми приоритетами, то для большинства операций очерёдность их выполнения – слева направо, исключения – операция = и составные операции на её основе, префиксный инкремент – эти операции выполняются справа налево. Для изменения очерёдности выполнения операций в выражении используют круглые скобки.

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

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

При присваивании и арифметических операциях C++ выполняет все осмысленные преобразования между основными типами, чтобы их можно было сочетать без ограничений в выражениях. При автоматическом преобразовании типов операндов в выражении компилятор исходит из простого правила – преобразование выполняется к типу с более широким диапазоном. Например, при вычислении выражения 1/5.0 единица в числителе будет преобразована к вещественному типу и операция деления будет выполнена над двумя операндами с плавающей точкой – т.е. будет получен результат вещественного типа (без отсечения дробной части). Если нужно точнее указать тип знаменателя – можно написать 1/5.0F или 1/5.0L.

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

int x=1, y=5;

double z = x / y;

Несмотря на то, что переменная z имеет вещественный тип, она всё же будет инициализирована нулём, поскольку операция деления будет выполнена по правилам целочисленной арифметики с отсечением дробной части, а лишь после этого полученный нуль будет преобразован в вещественный тип.

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

1). Преобразование в стиле С – c-style cast. Синтаксис:

(тип) выражение или тип(выражение)

Например, для нашего случая:

int x=1, y=5; double z=(double)x/y;

В таком варианте переменная z получит корректное значение 0.2.

Этот способ может быть использован для преобразования любого типа в любой другой тип, при этом не проверяется корректность и безопасность преобразования – вся ответственность целиком ложится на программиста. Мы не рекомендуем использовать этот небезопасный способ. К тому же, такую конструкцию сложно искать в коде программы.

2). static_cast.

Для нашего примера:

int x=1, y=5; double z=static_cast<double>(x)/y;

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

3). const_cast − преобразование константной переменной в неконстантную (применяется только для типов - указателей и ссылок).

const int x = 1;

int *p = &x; // ошибка компиляции

int *p = const_cast<int*>(&x); // компилируется

В хорошо организованном коде необходимость такого преобразования, как правило, не возникает.

4). dynamic_cast – используется в объектно-ориентированном программировании для безопасного преобразования типов по иерархии наследования. Этот вариант будет рассмотрен в главе, посвящённой ООП.

5). reinterpret_cast − преобразование типов, которое никак не контролируется. Обычно используется, чтобы привести указатель к указателю, указатель к целому, целое к указателю. Применяется только в случае полной уверенности программиста в собственных действиях.

Ветвления и циклы

1.5.1 Организация ветвлений в программах на С++

Как и во многих других языках программирования, в С++ имеется два оператора для реализации ветвлений: условный оператор и оператор выбора.

Условный оператор (оператор if).

if (выражение) оператор1 [else оператор2]

В зависимости от условия, которое задаётся выражением в скобках, будет выполняться или оператор1, или оператор2, причём else оператор2 можно и опустить – тогда получится сокращённая форма условного оператора, позволяющая выполнять какие-либо действия только при определённом условии. Безусловно, оператор1 и оператор2 могут быть составными (в С++ обычно используют термин блок – группа операторов, заключённая в фигурные скобки).

Отметим, что в качестве условия можно использовать любое выражение – мы уже обращали внимание, что если результат вычисления выражения равен нулю, то это трактуется, как false (ложь), все значения, отличные от нуля, означают true (истина). Например, оператор if (x=0) x=1; является синтаксически правильным, но, скорее всего, это ошибка программиста, который вместо сравнения на равенство (два знака равно) записал обнуление переменной x – результат такого выражения трактуется как false, и значение 1 переменная x никогда не получит. Рекомендуем в настройках проекта включать максимальный уровень предупреждений (warnings) – это может помочь в обнаружении подобных ошибок.

Пример 1.3. Программа выполняет преобразование дюймов в сантиметры или сантиметров в дюймы в зависимости от введённых данных; предполагается, что вы укажете единицы измерения, добавляя i для дюймов и c для сантиметров (допустим, 5i или 4.3с). В программе как раз и проверяется введённая единица измерения. Если единица измерения указана некорректно, программа должна вывести нули.

// Пример 1.3 – использование оператора if

#include <iostream>

using namespace std;

int main() {

setlocale(LC_ALL, "Russian");

const float fac = 2.54; // коэффициент пересчёта

float x, in, cm; char ch = 0;

cout << "введите длину: "; cin >> x >> ch;

if (ch == 'i') { // inch - дюймы

in = x; cm = x*fac;

}

else if (ch == 'c') { // cm - сантиметры

in = x/fac; cm = x;

}

else

in = cm = 0; // неправильный ввод

cout << in << " in = " << cm << " cm\n";

system("pause"); return 0;

}

Наши рекомендации