Если число слишком большое, оно перетекает в следующую ячейку памяти?

30

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

Давайте возьмем этот код для примера:

int myArray[5] = {1, 2, 2147483648, 4, 5};
int* ptr = myArray;
int i;
for(i=0; i<5; i++, ptr++)
    printf("\n Element %d holds %d at address %p", i, myArray[i], ptr);

Я знаю, что int может содержать максимальное положительное значение 2 147 483 647. Таким образом, переходя к одному из них, он «перетекает» на следующий адрес памяти, в результате чего элемент 2 отображается по этому адресу как «-2147483648»? Но тогда это на самом деле не имеет смысла, потому что в выходных данных он по-прежнему говорит, что следующий адрес содержит значение 4, а затем 5. Если число перешло на следующий адрес, не изменит ли это значение, хранящееся по этому адресу ?

Я смутно помню из программирования в MIPS Assembly и наблюдения за изменением значений адресов во время программы, шаг за шагом, что значения, назначенные этим адресам, будут меняться.

Если я не помню неправильно, тогда возникает другой вопрос: если число, назначенное конкретному адресу, больше, чем тип (как в myArray [2]), то не влияет ли это на значения, сохраненные в последующем адресе?

Пример: у нас есть int myNum = 4 миллиарда по адресу 0x10010000. Конечно, myNum не может хранить 4 миллиарда, поэтому он выглядит как отрицательное число по этому адресу. Несмотря на невозможность сохранить это большое число, оно не влияет на значение, сохраненное по последующему адресу 0x10010004. Правильный?

Адреса памяти просто имеют достаточно места для хранения чисел / символов определенного размера, и если размер превысит лимит, он будет представлен по-другому (например, попытка сохранить 4 миллиарда в int, но будет отображаться как отрицательное число) и так что это не влияет на числа / символы, хранящиеся по следующему адресу.

Извините, если я пошел за борт. У меня от этого весь день пукнул мозг.

приземистый
источник
10
Вы можете запутаться с переполнением строки .
Робби Ди
19
Домашнее задание: Изменить простой процессор , так что это делает разлив. Вы увидите, что логика становится намного более сложной, все для «функции», которая гарантировала бы дыры в безопасности повсюду, не будучи в первую очередь полезной.
Phihag
4
Если вам нужны действительно огромные числа, возможно иметь представление чисел, которое увеличивает объем памяти, используемый для больших чисел. Сам процессор не может этого сделать, и это не особенность языка C, но библиотека может реализовать его - общая библиотека C - это арифметическая библиотека GNU Multiple Precision . Библиотека должна управлять памятью, чтобы хранить числа, которые имеют стоимость производительности помимо арифметики. Во многих языках встроены подобные вещи (что не обходится без затрат).
Steve314
1
написать простой тест, я не программист на C, но что-то вроде int c = INT.MAXINT; c+=1;и посмотреть, что случилось с c.
Джон
2
@JonH: проблема в том, что переполнение в неопределенном поведении. Компилятор AC может определить этот код и сделать вывод, что это недоступный код, потому что он безусловно переполняется. Поскольку недоступный код не имеет значения, его можно устранить. Конечный результат: кода не осталось.
MSalters

Ответы:

48

Нет. В C переменные имеют фиксированный набор адресов памяти для работы. Если вы работаете в системе с 4-байтовым кодом ints, и вы устанавливаете intпеременную, 2,147,483,647а затем добавляете 1, переменная обычно будет содержать -2147483648. (На большинстве систем. Поведение фактически не определено.) Другие области памяти не будут изменены.

По сути, компилятор не позволит вам присвоить слишком большое значение для типа. Это сгенерирует ошибку компилятора. Если принудительно указать регистр, значение будет усечено.

Посмотрите побитовым образом, если тип может хранить только 8 битов, и вы пытаетесь ввести значение 1010101010101в него с регистром, вы получите нижние 8 битов, или 01010101.

В вашем примере, независимо от того, что вы делаете myArray[2], myArray[3]будет содержать «4». Там нет "разлива". Вы пытаетесь поместить что-то более чем в 4 байта, оно просто отбросит все на верхнем уровне, оставляя нижние 4 байта. На большинстве систем это приведет к -2147483648.

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

Горт Робот
источник
52
Если вы работаете в системе с 4-байтовыми целыми числами, и вы задали для переменной int значение 2 147 483 647, а затем добавили 1, переменная будет содержать -2147483648. => Нет , это неопределенное поведение , поэтому оно может зацикливаться или делать что-то еще полностью; Я видел компиляторы, оптимизирующие проверки, основанные на отсутствии переполнения, и получил, например, бесконечные циклы ...
Матье М.
Извините, да, вы правы. Я должен был добавить «обычно» там.
Gort Робот
@MatthieuM с языковой точки зрения, это правда. С точки зрения исполнения в данной системе, о чем мы здесь говорим, это абсолютная чепуха.
Хоббс
@hobbs: Проблема в том, что когда компиляторы манипулируют программой из-за неопределенного поведения, фактический запуск программы действительно приведет к неожиданному поведению, сравнимому с перезаписью памяти.
Матье М.
24

Целочисленное переполнение со знаком - неопределенное поведение. Если это произойдет, ваша программа недействительна. Компилятор не обязан проверять это для вас, поэтому он может сгенерировать исполняемый файл, который, по-видимому, делает что-то разумное, но нет гарантии, что он это сделает.

