Особенности представления информации в компьютере. Представление целых чисел. Особенности реализации арифметических операций в конечном числе разрядов.

Особенности представления информации в компьютере.

В современной вычислительной технике вся информация хранится на информационных носителях, имеющих “ячейки”, которые могут находиться в двух устойчивых состояниях. Это относится и к жестким дискам, и к динамической памяти, и к flash-накопителям.

Этим и некоторыми историческими причинами обусловлен тот факт, что вся информация в компьютерах хранится в виде двоичного кода – некого представления в виде “нулей” и “единиц”.

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

Как уже говорилось, современные запоминающие устройства имеют “ячейки”, которые могут находиться в двух устойчивых состояниях. Такие ячейки будем называть разрядами. В связи с чисто физическими ограничениями, под хранение информации отводится конечное число разрядов.

Группы разрядов фиксированного размера называются регистрами.

Забегая вперед, скажу, что двоичный разряд хранит количество информации, называемое бит (от binary digit). Регистр, состоящий из \(n\) ячеек называется \(n\)-битным. Так, регистр из 8 ячеек называется восьмибитным, из 16 – шестнадцатибитным и т.п.

Далее мы рассмотрим, как в памяти компьютера представляется различная типичная информация, такая как целые и дробные числа, а так же текст.

Представление целых чисел

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

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

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

Так, например, можно говорить о беззнаковом восьмибитном целом представлении.

Беззнаковое представление

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

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

Пример:

Представлением числа \(1234_{10} = 10011010010_2\) будет следующий набор разрядов:

1 0 0 1 1 0 1 0 0 1 0

Поскольку количество разрядов в регистре ограничено, существуют так же ограничения на максимально представимое число. Минимально представимое число для беззнакового представления – всегда 0.

Так, например, в 8 разрядах максимально представимое число \(11111111_2 = FF_{16} = 255_{10}\)

Общая формула для максимально представимого числа в \(n\) разрядах \(I_{max} = 2^{n} - 1\)

Некоторые распространенные примеры:

Число разрядов Тип в C++1 Максимально представимое
8 unsigned char 255
16 unsigned short 65535
32 unsigned int 4 294 967 295
64 unsigned long long 18 446 744 073 709 551 615

Существует способ проверить размерность основных целочисленных типов. Пример программы для проверки битности:

#include <iostream>
using namespace std;

int main() {
	cout << "unsigned char " << sizeof(unsigned char) * 8 << "\n" <<
	        "unsigned short " << sizeof(unsigned short) * 8 << "\n" <<
	        "unsigned int " << sizeof(unsigned int) * 8 << "\n" <<
	        "unsigned long " << sizeof(unsigned long) * 8 << "\n" <<
	        "unsigned long long " << sizeof(unsigned long long) * 8 <<
          endl;
}

Стандартным способом проверки максимального значения в C++ является шаблонная структура std::numeric_limits<T>, определенная в заголовке <limits>. Пример проверки максимального значения основных целочисленных типов:

#include <iostream>
#include <limits>
using namespace std;

int main() {
	cout << "unsigned char "      << +numeric_limits<unsigned char>::max()      <<
          "\n" <<
	        "unsigned short "     << +numeric_limits<unsigned short>::max()     <<
          "\n" <<
	        "unsigned int "       << +numeric_limits<unsigned int>::max()       <<
          "\n" <<
	        "unsigned long "      << +numeric_limits<unsigned long>::max()      <<
          "\n" <<
	        "unsigned long long " << +numeric_limits<unsigned long long>::max() <<
          endl;
}

Одним из важных следствий представления числа в конечном числе разрядов является то, что при попытке представления числа более максимально представимого происходит целочисленное переполнение и старшие разряды безвозвратно утрачиваются. Одно из ключевых следствий заключается в том, что в \(n\) разрядах число \(2^n\) представляется так же, как \(0\). На основе этого свойства строится представление отрицательных чисел.

Знаковое представление

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

Дополнительного до чего? Оказывается, дополнительного до \(0\), или, как было сказано выше, в \(n\) разрядах – до \(2^n\).

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

Построение дополнительного кода2
  1. В \(n\)-битном представлении записывается модуль числа – это называется прямым кодом
  2. Все разряды инвертируются (там, где был 1 ставится 0, и наоборот) – это называется обратным кодом.
  3. К полученному представлению по правилам двоичной арифметики прибавляется 1.

Пример:

Построим дополнительный код числа \(-11\) в 8-битном знаковом представлении. \(11 = 0000\,1011_2\). Обратный код \(11110100_2\). Прибавляя 1, получаем дополнительный код: \(11110101_2\).

Для положительных чисел считается, что дополнительный код совпадает с прямым.

Если построить дополнительный код от дополнительного кода, снова получается прямой.

Таким образом, если старший (“левый”) разряд равен 1, то в знаковом представлении число интерпретируется как отрицательное.

Покажем, что основное арифметическое тождество выполняется:

\(11 - 11 = 0\). Рассмотрим представление в 8 разрядах.

Прямой код (11):

0 0 0 0 1 0 1 1

Дополнительный код (-11):

1 1 1 1 0 1 0 1

Сумма:

0 0 0 0 1 0 1 1
1 1 1 1 0 1 0 1
1 0 0 0 0 0 0 0 0

