company_banner

Свод правил по работе с целыми числами в C/C++

Автор оригинала: Project Nayuki
  • Перевод

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

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

Типы данных


Базовые целочисленные типы


Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора {char, short, int, long, signed, unsigned}.

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

  • char: минимум 8 бит в ширину;
  • short: минимум 16 бит и при этом не меньше char;
  • int: минимум 16 бит и при этом не меньше short;
  • long: минимум 32 бит и при этом не меньше int;
  • long long: минимум 64 бит и при этом не меньше long.

Наличие знака


  • Стандартный сhar может иметь знак или быть беззнаковым, что зависит от реализации.
  • Стандартные short, int, long и long long идут со знаком. Беззнаковыми их можно сделать, добавив ключевое слово unsigned.
  • Числа со знаком можно кодировать в двоичном формате в виде дополнительного кода, обратного или как величину со знаком. Это определяется реализацией. Заметьте, что обратный код и величина со знаком имеют различные шаблоны битов для отрицательного нуля и положительного, в то время как дополнительный код имеет уникальный нуль.
  • Символьные литералы (в одинарных кавычках) имеют тип (signed) intв C, но (signed или unsigned) char в C++.

Дополнительные правила


  • sizeof(char) всегда равен 1, независимо от битовой ширины char.
  • Битовая ширина не обязательно должна отличаться. Например, допустимо использовать char, short и int, каждый шириной в 32 бита.
  • Битовая ширина должна быть кратна 2. Например, int может иметь ширину 36 бит.
  • Есть разные способы написания целочисленного типа. К примеру, в каждой следующей строке перечислен набор синонимов:
    • int, signed, signed int, int signed;
    • short, short int, short signed, short signed int;
    • unsigned long long, long unsigned int long, int long long unsigned.


Типы из стандартных библиотек


  • size_t (определен в stddef.h) является беззнаковым и содержит не менее 16 бит. При этом не гарантируется, что его ширина будет как минимум равна int.
  • ptrdiff_t (определен в stddef.h) является целочисленным типом со знаком. Вычитание двух указателей будет давать этот тип. При этом не стоит ожидать, что вычитание двух указателей даст int.
  • В stdint.h определена конкретная ширина типов: uint8_t, int8_t, 16, 32 и 64. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t, как можно было предположить.


Преобразования


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

Как происходит преобразование?

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

Говоря конкретнее:

  • Когда исходный тип расширяется до целевого типа с аналогичной знаковой характеристикой (например, signed char -> int или unsigned short -> unsigned long), каждое исходное значение после преобразования сохраняется.
  • Даже если исходный и целевой типы имеют разные диапазоны, все значения в их пересекающейся части будут сохранены. Например, int, содержащий значение в диапазоне [0, 255], будет без потерь преобразован в unsigned char.

В более точной форме эти правила звучат так:

  • При преобразовании в беззнаковый тип новое значение равняется старому значению по модулю 2целевая ширина в битах. Объяснение:
    • Если исходный тип беззнаковый и шире целевого, тогда старшие биты отбрасываются.
    • Если исходный тип имеет знак, тогда в процессе преобразования берется исходное значение, и из него/к нему вычитается/прибавляется 2целевая ширина в битах до тех пор, пока новое значение не впишется в диапазон целевого типа. Более того, если число со знаком представлено в дополнительном коде, то в процессе преобразования старшие биты отбрасываются, как и в случае с беззнаковыми числами.

  • В случае преобразования в тип со знаком случаи могут быть такими:
    • Если исходное значение вписывается в диапазон целевого типа, тогда процесс преобразования (например, расширение знака) производит целевое значение, семантически равное исходному.
    • Если же оно не вписывается, тогда поведение будет определяться реализацией и может вызвать исключение (к примеру, прерывание из-за переполнения).


Арифметика


Продвижение/преобразование


  • Унарный арифметический оператор применяется только к одному операнду. Примеры: -, ~.
  • Бинарный оператор применяется к двум операндам. Примеры: +, *, &. <<.
  • Если операнд имеет тип bool, char или short (как signed, так и unsigned), тогда он продвигается до int (signed), если int может содержать все значения исходного типа. В противном случае он продвигается до unsigned int. Процесс продвижения происходит без потерь. Примеры:
    • В реализации присутствуют 16-битный short и 24-битный int. Если переменные x и y имеют тип unsigned short, то операцияx & y продвигает оба операнда до signed int.
    • В реализации присутствуют 32-битный char и 32-битный int. Если переменные x и y имеют тип unsigned char, то операцияx – y продвигает оба операнда до unsigned int.

  • В случае двоичных операторов оба операнда перед арифметической операцией неявно преобразуются в одинаковый общий тип. Ранги преобразования возрастают в следующем порядке: int, long, long long. Рангом общего типа считается старший ранг среди типов двух операндов. Если оба операнда являются signed/unsigned, то их общий тип будет иметь ту же характеристику. Если же операнд с беззнаковым типом имеет старший или равный ранг по отношению ко второму операнду, то их общий тип будет беззнаковым. В случае, когда тип операнда со знаком может представлять все значения другого типа операнда, общий тип будет иметь знак. В противном случае общий тип получается беззнаковым. Примеры:
    • (long) + (long) → (long);
    • (unsigned int) * (int) → (unsigned int);
    • (unsigned long) / (int) → (unsigned long);
    • если int является 32-битным, а long 64-битным: (unsigned int) % (long) → (long);
    • если int и long оба являются 32-битными: (unsigned int) % (long) → (unsigned long).


Неопределенное поведение


Знаковое переполнение:

  • При выполнении арифметических операций над целочисленным типом переполнение считается неопределенным поведением (UB). Такое поведение может вызывать верные, несогласованные и/или неверные действия как сразу, так и в дальнейшем.
  • При выполнении арифметики над беззнаковым целым (после продвижений и преобразований) любое переполнение гарантированно вызовет оборот значения. Например, UINT_MAX + 1 == 0.
  • Выполнение арифметики над беззнаковыми целыми фиксированного размера может привести к едва уловимым ошибкам. Например:
    • Пусть uint16_t = unsigned short, и int равен 32-битам. Тогда uint16_t x=0xFFFF, y=0xFFFF, z=x*y; x и y будут продвинуты до int, и x * y приведет к переполнению int, вызвав неопределенное поведение.
    • Пусть uint32_t = unsigned char, и int равен 33-битам. Тогда uint32_t x=0xFFFFFFFF, y=0xFFFFFFFF, z=x+y; x и y будут продвинуты до int, и x + y приведет к переполнению int, то есть неопределенному поведению.
    • Чтобы обеспечить безопасную арифметику с беззнаковыми целыми, нужно либо прибавить 0U, либо умножить на 1U в качестве пустой операции. Например: 0U + x + y или 1U * x * y. Это гарантирует, что операнды будут продвинуты как минимум до ранга int и при этом останутся без знаков.


