Якщо число завелике, воно перекинеться на наступне місце пам'яті?


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 зборах і перегляд адрес, які змінюють значення протягом програми, крок за кроком, що значення, призначені для цих адрес, змінюватимуться.

Якщо я не пам’ятаю неправильно, то тут є ще одне питання: Якщо число, призначене певній адресі, перевищує тип (наприклад, у myArray [2]), чи не впливає це на значення, збережені на наступній адресі?

Приклад: Ми маємо int myNum = 4 мільярди за адресою 0x10010000. Звичайно, myNum не може зберігати 4 мільярди, тому це відображається як якесь негативне число за цією адресою. Незважаючи на неможливість зберігання такої великої кількості, це не впливає на значення, збережене за наступною адресою 0x10010004. Правильно?

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

Вибачте, якщо я перейшов за борт. Цілий день я маю головний мозок від цього мозку.


10
Можливо, ви плутаєтесь із завитками рядків .
Роббі Ді

19
Домашнє завдання: Змініть простий процесор, щоб він не розлив. Ви побачите, що логіка стає набагато складнішою, все для "функції", яка б гарантувала дірки в безпеці скрізь, не будучи корисною в першу чергу.
Фігаг

4
Якщо вам потрібні дійсно величезні цифри, можна представити число, яке збільшує кількість пам'яті, яку вона використовує для розміщення великих чисел. Сам процесор цього не може зробити, і це не є особливістю мови С, але бібліотека може це реалізувати - загальна бібліотека С - це арифметична бібліотека множинної точності GNU . Бібліотека повинна керувати пам'яттю для зберігання чисел, які мають кращу ефективність, а не арифметику. У багатьох мовах вбудована така річ (що не дозволяє уникнути витрат).
Steve314

1
напишіть простий тест, я не програміст на C, але щось в порядку int c = INT.MAXINT; c+=1;і подивіться, що сталося з c.
JonH

2
@JonH: Проблема полягає в тому, що переповнення не визначеної поведінки. Компілятор змінного струму може помітити цей код і зробити висновок про недоступність коду, оскільки він безумовно переповнює. Оскільки недоступний код не має значення, його можна усунути. Кінцевий результат: код не залишився.
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. => Ні , це не визначена поведінка , тому вона може обертатися навколо або ж робити щось інше цілком; Я бачив компілятори, що оптимізують чеки на основі відсутності переповнення, і, наприклад, отримують нескінченні петлі ...
Matthieu M.

Вибачте, так, ви маєте рацію. Я мав би там додати "зазвичай".
Gort the Robot

@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 серпня
1313

я припускаю, що це не в стандарті С, але я припускаю, що може бути реалізація, коли регулярна двійкова арифметика не використовується int. Я гадаю, що вони можуть використовувати сірий код або BCD або EBCDIC . не знаю, чому хтось розробляє обладнання, щоб робити арифметику з кодом Грея або EBCDIC, але знову ж таки, я не знаю, чому хтось би робив unsignedбинарні файли і підписував що- intнебудь, крім додатка 2.
Роберт Брістоу-Джонсон

14

Отже, тут є дві речі:

  • мовний рівень: що таке семантика С
  • рівень машини: яка семантика складання / процесора ви використовуєте

На мовному рівні:

В:

  • переповнення та переливання визначаються як модуль арифметики для цілих цілей, що не підписуються, таким чином їх значення "петлі"
  • overflow та underflow - це не визначена поведінка для підписаних цілих чисел, тому може статися все, що завгодно

Для тих, хто хотів би прикладу "що б то не було", я бачив:

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

перетворюватися:

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

і так, це законна трансформація.

Це означає, що дійсно існують потенційні ризики перезапису пам’яті при переповненні через якесь дивне перетворення компілятора.

Примітка: у програмі Clang або gcc -fsanitize=undefinedв Налагодженні активується Невизначений дезіннізатор поведінки, який припинить переповнення / переповнення підписаних цілих чисел.

Або це означає, що ви можете перезаписати пам'ять, скориставшись результатом операції, щоб індексувати (невірно) в масив. На жаль, це набагато ймовірніше за відсутності виявлення підливу / переливу.

Примітка: у програмі Clang або gcc -fsanitize=addressв Налагодженні активуйте Sanitizer Address, який перестане працювати за межами доступу.


На рівні машини :

Це дійсно залежить від інструкцій по збірці та процесора, який ви використовуєте:

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

Зауважте, що незалежно від того, що трапляється в регістрах чи пам'яті, ні в якому разі процесор не перезаписує пам'ять при переповненні.


Чи підписані три останні режими? (Не має значення для першого, оскільки це 2-доповнення.)
Дедуплікатор

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

4

Для подальшої відповіді @ СтівенБурнап, причина цього відбувається в тому, як комп'ютери працюють на машинному рівні.

Ваш масив зберігається в пам'яті (наприклад, в оперативній пам'яті). Коли виконується арифметична операція, значення в пам'яті копіюється у вхідні регістри схеми, яка виконує арифметику (АЛУ: Арифметична логічна одиниця ), операція потім проводиться над даними у вхідних регістрах, створюючи результат у вихідному реєстрі. Потім цей результат копіюється назад в пам'ять за правильною адресою в пам'яті, залишаючи інші ділянки пам'яті недоторканими.


4

По-перше (якщо припустити стандарт C99), ви можете включити <stdint.h>стандартний заголовок і використовувати деякі типи, визначені там, зокрема int32_tце ціле ціле число з 32 бітами, або uint64_t64-бітне ціле ціле число, не підписане тощо. Можливо, ви хочете використовувати такі типи, як int_fast16_tз міркувань продуктивності.

Прочитайте відповіді інших, пояснюючи, що непідписана арифметика ніколи не розливається (або переливається) на сусідні місця пам'яті. Остерігайтеся невизначеної поведінки при підписаному переливі.

Тоді, якщо вам потрібно обчислити точно величезні цілі числа (наприклад, ви хочете обчислити коефіцієнт 1000 з усіма його 2568 цифрами у десятковій частині), ви хочете, щоб великі величини називались довільними точними числами (або двознаками). Алгоритми ефективної арифметики bigint дуже розумні, і зазвичай вимагає використання спеціалізованих інструкцій на машині (наприклад, деякі додають слово з перенесенням, якщо це має ваш процесор). Тому я настійно рекомендую в такому випадку використовувати деяку існуючу бібліотеку bigint, наприклад GMPlib

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.