Однако целочисленное переполнение без знака четко определено. Это обернет по модулю UINT_MAX + 1. Память, не занятая вашей переменной, не будет затронута.

Смотрите также https://stackoverflow.com/q/18195715/951890

Вон Катон
источник
Целочисленное переполнение со знаком точно так же, как и целочисленное переполнение без знака. если слово имеет $ N $ битов, верхняя граница целочисленного переполнения со знаком находится в $$ 2 ^ {N-1} -1 $$ (где оно оборачивается до $ -2 ^ {N-1} $), тогда как верхняя граница для переполнения целого числа без знака находится в $$ 2 ^ N - 1 $$ (где оно оборачивается до $ 0 $). те же механизмы сложения и вычитания, тот же размер диапазона чисел ($ 2 ^ N $), которые могут быть представлены. просто другая граница переполнения.
Роберт Бристоу-Джонсон
1
@ robertbristow-johnson: не соответствует стандарту C.
Вон Катон
ну, стандарты иногда анахроничны. Что касается ссылки на SO, то есть один комментарий, который непосредственно ее затрагивает: «Важное замечание, однако, заключается в том, что в современном мире не осталось никаких архитектур, использующих что-либо, кроме арифметики со знаком дополнения 2. Что стандарты языка все еще допускают реализацию например, PDP-1 является чисто историческим артефактом. - Энди Росс 12 августа '13 в 20:12 "
Роберт Бристоу-Джонсон
я предполагаю, что это не в стандарте C, но я предполагаю, что может быть реализация, для которой обычная двоичная арифметика не используется int. я s'pose они могли бы использовать код Грея или BCD или EBCDIC . Не знаю, почему кто-то может проектировать аппаратные средства для выполнения арифметики с кодом Грея или EBCDIC, но опять же, я не знаю, почему кто-то будет делать unsignedс двоичным кодом и подписывать intчто-либо, кроме дополнения 2.
Роберт Бристоу-Джонсон
14

Итак, здесь есть две вещи:

  • уровень языка: какова семантика C
  • уровень машины: какова семантика используемой сборки / процессора

На уровне языка:

В С:

  • Переполнение и понижение определяются как арифметика по модулю для целых чисел без знака , поэтому их значение «loop»
  • переполнения и опустошения являются Неопределенным поведением для подписанных целых чисел, таким образом , все , что может случиться

Для тех, кто хотел бы "что-нибудь" пример, я видел:

for (int i = 0; i >= 0; i++) {
    ...
}

превратиться в:

for (int i = 0; true; i++) {
    ...
}

и да, это законное преобразование.

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

Примечание: на Clang или gcc используйте -fsanitize=undefinedв Debug для активации Undefined Behavior Sanitizer, который прервет работу при переполнении / переполнении целых чисел со знаком.

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

Примечание: на Clang или gcc используйте -fsanitize=addressв Debug, чтобы активировать Address Sanitizer, который прервет работу за пределами доступа.


На уровне машины :

Это действительно зависит от инструкций по сборке и используемого процессора:

  • на x86 ADD будет использовать 2-дополнение к переполнению / переполнению и установит OF (флаг переполнения)
  • на будущем процессоре Mill будет 4 различных режима переполнения для Add:
    • По модулю: 2-модуль по модулю
    • Trap: ловушка генерируется, останавливая вычисления
    • Насыщать: значение застревает до минимума при переполнении или до максимума при переполнении
    • Двойная ширина: результат генерируется в регистре двойной ширины

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

Матье М.
источник
Последние три режима подписаны? (Не имеет значения для первого, так как он состоит из 2-х дополнений.)
Deduplicator
1
@Deduplicator: в соответствии с введением в модель программирования процессора Mill существуют различные коды операций для добавления со знаком и без знака; Я ожидаю, что оба кода операции будут поддерживать 4 режима (и смогут работать с различной битовой шириной и скалярными / векторами). Опять же, это пара аппаратных средств на данный момент;)
Матье М.
4

Для дальнейшего ответа @ StevenBurnap причина этого заключается в том, как компьютеры работают на уровне машины.

Ваш массив хранится в памяти (например, в оперативной памяти). Когда выполняется арифметическая операция, значение в памяти копируется во входные регистры схемы, которая выполняет арифметику (ALU: Арифметическая логическая единица ), затем выполняется операция с данными во входных регистрах, что дает результат в выходном регистре. Этот результат затем копируется обратно в память по правильному адресу в памяти, оставляя другие области памяти нетронутыми.

Pharap
источник
4

Во-первых (предполагая стандарт C99), вы можете захотеть включить <stdint.h>стандартный заголовок и использовать некоторые из определенных здесь типов, в частности, int32_tэто ровно 32-разрядное целое число со знаком или uint64_tровно 64-разрядное целое число без знака и т. Д. Возможно, вы захотите использовать такие типы, как int_fast16_tпо причинам производительности.

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

Затем, если вам нужно вычислить ровно огромные целые числа (например, вы хотите вычислить факториал 1000 со всеми 2568 цифрами в десятичном виде), вам нужно bigints или числа с произвольной точностью (или bignums). Алгоритмы для эффективной арифметики bigint очень умны и обычно требуют использования специализированных машинных инструкций (например, некоторые добавляют слово с переносом, если таковой имеется в вашем процессоре). Поэтому я настоятельно рекомендую в этом случае использовать некоторую существующую библиотеку bigint, такую ​​как GMPlib

Василий Старынкевич
источник