Поскольку представление 8-битное, старший бит результата отбрасывается. Остается

0 0 0 0 0 0 0 0

Поскольку один бит отводится под “знаковый” разряд, максимально представимое в знаковом представлении в 2 раза меньше, чем максимально представимое в беззнаковом представлении. По соглашению, в \(n\)-битном знаковом представлении, число \(1\underbrace{0\ldots0}_{n-1}\) считается отрицательным, однако его дополнительный код совпадает с прямым:

  • обратный код: \(0\underbrace{1\ldots1}_{n-1}\)
  • дополнительный код: \(0\underbrace{1\ldots1}_{n-1}+1= 1\underbrace{0\ldots0}_{n-1}\)

Таким образом, модуль минимально представимого числа на 1 больше, чем модуль максимально представимого числа.

Общая формула для максимально представимого в \(n\)-битном знаковом представлении: \(I_{max} = 2^{n-1}-1\).

Общая формула для минимально представимого в \(n\)-битном знаковом представлении: \(I_{min} = -2^{n-1}\).

Некоторые распространенные примеры:

Число разрядов Тип в C++3 Минимально представимое Максимально представимое
8 char -128 127
16 short -32768 32767
32 int -2 147 483 648 2 147 483 647
64 long long -9 223 372 036 854 775 808 9 223 372 036 854 775 807

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

#include <iostream>
using namespace std;

int main() {
	cout << "char " << sizeof(char) * 8 << "\n" <<
	        "short " << sizeof(short) * 8 << "\n" <<
	        "int " << sizeof(int) * 8 << "\n" <<
	        "long " << sizeof(long) * 8 << "\n" <<
	        "long long " << sizeof(long long) * 8 <<
          endl;
}

Пример программы проверки минимального и максимального значения знаковых целочисленных типов:

#include <iostream>
#include <limits>
using namespace std;

int main() {
	cout << "char "      << +numeric_limits<char>::min()      << " "  <<
                          +numeric_limits<char>::max()      << "\n" <<
	        "short "     << +numeric_limits<short>::min()     << " "  <<
                          +numeric_limits<short>::max()     << "\n" <<
	        "int "       << +numeric_limits<int>::min()       << " "  <<
                          +numeric_limits<int>::max()       << "\n" <<
	        "long "      << +numeric_limits<long>::min()      << " "  <<
                          +numeric_limits<long>::max()      << "\n" <<
	        "long long " << +numeric_limits<long long>::min() << " "  <<
                          +numeric_limits<long long>::max() << endl;
}

Переполнение знакового представления приводит к тому, что устанавливается “знаковый” бит. В результате, при прибавлении 1 к максимально представимому, получается минимально представимое.

Перечисление чисел в конечном числе разрядов

Если в общем случае при перечислении чисел мы имеем “числовую прямую”, то в конечном числе разрядов получается “числовая окружность”: при прибавлении 1 к максимально представимому, получаем минимально представимое.

Эту особенность очень важно иметь в виду при разработке программного обеспечения.

Например, если Вы считаете зарплату сотрудника за месяц исходя из почасовой ставки. Пусть в месяце 20 рабочих дней, в дне 8 рабочих часов, и почасовая ставка 100 рублей. Все числа представимы в 8-битном знаковом представлении. Умножая, получаем \(20\cdot8\cdot100 = 16000\). Попытавшись представить это число в 8-битном знаковом представлении, получим:

1 0 0 0 0 0 0 0

По соглашению, это отрицательное число \(-128\). Оказывается, сотрудник еще остался должен.

Подобные “сюрпризы” не являются редкостью и о них необходимо помнить.

О конкретных правилах арифметики в C++ рекомендую спросить Вашего преподавателя по C++.


  1. Конкретные значения, вообще говоря, зависят от архитектуры целевого процессора и компилятора, поэтому приведенные сопоставления лишь “наиболее распространенные”. Стандарт гарантирует, что (unsigned) char имеет минимум 8 бит, (unsigned) short – минимум 16 бит, (unsigned) int – минимум 16 бит, (unsigned) long – минимум 32 бита, и (unsigned) long long – минимум 64 бита.↩︎

  2. Почему этот алгоритм работает, понять в целом не сложно. Мы определяем дополнительный код как некое представление \(y\) числа \(x\), которое при сложении с прямым кодом в \(n\) разрядах даст \(y+x = 2^n\). Сложение обратного кода с прямым по построению даст \(2^n-1\), следовательно, прибавление к обратному коду 1 даст дополнительный код. Более строго это можно показать, если записать разложение прямого и дополнительного кодов в ряд по степеням 2 до \(2^{n-1}\) степени (поскольку это степень старшего разряда в \(n\) разрядах) – тогда соответствующие разряды дополнительного кода становится несложно попросту найти, учитывая, что по формуле частичной суммы геометрической прогрессии \(2^n = 1 + \sum_{i=0}^{n-1} 2^i\).↩︎

  3. Конкретные значения, вообще говоря, зависят от архитектуры целевого процессора и компилятора, поэтому приведенные сопоставления лишь “наиболее распространенные”. Стандарт гарантирует, что (unsigned) char имеет минимум 8 бит, (unsigned) short – минимум 16 бит, (unsigned) int – минимум 16 бит, (unsigned) long – минимум 32 бита, и (unsigned) long long – минимум 64 бита.↩︎