Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Прочее

Нужны ли беззнаковые целые?

Плюрализм в одной голове — это шизофрения.
Наум Коржавин

Без беззнаковых целых чисел, в принципе, можно обойтись. В самом деле если нам вдруг понадобилось использовать число, большее 2 147 483 647 (это максимум для 32-битных целых; максимум для 32-битных беззнаковых целых — это 4 294 967 295), то надо просто перейти на 64-битные целые числа. По этому пути пошёл язык Java. В нём нет беззнаковых целых. Живи мы в идеальном мире, который был бы создан точно по нашим вкусам, то беззнаковые целые следовало бы исключить из языка нашей мечты. Но приходится мириться с существующим окружением, которое часто представлено ОС Windows. А WinAPI вовсю использует беззнаковые целые. Если мы хотим полноценного взаимодействия наших программ с Windows, то беззнаковые целые отбрасывать нельзя.

        Каков максимальный размер файла, с которым может работать ваша программа? Попробуем разобраться. 32-разрядные версии Windows поддерживают работу с файлами длиной 4Gb. Убеждаемся в этом, читая описание функции WinAPI ReadFile:
  BOOL ReadFile
  {  HANDLE       hFile,                // дескриптор файла
     LPCVOID      lpBufer,              // буфер данных
     DWORD        nNumberOfBytesToRead, // размер буфера: unsigned int!
     LPDWORD      lpNumberOfBytesRead,  // прочитанных байтов: unsigned int!
     LPOVERLAPPED lpOverlapped
  };
        Но рассмотрим функции позиционирования в файле (Borland C++ 5.5):
int       _RTLENTRY _EXPFUNC fseek(FILE * __stream, long __offset, int __whence);
int       _RTLENTRY _EXPFUNC fgetpos(FILE * __stream, fpos_t*__pos);
        Как видим, беззнаковые целые в определении этих функций не используются. Только целые со знаком. Следовательно, Borland C++ 5.5 просто не позволит нам работать с файлами длиннее 2GB! Или придётся придётся ужаться в своих желаниях?

        Ограничение размера файла 2Gb — не самая острая проблема. Тем более в эпоху перехода к 64-разрядным вычислениям. Однако необходимость взаимодействия с WinAPI вынуждает иметь в языке беззнаковые целые.

Коды символов — целые без знака или со знаком?

        Нет, наверное, такого программиста, который бы ни разу в жизни не обращался к таблице символов. Допустим, понадобилось нам узнать код, допустим, буквы «я» в кодовой странице 1251. Это буква имеет код 255. Пишем в программе:
   int  code = 'я';
   cout << "print code: " << code << "\n";
        Будет выведено:
print code: -1;
        Что за чертовщина? Как это символ может иметь отрицательный код? В таблице символов нет ни одного отрицательного кода! Увы, отрицательные коды символов — горькая правда. Се ля ви. Казалось бы, ничего страшного, надо об этом просто помнить. Но допустим, что у вас есть массив, где каждому символу соответствует некое целое число.
   int  ar [256];		// массив неких характеристик для всех 8-битных символов
   ar ['z'] = func('z');	// заполняем некую характеристику символу 'z'
   ar ['я'] = func('я');	// заполняем некую характеристику символу 'я'
        Увы, так нельзя. Поскольку 'я' имеет код, равный -1, то это вызовет обращение к элементу массива с индексом -1. Это не проблема для языков типа PHP. Но что делать в языках типа C/C++? Нет, нам всё-таки не нравится работать с отрицательными индексами масива. И как это преоделеть? Может, взять на вооружение такой трюк:
   int  ar0 [128];		// массив для отрицательных индексов [-128;-1]
   int  ar1 [128];		// массив для индексов [0;127]
   ar1 ['z'] = func('z');	// обращаемся к массиву с положительными индексами
   ar1 ['я'] = func('я');	// написано ar1 ['я'] (или ar1 [-1]), но на самом 
				// деле обращаемся к ar0 [127]
        Но вообще-то компилятор не гарантирует, что массивы «ar0» и «ar1» будут размещены в смежных участках памяти. Стандарт C/C++ (впрочем, и любого другого языка!) никак не гарантирует размещение соседних переменных так же по соседству и в памяти. Да и зачем делать такое, неужели нет нормальных решений? Попробуем пойти в обход.
   unsigned  int  code2 = 'я';
   cout << "print code2: " << code2 << "\n";
        Получается:
