Чому індекси негативного масиву мають сенс?


14

Я натрапив на дивний досвід програмування на С. Розглянемо цей код:

int main(){
  int array1[6] = {0, 1, 2, 3, 4, 5};
  int array2[6] = {6, 7, 8, 9, 10, 11};

  printf("%d\n", array1[-1]);
  return 0;
}

Коли я компілюю та запускаю це, я не отримую жодних помилок чи попереджень. Як сказав мій лектор, індекс масиву має -1доступ до іншої змінної. Я все ще плутаюся, чому на землі мова мов програмування має таку можливість? Я маю на увазі, чому дозволяють індекси негативного масиву?


2
Хоча це питання мотивоване С як конкретною мовою програмування, я думаю, що це можна зрозуміти як концептуальне питання, яке тут є онопічним (якщо ледь).
Рафаель

7
@Raphael Я не погоджуюсь і вважаю, що він повинен належати до SO, так чи інакше, це не визначена поведінка підручника (посилання на пам'ять поза масивом), і належні прапори компілятора повинні попередити про це
ratchet freak

Я згоден з @ratchetfreak. Схоже, це недолік компілятора, оскільки допустимий діапазон індексів становить [0, 5]. Все, що знаходиться поза, має бути помилкою компіляції / виконання. Як і взагалі, вектори - це окремий випадок функцій , індекс перших елементів яких залежить від користувача. Оскільки контрактом С є те, що елементи починаються з індексу 0, помилка доступу до негативних елементів.
Валь

2
@Raphael C має дві особливості щодо типових мов із масивами, які тут важливі. Одне полягає в тому, що C має підрядні масиви, а перехід на елемент -1підматриці - цілком коректний спосіб позначення елемента перед цим масивом у більшому масиві. Інша полягає в тому, що якщо індекс недійсний, програма недійсна, але в більшості реалізацій ви будете мовчати поганою поведінкою, а не помилкою поза межами діапазону.
Жил "ТАК - перестань бути злим"

4
@Gilles Якщо це питання, це дійсно повинно було бути переповненням стека .
Рафаель

Відповіді:


27

Операція індексації масиву a[i]набуває свого значення завдяки наступним ознакам C

  1. Синтаксис a[i]еквівалентний *(a + i). Таким чином, справедливо сказати, 5[a]щоб потрапити на 5-й елемент a.

  2. Вказівник-арифметичний говорить, що дано вказівник pі ціле число i, p + i покажчик pрозширений у i * sizeof(*p)байтах

  3. Ім'я масиву aдуже швидко переходить до вказівника на 0-й елементa

Фактично, індексація масиву є особливим випадком індексації вказівників. Оскільки вказівник може вказувати на будь-яке місце всередині масиву, будь-яке довільне вираження, схоже p[-1], не є помилковим шляхом перевірки, і тому компілятори не (не можуть) вважати всі такі вирази помилками.

Ваш приклад, a[-1]де aнасправді є ім'я масиву, насправді недійсний. IIRC, це не визначено, якщо є значення значущого вказівника як результат виразу, a - 1де, aяк відомо, є вказівником на 0-й елемент масиву. Отже, розумний компілятор міг виявити це і позначити це як помилку. Інші компілятори все ще можуть бути сумісними, дозволяючи вам стріляти в ногу, даючи вам вказівник на випадковий слот для стека.

Відповідь з інформатики:

  • В C []оператор визначений на покажчиках, а не на масивах. Зокрема, він визначається з точки зору арифметики вказівника та відміни вказівника.

  • В C вказівник абстрактно є кортежем (start, length, offset)з умовою, що 0 <= offset <= length. Арифметика вказівника по суті є піднятою арифметикою на зміщення, із застереженням, що якщо результат операції порушує умова вказівника, це не визначене значення. Зняття посилання на покажчик додає додаткове обмеження, яке offset < length.

  • C має поняття, undefined behaviourяке дозволяє компілятору конкретно представляти цей кортеж як єдине число, і не повинні виявляти будь-яких порушень умови вказівника. Будь-яка програма, яка задовольняє абстрактну семантику, буде безпечною з конкретною (втратною) семантикою. Все, що порушує абстрактну семантику, може без коментарів приймати компілятор, і він може робити все, що завгодно.


Будь ласка, спробуйте дати загальну відповідь, а не одну залежно від ідіосинкразії будь-якої конкретної мови програмування.
Рафаель

6
@Raphael, питання було чітко про C. Я думаю, що я звернувся до конкретного питання, чому компілятору C дозволено складати, здавалося б, безглузде вираження в рамках визначення C.
Харі,

Питання щодо C, зокрема, тут не є актуальними; відзначте мій коментар до питання.
Рафаель

5
Я вважаю, що порівняльний мовознавчий аспект питання все ще корисний. Я вважаю, що я дав досить "ароматизований" опис того, чому конкретна реалізація демонструє конкретну семантику.
Харі

15

Масиви просто викладаються як суміжні шматки пам'яті. Доступ до масиву, такий як [i], перетворюється на доступ до адреси місцезнаходження пам'ятіOf (a) + i. Цей код a[-1]цілком зрозумілий, він просто посилається на адресу перед початком масиву.

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

  • дорого перевірити, чи знаходиться індекс i до [-] в межах масиву.
  • деякі методи програмування фактично використовують факт, що a[-1]є дійсним. Наприклад, якщо я знаю, що aце насправді не початок масиву, а вказівник на середину масиву, то a[-1]просто отримує елемент масиву, який знаходиться зліва від вказівника.

6
Іншими словами, його, мабуть, не слід використовувати. Період. Що, тебе звати Дональд Кнут, і ти намагаєшся зберегти ще 17 інструкцій? Усіма силами йти вперед.
Рафаель

Дякую за відповідь, але я не здобув ідеї. До речі, я прочитаю його знову і знову, поки не зрозумію .. :)
Мохаммед Фазан