Деление/остаток:

  • Деление на нуль и остаток с делителем нуля также относятся к неопределенному поведению.
  • Беззнаковое деление/остаток не имеют других особых случаев.
  • Деление со знаком может вызывать переполнение, например INT_MIN / -1.
  • Остаток со знаком при отрицательных операндах может вызывать сложности, так как некоторые части являются однообразными, в то время как другие определяются реализацией.

Битовые сдвиги:

  • Неопределенным поведением считается битовый сдвиг (< < и >>) на размер, который либо отрицателен, либо равен или больше битовой ширины.
  • Левый сдвиг беззнакового операнда (после продвижения/преобразования) считается определенным правильно и отклонений в поведении не вызывает.
  • Левый сдвиг операнда со знаком, содержащего неотрицательное значение, вследствие которого 1 бит переходит в знаковый бит, является неопределенным поведением.
  • Левый сдвиг отрицательного значения относится к неопределенному поведению.
  • Правый сдвиг неотрицательного значения (в типе операнда без знака или со знаком) считается определенным правильно и отклонений в поведении не вызывает.
  • Правый сдвиг отрицательного значения определяется реализацией.

Счетчик цикла


Выбор типа


Предположим, что у нас есть массив, в котором нужно обработать каждый элемент последовательно. Длина массива хранится в переменной len типа T0. Как нужно объявить переменную счетчика цикла i типа T1?

  • Самым простым решением будет использовать тот же тип, что и у переменной длины. Например:

uint8_t len = (...);
for (uint8_t i = 0; i < len; i++) { ... }
  • Говоря обобщенно, переменная счетчика типа T1 будет работать верно, если диапазон T1 будет являться (не строго) надмножетсвом диапазона T0. Например, если len имеет тип uint16_t, тогда отсчет с использованием signed long (не менее 32 бит) сработает.
  • Говоря же более конкретно, счетчик цикла должен просто покрывать всю фактическую длину. Например, если len типа int гарантированно будет иметь значение в диапазоне [3,50] (обусловленное логикой приложения), тогда допустимо отсчитывать цикл, используя char без знака или со знаком (в котором однозначно можно представить диапазон [0,127]).
  • Нежелательно использовать переменную длины и переменную счетчика с разной знаковостью. В этом случае сравнение вызовет неявное сложное преобразование, сопровождаемое характерными для платформы проблемами. К примеру, не стоит писать такой код:

size_t len = (...);  // Unsigned
for (int i = 0; i < len; i++) { ... }

Отсчет вниз


Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:

for (int i = len - 1; i >= 0; i--) {
    process(array[i]);
}

При этом для беззнакового счетчика код будет таким:

for (unsigned int i = len; i > 0; i--) {
    process(array[i - 1]);
}

Примечание: сравнение i >= 0 имеет смысл только, когда i является числом со знаком, но всегда будет давать true, если оно будет беззнаковым. Поэтому, когда это выражение встречается в беззнаковом контексте, значит, автор кода скорее всего допустил ошибку в логике.

Заблуждения


Все пункты приведенного ниже списка являются мифами. Не опирайтесь на эти ложные убеждения, если хотите писать корректный и портируемый код.

  • char всегда равен 8 битам. int всегда равен 32 битам.
  • sizeof(T) представляет число из 8-битных байтов (октетов), необходимых для хранения переменной типа T. (Это утверждение ложно, потому что если, скажем, char равняется 32 битам, тогда sizeof(T) измеряется в 32-битных словах).
  • Можно использовать int в любой части программы и игнорировать более точные типы вроде size_t, uint32_t и т.д.
  • Знаковое переполнение гарантированно вызовет оборот значения. (например, INT_MAX + 1 == INT_MIN).
  • Символьные литералы равны их значениям в коде ASCII, например ‘A’ == 65. (Согласно EBCDIC это утверждение ложно).
  • Преобразование указателя в int и обратно в указатель происходит без потерь.
  • Преобразование {указателя на один целочисленный тип} в {указатель на другой целочисленный тип} безопасно. Например, int *p (…); long *q = (long*)p;. (см. каламбур типизации и строгий алиасинг).
  • Когда все операнд(ы) арифметического оператора (унарного или бинарного) имеют беззнаковые типы, арифметическая операция выполняется в беззнаковом режиме, никогда не вызывая неопределенного поведения, и в результате получается беззнаковый тип. Например: предположим, что uint8_t x; uint8_t y; uint32_t z;, тогда операция x + y должна дать тип вроде uint8_t, беззнаковый int, или другой разумный вариант, а +z по-прежнему будет uint32_t. (Это не так, потому что при продвижении типов предпочтение отдается типам со знаком).