print code3: 4294967295;
        Идиотизм! Что за гениальный человек сделал коды отрицательными? Может, это всё из-за Страуструпа? Ведь в чистом C символьные константы типа 'я' имеют тип int, а в C++ — тип char. Переменные и константы типа char не могут иметь значения больше 127. Но если они имеют тип int, то int может позволить значения больше 127! Проверяем с помощью компилятора чистого C — TinyCC.
   int n = 'я';
   printf("code: %d\n", n);
   printf("size: %d\n", sizeof('я'));
Получаем:
   code: -1
   size: 4
Так, у Страуструпа есть отмазка: в C такой же бардак. Во всём виноват Ритчи. Вот его явка с повинной и чистосердечное признание (Керниган, Ритчи, «Язык программирования C», второе издание):

Являются ли переменные типа char знаковыми (signed) или беззнаковыми, зависит от конкретной системы, но выводимые на экран и печать символы всегда имеют положительные коды.

Вот так. Иными словами, хотите — придерживайтесь правил правостроннего движения. А можете — левостороннего, всё зависит от конкретной страны.

        Ну а нам-то что делать, когда нет стандартов де-юре, но есть стандарты де-факто? Пробуем побороться ещё:
   int  code3 = (unsigned  char)'я';
   cout << "print code3: " << code3 << "\n";
        Уф, наконец-то! Но как тщательно были расставлены грабли!
print code3: 255;
        В конце концов, если уж нам не нравится, что коды символов отрицательные, а в справочных таблицах написано совсем другое, то можем эти таблицы просто переписать. Теперь можно вздохнуть спокойно? Нет, погодите. Как бы не так! Первый попавшийся под руку учебник по C говорит нам, что... коды символов меняются от 0 до 255! Как же так? Мы же только что убедились, что символы хранятся в переменных типа char, которые имеют значения от -128 до 127! Давайте проверим, выведем на консоль символы в кодировке 866:
  cout << "\x8f\xe0\xa8\xa2\xa5\xe2\n";
        На консоли появится:
  Привет
        Что же получается? Строковые константы состоят из элементов типа char (дипазон значений — от -128 до 127), но в эти строки мы должны записывать данные от 0 до 255? Именно так! Язык C просто не предусматривает ввод отрицательных кодов внутри строковых констант!

        И это ещё не всё. При сортировке строк необходимо применять либо собственные функции сравнения (которые учитывают, что 'я' всё-таки больше 'z'), либо рассматривать сортируемые строки как unsigned char.

        Каков же вывод можно сделать из наших расследований? Можно ли сделать так, чтобы коды символов были бы положительными? Ведь есть же языки, где нет таких таких проблем. В Бейсике ведь всё нормально!

        Выходы видятся такими (в зависимости от типизации).
  • Статическая типизация, беззнаковых целых в языке не существуют. В этом случае придётся чем-то платить. Для хранения 8-битных символов в таком случае понадобится в теории 9 битов, а на практике 16. 16-битные символы UNICODE придётся хранить в 32 битах. Велика ли эта цена — решать создателям будущих языков.
  • Статическая типизация, беззнаковые целые в языке есть. Тогда символьные константы вполне могут быть unsigned char! Это один из аргументов «за» в спорах о праве на существование беззнаковых целых.
  • Динамическая типизация. В таких языках не имеет особого значения, какого типа та или иная величина. Если в программе будет написано «a = 'я';», то переменная «a» будет иметь строковый тип, а значением будет эта самая 'я'. А если понадобится узнать код этого символа? Так для этого в таких языках существует функция val(), которая возвращает код символа. А при написании функции всё в наших руках: хочу — и все коды будут положительные, хочу — всё наоборот. В этом случае лишние «технические детали» будут просто спрятаны и не видны программисту.

Сравнение знаковых и беззнаковых целых

        Если дать право на жизнь беззнаковым целым, то мы должны быть готовы и к другим фокусам. Например, к таким:
   unsigned  int  ui = 4294967290;
   int  i = -1;
   cout << "i < ui : ";
   if (i < ui)
      cout << "true";
   else
      cout << "false";
   cout << "\n";