2
@Raphael: Реалізація об'єктної моделі кола використовує позицію -1 для зберігання vtable : piumarta.com/software/cola/objmodel2.pdf . Таким чином, поля зберігаються в позитивній частині об'єкта, а vtable в негативній. Я не можу згадати деталі, але я думаю, що це стосується послідовності.
Дейв Кларк

@ DeZéroToxin: Масив - це справді лише місце в пам'яті, а поряд з ним є деякі локації, які логічно є частиною масиву. Але насправді масив - це лише вказівник.
Дейв Кларк

1
@Raphael, a[-1]має ідеальний сенс для деяких випадків a, у цьому конкретному випадку це звичайно незаконно (але не потрапив укладач)
vonbrand

4

Як пояснюють інші відповіді, це невизначена поведінка у C. Вважайте, що C був визначений (і в основному використовується) як "асемблер високого рівня". Користувачі C цінують це за його безкомпромісну швидкість, і перевірка матеріалів під час виконання не викликає сумнівів задля чистої продуктивності. Деякі конструкції С, які виглядають безглуздо для людей, що походять з інших мов, мають досконалий сенс у С, як це a[-1]. Так, це не завжди має сенс (


1
Мені подобається ця відповідь. Дає реальну причину, чому це нормально.
darxsys

3

Можна використовувати таку функцію для написання методів розподілу пам'яті, які безпосередньо отримують доступ до пам'яті. Одне таке використання - перевірити попередній блок пам'яті за допомогою негативного індексу масиву, щоб визначити, чи можна об'єднати два блоки. Я використовував цю функцію, коли розробляв енергонезалежний менеджер пам'яті.


2

С не набирається сильно. Стандартний компілятор C не перевіряє межі масиву. Інша справа, що масив в C - це не що інше, як суміжний блок пам'яті, а індексація починається з 0, тому індекс -1 - це розташування будь-якого біт-шаблону раніше a[0].

Інші мови приємно використовують негативні показники. У Python a[-1]поверне останній елемент, a[-2]поверне другий-останній елемент тощо.


2
Як співвідносяться сильні індекси типізації та масиву? Чи є мови з типом натуралів, де індекси масиву мають бути натуральними?
Рафаель

@Raphael Наскільки я знаю, сильне введення означає, що помилки типу вловлюються. Масив - це тип, IndexOutOfBounds - це помилка, тому в сильно набраній мові про це повідомлять, в C це не буде. Це я мав на увазі.
saadtaame

У моїх мовах індекси масивів мають тип int, тому a[-5]і, загалом, int i; ... a[i] = ...;правильно набрані. Помилки індексу виявляються лише під час виконання. Звичайно, розумний компілятор може виявити деякі порушення.
Рафаель

@Raphael Я говорю про тип даних масиву в цілому, а не про типи індексу. Це пояснює, чому C дозволяє користувачам писати [-5]. Так, -5 - це правильний тип індексу, але він виходить за межі, і це помилка. У моїй відповіді не згадується перевірка типу компіляції чи виконання.
saadtaame

1

Простими словами:

Всі змінні (включаючи масиви) в C зберігаються в пам'яті. Скажімо, у вас є 14 байт "пам'яті", і ви ініціалізуєте таке:

int a=0;
int array1[6] = {0, 1, 2, 3, 4, 5};

Також розглянути розмір int як 2 байти. Тоді, гіпотетично, у перших 2 байтах пам'яті буде збережено ціле число a. У наступних 2 байтах збережеться ціле число першої позиції масиву (це означає масив [0]).

Тоді, коли ви кажете масив [-1], це як посилання на ціле число, збережене в пам'яті, що знаходиться безпосередньо перед масивом [0], яке в нашому, гіпотетично, ціле число a. Насправді це не зовсім так, як змінні зберігаються в пам'яті.


0
//:Example of negative index:
//:A memory pool with a heap and a stack:

unsigned char memory_pool[64] = {0};

unsigned char* stack = &( memory_pool[ 64 - 1] );
unsigned char* heap  = &( memory_pool[ 0     ] );

int stack_index =    0;
int  heap_index =    0;

//:reserve 4 bytes on stack:
stack_index += 4;

//:reserve 8 bytes on heap:
heap_index  += 8;

//:Read back all reserved memory from stack:
for( int i = 0; i < stack_index; i++ ){
    unsigned char c = stack[ 0 - i ];
    //:do something with c
};;
//:Read back all reserved memory from heap:
for( int i = 0; i < heap_index; i++ ){
    unsigned char c = heap[ 0 + i ];
    //:do something with c
};;

Ласкаво просимо на CS.SE! Ми шукаємо відповіді з поясненням чи описом прочитаного. Ми не сайт кодування, і ми не хочемо відповідей, які є лише блоком коду. Ви можете подумати, чи можете ви редагувати свою відповідь, щоб надати таку інформацію. Дякую!
DW
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.