Моя критика


  • Если вкратце, то знание и постоянное использование всех этих правил сильно нагружает мышление. Допущение же ошибки в их применении приводит к риску написания неверного или непортируемого кода. При этом такие ошибки могут как всплыть сразу, так и таиться в течение дней или даже долгих лет.
  • Сложности начинаются с битовой ширины базовых целочисленных типов, которая зависит от реализации. Например, int может иметь 16, 32, 64 бита или другое их количество. Всегда нужно выбирать тип с достаточным диапазоном. Но иногда использование слишком обширного типа (например, необычного 128-битного int) может вызвать сложности или даже внести уязвимости. Усугубляется это тем, что такие типы из стандартных библиотек, как size_t, не имеют связи с другими типами вроде беззнакового int или uint32_t; стандарт позволяет им быть шире или уже.
  • Правила преобразования совершенно безумны. Что еще хуже, практически везде допускаются неявные преобразования, существенно затрудняющие аудит человеком. Беззнаковые типы достаточно просты, но знаковые имеют очень много допустимых реализаций (например, обратный код, создание исключений). Типы с меньшим рангом, чем int, продвигаются автоматически, вызывая труднопонимаемое поведение с диапазонами и переполнение. Когда операнды отличаются знаковостью и рангами, они преобразуются в общий тип способом, который зависит от определяемой реализацией битовой ширины. Например, выполнение арифметики над двумя операндами, как минимум один из которых имеет беззнаковый тип, приведет к преобразованию их обоих либо в знаковый, либо в беззнаковый тип в зависимости от реализации.
  • Арифметические операции изобилуют неопределенным поведением: знаковое переполнение в add/sub/mul/div, деление на нуль, битовые сдвиги. Несложно создать такие условия неопределенного поведения по случайности, но сложно вызвать их намеренно или обнаружить при выполнении, равно как выявить их причины. Необходима повышенная внимательность и усилия для проектирования и реализации арифметического кода, исключающего переполнение/UB. Стоит учитывать, что впоследствии становится сложно отследить и исправить код, при написании которого не соблюдались принципы защиты от переполнения/UB.
  • Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые.
  • Ни в одном другом передовом языке программирования нет такого числа правил и подводных камней касательно целочисленных типов, как в С и C++. Например:
    • В Java целые числа ведут себя одинаково в любой среде. В этом языке определено конкретно 5 целочисленных типов (в отличие от C/C++, где их не менее 10). Они имеют фиксированную битовую ширину, практически все из них имеют знаки (кроме char), числа со знаком должны находиться в дополнительном коде, неявные преобразования допускают только их варианты без потерь, а вся арифметика и преобразования определяются точно и не вызывают неоднозначного поведения. Целочисленные типы в Java поддерживают быстрое вычисление и эффективное упаковывание массивов в сравнении с языками вроде Python, где есть только bigint переменного размера.
    • Java в значительной степени опирается на 32-битный тип int, особенно для перебора массивов. Это означает, что этот язык не может эффективно работать на малопроизводительных 16-битных ЦПУ (часто используемых во встраиваемых микроконтроллерах), а также не может непосредственно работать с большими массивами в 64-битных системах. К сравнению, C/C++ позволяет писать код, эффективно работающий на 16, 32 и/или 64-битных ЦПУ, но при этом требует от программиста особой осторожности.
    • В Python есть всего один целочисленный тип, а именно signed bigint. В сравнении с C/C++ это сводит на нет все рассуждения на тему битовой ширины, знаковости и преобразований, так как во всем коде правит один тип. Тем не менее за это приходится платить низкой скоростью выполнения и несогласованным потреблением памяти.
    • В JavaScript вообще нет целочисленного типа. Вместо этого в нем все выражается через математику float64 (double в C/C++). Из-за этого битовая ширина и числовой диапазон оказываются фиксированными, числа всегда имеют знаки, преобразования отсутствуют, а переполнение считается нормальным.
    • Язык ассемблера для любой конкретной машинной архитектуры (x86, MIPS и т.д.) определяет набор целочисленных типов фиксированной ширины, арифметические операции и преобразования – с редкими случаями неопределенного поведения или вообще без них.


Дополнительная информация (англ.)




RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 99

    –1
    Если Вы не пишете аппаратно-зависимый софт, то этого всего Вы скорее всего не встретите. Стандартные библиотеки написаны максимально безопасным способом, чтобы при обычном использовании не возникало описанных проблем.
    А если Вы делаете что-то на уровне битового представления типов, то обычно это делаете под конкретную платформу, либо реализуете это с применением приемов адаптации под другие платформы.
      +11
      Ну в целом да, всякие 32-битные char'ы и 33-битные int'ы в повседневной жизни встретить довольно сложно, но про всякие подводные камни типа «перемножение двух беззнаковых типов размером меньше чем int способно привести к signed overflow из-за integer promotion» знать все-таки нужно, такое вполне может встретиться в реальной жизни.
        +1
        Все эти неприятности возникают при переносе кода, особенно с какого-нить старого 16-ти битного арма на 64-х битный.
          0
          А что за 16-битный ARM? Может 26-битный? Но там вроде никаких особых страстей всё равно не было.
          +5
          то этого всего Вы скорее всего не встретите

          Ну некоторые моменты никак библиотекой не спрячешь. Например, в результате вызова std::abs можно получить отрицательное число на любой платформе. А уж сколько было уязвимостей из-за целочисленное переполнения и сколько еще будет.

            0
            Например, в результате вызова std::abs можно получить отрицательное число на любой платформе
            Пограничный случай с -128/127 (и другие пределы) или что-то более интересное?
              0
              Хм, что может быть Вам интересно, как раз для std::abs(INT8_MIN) и std::abs(INT16_MIN) не просто так выявить их отрицательную сущность, ни в на печати, ни в сравнениях. Ввиду изложенного в статье «std::cout << std::abs(INT8_MIN)» напечатает «128»! А вот «std::cout << std::abs(INT64_MIN)» напечатает отрицательное число.
                0
                Ну то есть речь идёт всё же ровно об одном пограничном случае м xxx_MIN. ОК.
                  0
                  Проблема не столько в пограничном случае, хотя кто-то может быть искренне уверен в assert(std::abs(zz) >= 0), а это зависит.

                  Сколько ещё в нескольких тонкостях. Скажем, Вы понимаете, что и почему напечатает: ` std::cout << std::abs(INT8_MIN) << std::abs(INT16_MIN) << std::abs(INT32_MIN) << std::abs(INT64_MIN) '?
                    0

                    Я, к своему стыду, абсолютно не понимаю. Можно ткнуть носом, что почитать для понимания?

                      0
                      C++20: 26.8.2 Absolute values
                      C17: 7.22.6 Integer arithmetic functions
                      C17: 7.20.2.1 Limits of exact-width integer types
                      C++20: 6.8.4 Integer conversion rank
                      C++20: 7.3.6 Integral promotionsп

                      Первое значение «std::cout << std::abs(INT8_MIN)» — строго полностью определённое поведение, последнее значение «std::cout << std::abs(INT64_MIN)», на подавляющем числе систем, строго неопределённое поведение.
                      0
                      Вы понимаете, что и почему напечатает: ` std::cout << std::abs(INT8_MIN) << std::abs(INT16_MIN) << std::abs(INT32_MIN) << std::abs(INT64_MIN)
                      Да, понимаю, потому что базовый тип для std::abs — int и первые два значения будут расширены до него.
                      Но, опять же, эксплуатация ровно одного пограничного случая.
                        0
                        Хм, да, понимаете? А это ничего, что INT8_MIN и INT16_MIN — имеют тип int? А std::abs() перегружен по трём типам аргумента int/long/long long? В C++/C, увы, трудно быть хоть в чём-то полностью уверенным.

                        А так, да, С++20 ссылается на C17, а в C17 для abs(), labs() и llabs(), прописано, что если результат не может быть представлен, то поведение неопределённое (с отдельным напоминанием за наименьшее целое, в случае, дополнительной арифметики). Ну, а в других функциях могут быть свои заморочки.
                          0
                          Хм, да, понимаете? А это ничего, что INT8_MIN и INT16_MIN — имеют тип int? А std::abs() перегружен по трём типам аргумента int/long/long long?
                          То есть ровно то, что я и написал. Вы опять обсуждаете ровно один пограничный случай.
                0
                Насчёт любой, это ж вряд ли, ещё существуют калькуляторы без дополнительного кода.
                  0
                  А C-то на них существует? Насколько я знаю C поддерживается только на горсточке очень новых калькуляторов и там везде 32-битные процессоры. ARM, MC68000, SH-3
                    0
                    В каком-нибудь военном/ядерном/авиационном/космическом вычислителе можно нарваться на арифметику с поглощением (а ля x86/x64 SIMD/OpenCL) или на модульную арифметику для быстрых вариантов целых типов.

                    Или, как вариант, какая-нибудь военная/ядерная/авиационная/космическая libc выдаст иной вариант неопределённого поведения, отличный от типичного std::abs(INT64_MIN) == llabs(INT64_MIN) == INT64_MIN
                      +1
                      А, вы сюжет для фантастического романа обсуждаете, не реальный мир.

                      Я, в принципе, не против фантастики, но предпочитаю её обсуждать в отдельных темах.
                        0
                        В стандарте написано — неопределённое поведение, а неопределённое поведение это не только отрицательное значение std:abs(). Если бы всё было бы так однозначно, в стандарте бы так и написали: llabs(INT64_MIN) == INT64_MIN. С арифметикой — аналогично.

                        Я вполне себе могу представить компилятор, который операции с int16_t не векторизует, т.к. стандарт требует для них дополнительной арифметики, а int_fast16_t векторизует.
                          0
                          Причём тут std:abs, извините? Представление чисел важно не только для std:abs, но и для банального a = b;. Тут нет неопределённого поведения при любом целочисленном a и любом целочисленном b.
                            +1
                            Ну, началось же с: «Ну некоторые моменты никак библиотекой не спрячешь. Например, в результате вызова std::abs можно получить отрицательное число на любой платформе.» от apro

                            Так же, разрешите напомнить, что в стандарте C17, на который ссылается C++20, указано «The abs, labs, and llabs functions compute the absolute value of an integer j. If the result cannot be represented, the behavior is undefined

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

                            Кроме того, какие фокусы, бывает, выкидывают современные оптимизирующие компиляторы для программ с неопределённым поведением, наверное, не мне Вам рассказывать.
                        0
                        std::abs(INT64_MIN) == llabs(INT64_MIN) == INT64_MIN
                        это вы ещё сравнивать вещёственный числа не пробовали видимо :)
                        float a = 3.5;
                        float b = 2.0 + 1.5;
                        if (a == b) { ... // <-  так сравнивать нельзя
                          0
                          Это почему это нельзя?! Для кого IEEE-754 (ISO/IEC/IEEE 60559) пишут? В данном случае можно, т.к. 1.5, 2.0 и 3.0 имеют точное представление. Конечно, по хорошему надо поставить static_assert на __STDC_IEC_559__/__GCC_IEC_559 и несколько ещё вариантов.

                          Эх, было время, я длинное целое умножение/деление на плавающем сопроцессоре делал.

                          Знать как нельзя, это действительно большое достижение, многие даже и не знают, но некоторым бывает интересен ответ на вопрос почему.
                            0
                            Опять рассматриваем частный случай? ОК, даже если так, перефразирую вопрос, почему следующая функция не корректна
                            bool is_equal( float a, float b ) {
                            return ( a == b );
                            }
                              0
                              Конечно, для C/C++ немного странная функция, аргументы float, но зависит от программы.

                              И нет, и это не частный, случай. В предыдущем тезисе Вы, по-моему, так и не выделили три различные проблемы, а свалили всё в одну кучу.

                              А здесь, так это вообще множество общих случаев.

                              Просто к ошибкам округления, как и ко всему остальному, стоит подходить с пониманием. Есть алгоритмы, когда их можно считать случайными. Есть алгоритмы в которых их следует учитывать для правильного выбора констант разложений и т.п. (0.125 — точно, 0.1 — неточно, /10 — точно). Есть алгоритмы, где они «самоуничтожаются» (скажем, Кнут т. 2, s = a + b; e = b — (s — a);). А есть алгоритмы где их вообще не возникает.
                                0
                                Понятно, теоретик. Так и запишем.
                                  0
                                  Хм, художника всякий обидеть норовит?! Я бы это назвал, практик, вычислитель, вычислительная математика она ж такая. У меня есть все виды перечисленных выше программ, которые работают много много лет.

                                  И да, в юнит-тестах, регрессионных тестах, а так же в тестах самотестирования на объекте, double сравниваются на равенство. Что б поймать за руку какого-нибудь молодого, да раннего, инженера по сборке, если он накосячит. Или сторонний модуль, если он что-то в настройках FPU изменит.
                                    0
                                    Я бы это назвал, практик, вычислитель, вычислительная математика она ж такая
                                    Математик-теоретик, не программист-практик, уж извините :)
                                    И да, в юнит-тестах, регрессионных тестах, а так же в тестах самотестирования на объекте, double сравниваются на равенство

                                    github.com/google/googletest/blob/master/docs/advanced.md#floating-point-comparison
                                    они ссылаются сюда
                                    randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition
                                    Уверен, что вы читали эту статью или подобную, но ответ:
                                    к ошибкам округления, как и ко всему остальному, стоит подходить с пониманием. Есть алгоритмы, когда их можно считать случайными. Есть алгоритмы в которых их следует учитывать для правильного выбора констант разложений и т.п. Есть алгоритмы, где они «самоуничтожаются». А есть алгоритмы где их вообще не возникает.
                                    как раз и написал математиком-теоретиком, а не программистом-практиком, или прагматиком, если хотите.
                                      0
                                      Я не хочу никого обидеть, но это мурзилки для тех, кто прогуливал уроки в школе. Которые, к тому же, желают вместо понимания IEEE-754, С17, С++20, неких магических ритуалов.

                                      Грубо говоря,
                                      for(i = 0; i < sqrt(n); i++)
                                          a[i*i] = 1.;
                                      

                                      Совершенно не требует «улучшайзинга». ;) Если в контексте уместно, то вместо «i < sqrt(n)» столь же корректно написать «i != sqrt(n)» ;) Если n < 252, конечно, но компьютеры с памятью больше 255, есть только у китайцев. ;) И таких примеров вагон и маленькая тележка.
                                0

                                Сравнивать на равенство нужно, например, чтобы определить, надо ли сериализить значение или оно дефолтное.

                                  0
                                  вещественные числа сравнивать на равенство нельзя, в общем случае.
                                  (a — b) < epsion
                                    0
                                    Хм, а ещё станцевать танец отпугивания злых духов и трижды перекреститься. (Да, std::abs() ещё ж забыли...) И получить, в результате, некорректный код.

                                    Заметьте: «или оно дефолтное»!

                                    Нет, ну если процессорного времени вагон и маленькая тележка? Почему нет? Но Роскосмос, НАСА, как и Илон Маск, лично, не одобрят.
                  –3
                  В начале перечисляются стандартные типы
                  char: минимум 8 бит в ширину;
                  short: минимум 16 бит и при этом не меньше char;
                  int: минимум 16 бит и при этом не меньше short;
                  long: минимум 32 бит и при этом не меньше int;
                  long long: минимум 64 бит и при этом не меньше long.
                  но ни слова про уже, практически столь же стандартные (u)int8_t/(u)int_16_t и т.д.
                  И позже, внезапно:
                  uint8_t len = (...);
                  for (uint8_t i = 0; i < len; i++) { ... }
                  что? откуда взялось uint8_t если до этого про этот тип ни слова?
                  Пусть uint32_t = unsigned char, и int равен 33-битам.
                  Откуда взялся теоретик ни когда не слышавший о моделях ILP32, LLP64, LP64? Экзотика, конечно встречается, но и тогда есть определённый ряд соглашений, иначе ни одну библиотеку под эту платформу портировать не удастся, а, скажем, писать реализацию TLS 1.3/1.4 самостоятельно вряд ли кто захочет.
                    +5
                    В stdint.h определена конкретная ширина типов: uint8_t, int8_t, 16, 32 и 64. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t, как можно было предположить.


                    Вы не очень внимательно читаете
                      0
                      Это всё же не перечисление типов, это именно рояль в кустах ещё и с кривым определением
                      This header defines a set of integral type aliases with specific width requirements
                      +2

                      uint8_t может быть не определён, например, на процессоре с минимальным адресуемым словом в 16 бит. Но тип, скажем, uint_fast8_t всегда есть

                      +10
                      Хороший обзор. Но для полноты стоило бы упомянуть нюансы little-endian vs big-endian.
                        +2
                        Насколько я понял автора, посыл в том, что C — это не кроссплатформенный ассемблер с единым синтаксисом. Тут полностью согласен. А реально с уверенностью рассчитывать можно только на то, что один бит всегда принимает значения «0» и «1» но это это не точно
                          +1
                          Преобразование указателя в int и обратно в указатель происходит без потерь
                          На x64, ожидаемо указатели 64 битные (а int 32 битный).
                            0
                            Стандарт C допускает такие платформы, где указатель не может быть без потерь преобразован ни в один целочисленный тип (типы вида uintptr_t являются опциональными и могут отсутствовать). Думаю, автор привёл int для примера и экономии места.
                              0
                              На x64 свет клином не сошёлся
                              +1
                              Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов

                              Было дело, я сначала плевался на Java: как из-за отсутствия unsigned переносить код с с/с++? Потом восхищался C#: «ну вот там же сделали, всё для людей!»
                              Потом понял: те операции, для которых (тогда мне было) это важно — они вообще не про арифметику были, а про AND, OR, XOR, где вообще понятия «знаковости» нет. Но в коде, который довелось переносить — почему-то через "+" и "-" многое было сделано.
                                +2
                                Спасибо! Очень интересно было узнать про то что char бывает разного размера. Мне char больше чем 8 бит никогда не встречался.
                                  +3
                                  Посмотрите на TMS320C3x/4x или 1867ВЦ6Ф. Там все типы 32 бита (за небольшим исключением). :)
                                    +2
                                    надо же, у TMS320C3x/4x даже double 40 бит
                                    0
                                    Так ведь char уже может быть utf (а там как раз до 32 бит)?
                                      +3
                                      Имеется в виду, что мне не встречалось такое устройство, чтобы в <limits.h> написанном под него значение CHAR_BIT было равно любому числу кроме 8. Так уж сложилось, что слово character в стандарте языка C и character в стандартах описывающих UTF — обозначает совсем разные вещи.
                                      0

                                      Старые 36-битные мейнфреймы имели char 9 бит

                                      +2

                                      А как int может быть шириной 33 бита, если ранее вы пишете


                                      Битовая ширина должна быть кратна 2.

                                      ?

                                        +3
                                        Я полагаю, что автор исходного текста хотел таким образом привлечь внимание к проблеме. Но просто сам себе противоречит в размере. Примите в данном примере ширину 34-м битам, смысл останется тот же.
                                        –3
                                        Хочу отметить, что на КДПВ не С++.
                                            +2
                                            Судя по string он и есть.
                                              0
                                              Глаз-алмаз, я прошляпил. Но судя по остальному коду, это писал сишник на Си, просто он освоил немного стандартной библиотеки С++ :) Особенно объявления всех переменных без иницализации вверху скоупа радуют мой глаз.
                                                0
                                                Слава богу вверху скоупа даже в C объявлять переменные больше не нужно. Правда под Windows придётся взять довольно новую Visual Studio. Microsoft с переходом с C89 на более новые стандарты чуть-чуть затянул.

                                                Но перешли же?
                                            0
                                            До сих пор впадаю в ступор, когда вижу код наподобие
                                            unsigned long long = -1;
                                            Я ведь правильно понимаю, что это эквивалентно
                                            unsigned long long = -1ull;
                                            ?
                                            Тоесть если к результирующему значению прибавить 1, то получится 0?
                                            В статье об этом сказано, но не очень понятно
                                              +1
                                              я думаю, что в случае с unsigned long long = -1 сначала -1 продвигается до signed int и дальше преобразовывается к типу unsigned long long для этого скорее всего будет прибавлен LLONG_MAX + 1. Однако, это не эквивалентно unsigned long long = -1ull. Так как в этом случае правая часть уже имеет тип unsigned long long.
                                                +2
                                                unsigned long long = -1;
                                                Просто общепринятый трюк с переполнением для получения 0xFFFF…
                                                Я ведь правильно понимаю, что это эквивалентно
                                                unsigned long long = -1ull;
                                                Результат — да, преобразования — нет.
                                                  +1

                                                  Это определено стандартом. signed-to-unsigned implicit conversion.


                                                  Although the definition uses -1, the value of unsigned type is the largest positive value it can hold, due to signed-to-unsigned implicit conversion. This is a portable way to specify the largest value of any unsigned type.
                                                    +3

                                                    Кстати, автор в 4 пункте раздела "Заблуждения" остерегает использовать этот трюк.
                                                    Ни разу ни плюсовик, но часто сталкиваюсь с тем что приходится спорить даже с "продвинутыми" плюсовиками.
                                                    Из недавнего, спор о длине(размере) char. Ни один из трёх не знал что стандарт не гарантирует длину в 8 бит. Прямо как откровение. И так с каждым положением.
                                                    Могу сделать совет начинающим вайтишникам — не начинайте учить плюсы и джаваскрипт если вам дорого ваше психическое здоровье, и вы хотите быть уверенным до конца хоть в чём то.


                                                    ПС А ведь в статье слегка освещены проблемы только с целочисленными типами. А сколько там граблей с вещественными числами — мама не горюй...

                                                      0
                                                      Ни один из трёх не знал что стандарт не гарантирует длину в 8 бит.
                                                      Тоже мне проблема. Пишем:
                                                      [[maybe_unused]] std::int8_t we_need_8bit_char;
                                                      

                                                      Всё, стандарт резко загарантировал длину 8 бит.
                                                        0
                                                        Может быть минусующие объяснят, что им не по нраву?

                                                        Стандарт действительно гарантирует 8битный char, на платформах, где вышеописанный код скомпилируется.

                                                        И, соотвественно, любой код, в котором есть хотя бы одна переменная типа std::int8_t — тоже может полагаться на 8-битный char.

                                                        В чём проблема-то?
                                                          0
                                                          Проблема, думаю, в разном понимании «гарантирует длину в 8 бит». Ваш код блокирует компиляцию на некоторых, редких, платформах. А товарищ, вероятно, имел ввиду, что его собеседники испытывают затруднения при написании работающего переносимого кода и для этих платформ тоже.
                                                    0
                                                    Это вам не Javascript!
                                                      +2
                                                      Эх, это вы еще не перечислили проблемы с системами построенными не на двоично-дополнительном коде.
                                                      Вот скажем «классический пример»:
                                                      switch (-1&3)
                                                      { case 1: /* ... */ break;
                                                      case 2: /* ... */ break;
                                                      case 3: /* ... */ break;
                                                      }
                                                      

                                                      когда сработают ветки case 1 и case 2?
                                                        0
                                                        Только вот таких систем, похоже, в мире просто не осталось. Еще в 2012 году на StackOverflow на вопрос а остались ли на практике такие системы смогли лишь найти один чрезвычайно частный случай со своим собственным компилятором и отметить, что системы не на дополнительном коде скорее всего успели выйти из активного употребления еще до стандартизации С.

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

                                                        stackoverflow.com/questions/12276957/are-there-any-non-twos-complement-implementations-of-c
                                                          0
                                                          Надеюсь всё-таки P0907 примут и проблема останется в прошлом.

                                                            0
                                                            Да, именно архитектур почти не осталось. Например: руководство по С для unisys 2200.

                                                            Но можно посмотреть на вопрос немного шире. Например, числа с плавающей точкой хранятся не в двоично-дополнительном коде, но тем не менее операции над ними иногда делают, приводя их к целочисленным ради быстродействия, поэтому помнить о других возможных представлениях надо.
                                                          +3

                                                          Ещё один момент забыт. Иногда это бывает важно — может не так часто, но в любом случае знать стоит. signed int и int — это один и тот же тип, signed long и long — тоже и т. д., но вот char, signed char и unsigned char — это три разных типа.

                                                            +2
                                                            char, signed char и unsigned char — это три разных типа.
                                                            Лет 10 назад и (un)signed bool работало, и сейчас может сработать для чистого Си, где bool объявлен как какой-нибудь char или int :)
                                                              0
                                                              Так речь о «чистом Си», или о конкретных реализациях?
                                                                0
                                                                в «чистом Си» до C99 типа bool вообще не было, поэтому до сих пор в легаси коде С89/90 встречается определение bool как int и, соответственно, занятные моменты, связанные с этим. В MSVC до версии 2008 unsigned bool был валидным типом. Про GCC не помню, там всё достаточно хорошо было организовано уже очень давно.
                                                                  0
                                                                  типа bool и после C99 нет. bool это просто макрос который соответствует типу _Bool. Если компилятор тип _Bool не определяет, то он будет определён как int.
                                                                  Если говорить о легаси до C99, то там bool ваще как что угодно может быть определён. я и как enum встречал.
                                                            +1
                                                            > for (unsigned int i = len; i > 0; i--) {
                                                            > process(array[i — 1]);
                                                            > }

                                                            for (unsigned int i = len; i-- > 0;) {
                                                            process(array[i]);
                                                            }
                                                              +1

                                                              Самое забавное, что комитет только к C++20 выкатил ssize() и только к C++23 родил суффиксы для ptrdiff_t и size_t (и наотрез отказывается вводить ssize_t).
                                                              Скоро никаких static_cast'ов, надо только подождать...

                                                                0
                                                                Всё актуальное лежит в stdint.h, оно прям цветом выделяется в нормальной ide. И читать удобно, и работать будет везде. Для экзотики типа десятичных с плавающей точкой — есть отдельные библиотеки, и до них вы сами пешком дойдёте, когда прижмёт.
                                                                А всё остальное можно признать морально устаревшим, и просто выбросить.
                                                                  +4
                                                                  Да уж, ситуация печальная. Стандартом языка расставлено множество граблей, а всем, кто не желает по ним ходить, лепят ярлык некомпетентных, неаккуратных, ленивых и т.д.

                                                                  Недавно я делал 96-битную арифметику на стандартном Си. Только сложение и вычитание. Сложение получилось быстро, с вычитанием пришлось долго повозиться. Затраты времени на это оказались в десятки раз выше, чем на написание соответствующего кода на ассемблере.
                                                                    –1
                                                                    Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые.


                                                                    Если все корректно работает, то умственной нагрузки нет. Ведь правило простое: если число может быть отрицательным, то signed. Просто, не правда ли? Проблема в другом.
                                                                      0

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

                                                                        0
                                                                        Ведь правило простое: если число может быть отрицательным, то signed. Просто, не правда ли?
                                                                        Все так, если бы этому правилу всегда следовали. А то ведь time_t, например, знаковый, с той целью, чтобы функции, возвращающие time_t значения, могли вернуть -1 в случае ошибки. А был бы он беззнаковым, могли бы не париться про 2038 год.
                                                                        –4
                                                                        современный с++ дегенерат-вырожденец, хотите «умные» указатели, не е… те людям мозги, возьмите c#, java и т.п. А если используете variadic темплейты, лучше ждите технологическую сингулярность.
                                                                          0
                                                                          Как Вы жестко с C++, мне кажется, это уж перебор
                                                                            0

                                                                            Почему не Rust?

                                                                            –1
                                                                            Дарован был людям Раст, нет, будем сотни страниц со списками особенностей приведения и неопределённого поведения держать в голове. А потом в Гугл заявляют, что у них 70% ошибок из-за ошибок в работе с памятью. Если ещё переполнение и все эти UB добавить, то смело можно говорить о 90%.

                                                                            Боюсь, лет через 5 С++ будет эдаким Паскалем/Делфи, который по инерции вроде ещё изучается и даже используется в старых проектах, но новых практически нет.
                                                                              0
                                                                              Ну, с растом тоже многое нужно держать в голове, например то, что len() в итераторе может теоретически вернуть что угодно, а если этого не держать в голове, то могут случиться ошибки в работе с памятью внезапно. Никакой раст не спасет от необходимости думать при написании кода и внимательно читать документацию, а иначе проблемы неизбежны — хоть с памятью, хоть с чем.
                                                                                +1
                                                                                Это относится, во-первых, к сторонней библиотеке, во-вторых, это ошибка, а не стандартизированное поведение, ну и под него уже подстелили соломку. Ну и вон целый комитет есть, который следит за безпастностью сторонних либ. Даже не знал про него.

                                                                                Разумеется, и там можно выстрелить в ногу, если постараться, но там компилятор, хоть и тормозной, даёт куда больше гарантий, а всё или почти всё, что даёт UB, разрешено только в unsafe-блоках, и это очень небольшая часть языка и стандартной библиотеки, нужная для нетривиальных случаев.
                                                                                  –1
                                                                                  Это типичная ошибка, связанная с тем, что что-то, написанное в примечании на некоей странице, на которую ведет не слишком приметная ссылка с другой страницы с описанием функции len(), разработчик сторонней библиотеки (типичный средний разработчик, который пишет или будет писать софт на расте) не держал в голове. «Все как всегда, и лучше будет едва ли» ©. А если писателей на расте станет достаточно много, то количество подобных ошибок будет увеличиваться в геометрической прогрессии.

                                                                                  Разумеется, и там можно выстрелить в ногу, если постараться, но там компилятор, хоть и тормозной, даёт куда больше гарантий, а всё или почти всё, что даёт UB, разрешено только в unsafe-блоках, и это очень небольшая часть языка и стандартной библиотеки, нужная для нетривиальных случаев.

                                                                                  Верно. Но ошибка в unsafe-блоке — это тоже ошибка. В расте ряд вещей сделан «более по уму», чем в C/C++, и так и должно быть — он не вынужден тянуть на себе бремя совместимости длиной в полсотни лет, но он от этого не становится какой-то серебряной пулей. По-прежнему нужно держать многое в голове, особенно в случае «нетривиальных случаев», да.
                                                                                    +1
                                                                                    Ошибку в unsafe легко отловить — она локализована, а остальной код проверяет компилятор, если, конечно не писать в стиле «unsafe fn main()». Если, как в Си, при разыменовании любой указатель может потенциально быть невалидным, это уже совсем другое дело. Понятно, что в современном С++ сейчас нет особенной нужды на каждом шагу жонглировать указателями, но частных случаев, неявных приведений, методов по умолчанию, и т. п., остаётся до ужаса много. Вон даже в банальной арифметике столько разных нюансов. Тут где-то на Хабре была статья бывшего плюсовика со стажем чуть ли не в 20 лет, который подробно описывал свои мысли по этому поводу.
                                                                                      –1
                                                                                      Ошибку в unsafe легко отловить

                                                                                      Относительно легко. Но думать при этом всё-таки надо. Что бы вам такое для примера привести… Например, есть такой трейт — Zero. Имплементирующий его код обязан соблюсти закон, что прибавление нуля не меняет значение, это прописано в документации. Вопрос: можете ли вы в своём unsafe коде рассчитывать на этот закон для неизвестного типа? Правильный ответ: нет.

                                                                              0
                                                                              неявные преобразования
                                                                              — это конечно самая большая неприятность

                                                                              Если бы меня спросили, что в первую очередь улучшить в С/C++, то первые в расстрельном списке это неявные преобразования между различными целыми, а также между целыми и числами с плавающей точкой…

                                                                              Тяжкое бремя совместимости висит над ним. Ну так хотя бы добавили прагму для отключения…
                                                                                +1
                                                                                Добавлю ещё один миф: int принимает разрядность целевой вычислительной архитектуры. Это неверно, потому что для 8-битной архитектуры AVR, ширина int равна 16 битам. Но это скорее сделали так, потому что у крупных ядер AVR есть инструкции, типа movw и регистровые пары, используемые в качестве указателей.
                                                                                  0
                                                                                  Но это скорее сделали так, потому что у крупных ядер AVR есть инструкции, типа movw и регистровые пары, используемые в качестве указателей.
                                                                                  И всё это приводит к тому, что понятие «разрядность вычислительной архитектуры» становится понятием не шибко определённым.

                                                                                  Мне даже никто так и не смог объяснить, почему Z80 — 8-битный, 8086 — 16-битный, а Pentium4 — 32-битный. Потому что по размерам регистров у вас Z80 получается 16-битным (там IX и IY есть и операции с ними), а по размеру ALU у вас Pentium4 окажется 16-битным (там ALU работает с половинками регистров на удвоенной частоте).
                                                                                    0
                                                                                    почему Z80 — 8-битный, 8086 — 16-битный, а Pentium4 — 32-битный. Потому что по размерам регистров у вас Z80 получается 16-битным
                                                                                    Шина адресов да, 16 бит, но данные всё равно читались по 8 бит. Такая же история и с i8088/КР580ВМ, где шина адреса тоде была 16 битной, а данные всё равно читались по 8 бит.
                                                                                    По этой причине процессоры и были 8-битными, первый полноценно 32-битный (из известных) считается i386, где все регистры были сделаны 32-битными, у его предшественника — i286, регисты шины адреса были 24-битными, а данных — 16, при этом было несколько регистров по 48 бит, 40 и даже 64.
                                                                                      0
                                                                                      Шина адресов да, 16 бит, но данные всё равно читались по 8 бит. Такая же история и с i8088/КР580ВМ, где шина адреса тоде была 16 битной, а данные всё равно читались по 8 бит.
                                                                                      Вот только Z80 — считается вершиной 8-битных процессоров, а i8088/КР580ВМ — считаются 16-битными.

                                                                                      По этой причине процессоры и были 8-битными, первый полноценно 32-битный (из известных) считается i386
                                                                                      Что значит «полноценно 32-битный», извините? А 68k куда дели?

                                                                                      Некоторые, правда, говорят, что первенец, 68000 — типа 32/16 битный, так как ALU у него 16 битное. Но вот Prescott 64/30-битным не называют. Впрочем 68020 всё равно на год раньше 80386го появился.

                                                                                      при этом было несколько регистров по 48 бит, 40 и даже 64.
                                                                                      Современные процессоры имеют регистры аж до 512 бит. Но 512-битными их никто не называет.

                                                                                      В общем битность процессора это дело такое, зависящее от документации, а не от реальной архитектуры. Потому с ней размер int и не совпадает.
                                                                                        0
                                                                                        i8088/КР580ВМ — считаются 16-битными.
                                                                                        Эмммм… нет
                                                                                        КР580ВМ80А — 8-разрядный микропроцессор.
                                                                                        ru.wikipedia.org/wiki/%D0%9A%D0%A0580%D0%92%D0%9C80%D0%90
                                                                                        С i8088 это я ошибся, КР580 был клоном i8080
                                                                                        Что значит «полноценно 32-битный», извините? А 68k куда дели?
                                                                                        А вы ремарку прочитали или сразу спорить?
                                                                                        Ни куда не дел, так как оригинальный 68к имел 16-битные разряды данных, о чём прямо даже в вики написано
                                                                                        RU: из-за чего иногда процессор описывается как имеющий смешанную битность 16/32
                                                                                        EN: Generation one (internally 16/32-bit...
                                                                                        Полноценный 32-битный 68020 вышел конечно раньше i386, на год, примерно, на нём даже был выполнен Apple II, если не ошибаюсь.
                                                                                        В общем битность процессора это дело такое, зависящее от документации, а не от реальной архитектуры
                                                                                        Об этом и речь, но ведь оригинальный вопрос был, почему Z80 не 16-битный, потому что набор команд и шина данных 8-битные, а размер регистров не определяет битность процессора.
                                                                                          0
                                                                                          С i8088 это я ошибся, КР580 был клоном i8080
                                                                                          А я только на i8088 и глянул, не обратил внимания, что вы не про КР580ВМ88.

                                                                                          Об этом и речь, но ведь оригинальный вопрос был, почему Z80 не 16-битный, потому что набор команд и шина данных 8-битные, а размер регистров не определяет битность процессора.
                                                                                          Вот только в наоборе команд есть и 8-битные и 16-битные регистры, а шина данных такая же 8-битная, как и у 8088.

                                                                                          И у того же «смешанного» 68000 регистры и система команд таки 32-битная.

                                                                                          а размер регистров не определяет битность процессора.
                                                                                          А что её определяет? Вот почему у вас 8088й резко стал 16-битным, аж даже извиняться и «отыгрывать» пришлось?

                                                                                          И почему послений, доживший до сегодня, потомок Z80, имея 24-битные регистры и ALU считается, тем не менее, 8-битным — из-за архитекуры, типа, а 68000, имеющий 32-битную архитектуру — не считается 32-битным? Хотя совместимый с ним 68020 (гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме) — полноценно 32-битный?
                                                                                            0
                                                                                            вы не про КР580ВМ88
                                                                                            я даже специально загуглил и могу ответить только цитатой с одного из форумов
                                                                                            Очень интересно, учитывая, что КР580ВМ88 не существует, а КР580ВМ80 — это i8080, с i8086/88 не совместимый.


                                                                                            Вот почему у вас 8088й резко стал 16-битным, аж даже извиняться и «отыгрывать» пришлось?

                                                                                            потому как i8088 это переделка i8086, 16-битного процессора под 8-битные микросхемы памяти, а извинялся я за опечатку, вместо 8080 я напечатал 8088.
                                                                                            а шина данных такая же 8-битная, как и у 8088
                                                                                            Тут да, но оперировал 8088 16-битными данными, а даунгрейд был сделан по техническим причинам
                                                                                            The main difference is that there are only eight data lines instead of the 8086's 16 lines. All of the other pins of the device perform the same function as they do with the 8086 with two exceptions. First, pin 34 is no longer BHE (this is the high-order byte select on the 8086—the 8088 does not have a high-order byte on its eight-bit data bus).

                                                                                            Ну то есть 8088 читал побайтно даже 16-битные данные (и писал так же), так как 8-битных чипов память наделали столько, что нужно было куда-то их реализовывать.
                                                                                            а 68000, имеющий 32-битную архитектуру — не считается 32-битным? Хотя совместимый с ним 68020 (гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме) — полноценно 32-битный?
                                                                                            Если хотите спорить ради спора, то хотя бы читайте первоисточники, хоть какие-нибудь.
                                                                                            The Motorola 68000…

                                                                                            The design implements a 32-bit instruction set, with 32-bit registers and a 16-bit internal data bus. The address bus is 24-bits and does not use memory segmentation, which made it popular with programmers. Internally, it uses a 16-bit data arithmetic logic unit (ALU) and two more 16-bit ALUs used mostly for addresses, and has a 16-bit external data bus. For this reason, Motorola termed it a 16/32-bit processor.

                                                                                            The 68020 added many improvements over the 68010 including a 32-bit arithmetic logic unit (ALU), 32-bit external data and address buses, extra instructions and additional addressing modes.
                                                                                              0
                                                                                              На это стоит ответить отдельно
                                                                                              (гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме)
                                                                                              Ну то есть появление в i386 принципиально иных режимов, как то, страничное управление памятью, защищённый режим работы процессора, который и заставил делать целый слой совместимости со старыми процессорами — виртуальный режим, это фигня и её можно сравнивать с эволюцией линейки 68k?
                                                                                              Ну то есть, ещё раз, 68020 и i386 в реальном режиме как бы да, сравнимо, но i386 в защищённом режиме это как бы совершенно отдельная история.
                                                                                                0

                                                                                                Для 68020 и 68000 нет никакого 'реального режима'. Они всегда работают с полноценными 32-битными регистрами и полноценной 32-битной адресацией (из которой только 24 бита адреса вылазят наружу для 68000, но уже все 32 бита для 68020). В противовес 8086 с ужасной 16-битной адресацией через сегменты, навёрнутом на основе этого в 80286 защищённом режиме и наконец расширении всего этого унылого безобразия до 32 бит с добавкой виртуальной страничной памяти в 80386.


                                                                                                Взамен всей этой кучи бессмысленной ерунды в 68020 просто добавили страничную MMU (внешним чипом, с 68030 MMU уже на кристалле процессора).

                                                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                                  Самое читаемое