Несмотря на то, что -1 < 4294967290, выводится:
i < ui : false
        Переменная «i» трактуется, как целое без знака! Да, и тут засада... Но избежать её — вполне посильная задача.

Беззнаковых целые в циклах

        Применение беззнаковых целых в качестве счётчика в циклах иногда чревато непредсказуемыми последствиями.
   char  ar [N];
   for (unsigned  int  i=N-1; i >= 0; --i)
      ar [i] = ...;
        В том случае, когда i = 0, то в конце цикла выполнится «--i». И тогда беззнаковый ноль превратится в ... Во что? В 4294967295? Ох! Уж лучше б в элегантные шорты... Ведь это означает, что условие «i >= 0» всегда истинно и цикл не прервётся, как ожидается, а продолжится дальше. И, следовательно, здесь имеет место бесконечный цикл, в котором «i» меняет свои значения от 4294967295 до 0. Причина — некорректное сравнение беззнаковых и знаковых целых при проверке условия продолжения цикла. Это мы рассматривали в предыдущем пункте.

Поведение при переполнениях

        Описанные выше проблемы связаны с некорректным поведением программ в случае переполнения. Переполнение на аппаратном уровне отследить вполне возможно. Архитектурой IBM/360/370 и ЕС ЭВМ преусмативалось программное прерывание при переполнениях. В архитектуре x86 есть специальный регистр флагов, в котором флаги CF и OF сигнализируют об имевших место переполнениях при арифметических операциях. Казалось бы, ничто не мешает как-то реагировать на них. Но в C/C++ этого нет, виной тому, скорее всего, необхомость межплатформенной переносимости. Ведь этот язык должен одинаково себя вести на сотнях платформ, включая компьютеры из 1970-х годов: PDP, DEC, ICL. Язык C возник в начале 1970-х и должен вести себя так же, как и 40 лет назад. Поэтому и современные компиляторы игнорирует аппаратные возможности реакции на переполнения.

        А вот язык Ruby, не имея за спиной ветхозаветного унаследованного кода, может себе позволить работать правильно. В Ruby при возникновении переполнения ячейка для хранения значения переменной «расширяется» таким образом, чтобы вместить без потерь новое значение. Т.е. «на ходу» меняется разрядность вычислений. Именно поэтому Ruby не проваливает тест на вычисление факториала. Большинство языков «ломается» на 12! или 13! В их числе Pascal, который хвалят за его «математичность».

Выводы по использованию беззнаковых целых.

        Они более применимы для языков со статической типизацией. Вот они:
  • В будущих языках всё-таки лучше иметь беззнаковые целые.
  • Переполнения должны генерировать исключительные ситуации. Программа не должна молча проглатывать такие ошибки. Ошибки не должны замалчиваться!
  • При присвоении целым числам некорректных значений тоже должна генерироваться исключительная ситуация.
  • Необходимо сделать корректным сравнение целых со знаком и без. Если сравниваются unsigned int A и int B, то
    • Если у А старший бит равен 1 (т.е. A > 2 147 483 647), то A > B
    • Если у B старший бит равен 1 (т.е. B отрицательно), то A > B
    • Иначе старшие биты у обоих чисел равны 0 и результат сравнения A > B будет корректен.

Что ещё почитать на эту тему

Последняя правка: 2015-01-25    08:34

ОценитеОценки посетителей
   █████████████ 3 (30%)
   █████ 1 (10%)
   █████████ 2 (20%)
   █████████████████ 4 (40%)

Отзывы

     2012/10/05 08:32, utkin

В языке программирования Scheme реализована такая модель чисел, которая наиболее соответствует современным математическим представлениям. Каждое число там является комплексным. Однако оно имеет и другие статусы. Например - 3 это и целое число и вещественное число и +3 и комплексное число. Скажем так все целые числа языка являются вещественными. Все вещественные числа являются комплексными. И хотя для внутреннего представления используются разные форматы, эта модель полностью прозрачна для программиста - разные числа могут участвовать в одном выражении без явного преобразования. Исключения могут составлять только целые числа - в ряде реализации они являются длинными и могут содержать произвольное число разрядов. Это при операциях, результатом которого указано не целое число может привести к переполнению Обязанности по отслеживанию такой ситуации лежат на программисте.

     2014/09/28 15:51, 1231231

