Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки 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 будет корректен.

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

Последняя правка: 2018-11-30    09:50

ОценитеОценки посетителей
   █████████████ 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);
...

     2016/08/19 11:36, Автор сайта

индексируйте в обе стороны на здоровье... не все ли равно в какую сторону сортировать?.. не понимаю в чем проблема

Это в Паскале индексы в массиве имеют произвольные величины:
M [m..n]
В Си индексы начинаются с нуля. Адресом массива является адрес его нулевого элемента
M [n]
& M [0] == M
Проблема заключается в нелогичности и непоследовательности. Читаем в десяти справочниках о том, что код символа «я» равен 255, а потом в десяти компиляторах убеждаемся, что это не так. Читаем наставления гуру от программирования, что выход за границы массива является одной из самых распространённых ошибок в Си, но сами стимулируем эти ошибки отрицательными диапазонами.

     2018/11/29 05:19, Freeman

Справедливости ради, fseek перемещает позицию в обе стороны, так что смещение может быть отрицательным по задаче. Про fpos_t сходу не скажу, но возможно, файловый API стандартной библиотеки Си предполагает возврат отрицательных позиций в качестве признака ошибки или выхода за границы файла.

     2018/12/02 18:00, БудДен

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

Соответственно, вопрос ортогонален тому, статическая ли типизация.

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

С действительными числами всё ещё хуже. Теоретически, можно пойти по определению и описать действительное число функцией(n), выдающий n-й член последовательности, сходящейся к этому числу. Если будут заданы ещё и свойства сходимости этой последовательности, то можно будет даже проводить операции над ними. Но мне никогда не попадался такой подход, разве только в Mathematica есть что-то подобное. Числа с плавающей точкой — это особый объект. Слава Богу, хотя бы есть стандарт. Хотя не факт, что этот стандарт хорошо подходит для данной конкретной задачи.

     2018/12/03 08:44, Freeman

Про математическую библиотеку для ЯП с критикой IEEE 754 было когда-то обсуждение на OSDev.ru: http://osdev.ru/viewtopic.php?f=18&t=1057

     2018/12/07 04:38, rst256

В Си индексы начинаются с нуля. Адресом массива является адрес его нулевого элемента

В Си индексы могут быть отрицательными, это просто смешение указателя. Делайте указатель на середину массива (т.е. на символ с кодом равным 0) и индексируйте на здоровье.

     2018/12/07 08:36, Автор сайта

В том то и дело, что я этого не хочу! Я хочу соблюдать правила, а по правилам индексы имеют нумерацию от 0 до n-1. И хочу, чтобы компилятор следил за выполнением правил. Но он (компилятор Си), не следит. Это плохо, но позволяет выходить за границы массива — прямо сейчас, без дополнительных усилий можно просто написать M[-1] — и это будет работать.

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

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

Авторизация

Регистрация

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

Карта сайта


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Комментарии

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

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

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

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

●  Помеченные комментарии

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

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

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

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

Циклы

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

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

Изменение приоритетов операций

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

●  Изменение длины объекта в стеке во время исполнения

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

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

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

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

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

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

Компилятор

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

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

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

Новости и прочее

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

2018/12/16 17:17 ••• Геннадий Тышов
✎ Программирование без программистов — это медицина без врачей

2018/12/07 08:57 ••• Автор сайта
✎ Почему обречён язык Форт

2018/12/07 08:36 ••• Автор сайта
✎ Нужны ли беззнаковые целые?

2018/12/03 13:51 ••• kt
✎ Экстракоды при синтезе программ

2018/11/30 17:56 ••• Freeman
✎ Изменение приоритетов операций

2018/11/30 17:20 ••• Автор сайта
✎ Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

2018/11/26 14:23 ••• Автор сайта
✎ Так ли нужны операции «&&», «||» и «^^»?

2018/11/18 15:21 ••• Freeman
✎ Устарел ли текст как форма представления программы

2018/11/17 03:28 ••• Comdiv
✎ Изменение длины объекта в стеке во время исполнения

2018/11/16 12:53 ••• Автор сайта
✎ Помеченные комментарии

2018/11/11 14:01 ••• Александр Коновалов aka Маздайщик
✎ Нерабочий код

2018/11/11 13:39 ••• Александр Коновалов aka Маздайщик
✎ О русском языке в программировании