"Переменная «ui» трактуется, как целое со знаком! Да, и тут засада... Но избежать её — вполне посильная задача."

Наоборот это "i" трактуется, как целое без знака.

     2014/09/29 14:16, Автор сайта

Да, Вы правы, сейчас исправлю

     2016/08/13 23:30, rst256

Увы, так нельзя. Поскольку 'я' имеет код, равный -1, то это вызовет обращение к элементу массива с индексом -1. Это не проблема для языков типа PHP. Но что делать в языках типа C/C++?

char s[256]; char *s1=&(s[127]); //и индексируйте в обе стороны на здоровье

Идиотизм! Что за гениальный человек сделал коды отрицательными?

Бред, не понимаю в чем проблема, signed char - будут отрицательные, unsigned char - не будут. И не все ли равно в какую сторону сортировать?

И по вопросу переполнения.
Gcc:
6.53 Built-in Functions to Perform Arithmetic with Overflow Checking

The following built-in functions allow performing simple arithmetic operations together with checking whether the operations overflowed.
— Built-in Function: bool __builtin_add_overflow (type1 a, type2 b, type3 *res)
— Built-in Function: bool __builtin_sadd_overflow (int a, int b, int *res)
— Built-in Function: bool __builtin_saddl_overflow (long int a, long int b, long int *res)
...
И это:
Clang provides the following checked arithmetic builtins:
bool __builtin_add_overflow (type1 x, type2 y, type3 *sum);
bool __builtin_sub_overflow (type1 x, type2 y, type3 *diff);
bool __builtin_mul_overflow (type1 x, type2 y, type3 *prod);
...

Написать отзыв

Написать автору можно на электронную почту mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Каким должен быть язык программирования?

Анализ и критика

Устарел ли текст как форма представления программы

Русский язык и программирование

Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

Некоторые «вкусности» Алгол-68

«Двухмерный» синтаксис Python

Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

Должна ли программа быть удобочитаемой?

Стиль языка программирования

Тексто-графическое представление программы

●  Разделители

●  Строки программы

●  Слева направо или справа налево?

Комментарии

●  Длинные комментарии

●  Короткие комментарии

●  Комментарии автоматической генерации документации

●  Нерабочий код

Нужны ли беззнаковые целые?

Шестнадцатиричные и двоичные константы

Условные операторы

Переключатель

Циклы

●  Продолжение цикла и выход из него

Некошерный «goto»

Операции присвоения и проверки на равенство. Возможно ли однаковое обозначение?

Так ли нужны операции «&&», «||» и «^^»?

Постфиксные инкремент и декремент

Почему в PHP для конкатенации строк используется «.»?

Указатели и ссылки в C++

Использование памяти

Почему динамическое распределение памяти – это плохо

Как обеспечить возврат функциями объектов переменной длины?

●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●  Массивы переменной длины в C/C++

●  Размещение объектов в стеке, традиционный подход

●  Размещение объектов переменной длины с использованием множества стеков

●  Размещение объектов переменной длины с использованием двух стеков

●  Реализация двухстековой модели размещения данных

●  Двухстековая модель: тесты на скорость

●  Размещение объектов переменной длины с использованием одного стека

Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

Безопасность и размещение объектов переменной длины в стеке

Массивы, структуры, типы, классы переменной длины

О хранении данных в стеке, вместо заключения

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Прочее

Последние комментарии

2018/10/11 22:29, Автор сайта
Формула расчета точности для умножения

2018/10/08 14:00, Неслучайный читатель
Сколько проходов должно быть у транслятора?

2018/10/06 12:19, Автор сайта
Тексто-графическое представление программы

2018/10/04 17:39, Автор сайта
Об исключенных командах или за что «списали» инструкцию INTO?

2018/09/29 16:52, Автор сайта
Как отличить унарный минус от бинарного

2018/09/22 20:13, Д.Ю.Караваев
Идеальный транслятор

2018/09/22 12:32, Автор сайта
Типы в инженерных задачах

2018/09/22 12:20, Д.Ю.Караваев
О русском языке в программировании