Індексація вказівника


11

Зараз я читаю книгу під назвою "Числові рецепти на С". У цій книзі автор детально описує, як певні алгоритми за своєю суттю працюють краще, якби у нас були індекси, починаючи з 1 (я не повністю дотримуюся його аргументу, і це не суть цієї публікації), але C завжди індексує свої масиви, починаючи з 0 Для того, щоб обійти це, він пропонує просто декрементувати покажчик після виділення, наприклад:

float *a = malloc(size);
a--;

Це, за його словами, ефективно дасть вам покажчик, який має індекс, починаючи з 1, а потім буде безкоштовно:

free(a + 1);

Наскільки мені відомо, це не визначена поведінка за стандартом C. Це, мабуть, дуже авторитетна книга у спільноті HPC, тому я не хочу просто ігнорувати те, що він говорить, а просто декрементація покажчика поза виділеним діапазоном здається мені дуже схематичною. Це "дозволена" поведінка в С? Я перевірив це, використовуючи і gcc, і icc, і обидва ці результати, мабуть, вказують на те, що я переживаю ні про що, але хочу бути абсолютно позитивним.


3
на який стандарт C ви посилаєтесь? Я прошу, тому що, за моїм спогадом, "Числові рецепти на С" були опубліковані в 1990-х, в давнину K&R і, можливо, ANSI C
gnat

2
Пов'язаний питання SO: stackoverflow.com/questions/10473573 / ...
dan04

3
"Я перевірив це, використовуючи і gcc, і icc, і обидва ці результати, мабуть, свідчать про те, що я нічого не переживаю, але хочу бути абсолютно позитивним". Ніколи не припускайте, що оскільки ваш компілятор дозволяє, мова С дозволяє це. Якщо, звичайно, ви не будете добре з тим, щоб ваш код порушився в майбутньому.
Doval

5
Не бажаючи бути химерними, "Числові рецепти", як правило, вважаються корисною, швидкою та брудною книгою, а не парадигмою ні розробки програмного забезпечення, ні чисельного аналізу. Перегляньте статтю Вікіпедії на тему "Числові рецепти", щоб ознайомитись із деякими зауваженнями.
Чарльз Е. Грант

1
Окрім того, ось чому ми індексуємо з нуля: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Рассел Борогов

Відповіді:


16

Ви праві, що такий код, як

float a = malloc(size);
a--;

дає не визначене поведінку, згідно стандарту ANSI C, розділ 3.3.6:

Якщо обидва операнда вказівника і результату не вказують на члена одного об'єкта масиву або одного минулого члена об'єкта масиву, поведінка не визначена

Для такого коду якість коду С у книзі (ще коли я використовував її наприкінці 1990-х) не вважалася дуже високою.

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

Тільки майте на увазі, що код може порушитися, коли середовище виконання буде змінено або коли код перенесено в інше середовище.


4
Точно в багатобанківській архітектурі можливо, що malloc міг би дати вам 0-ту адресу в банку, і зменшення його може спричинити пастку процесора з підтоком для одного.
Vality

1
Я не згоден, що це "пощастило". Я думаю, що було б набагато краще, якби компілятори випустили код, який негайно вийшов з ладу щоразу, коли ви викликали невизначене поведінку.
Девід Конрад

4
@DavidConrad: Тоді C - це не мова для вас. Значну частину невизначеної поведінки на C не можна легко виявити або лише за допомогою серйозного удару.
Барт ван Інген Шенау

Я думав додати "з компілятором". Очевидно, ви б не хотіли цього для оптимізованого коду. Але ти маєш рацію, і тому я відмовився писати С десять років тому.
Девід Конрад

@BartvanIngenSchenau залежно від того, що ви маєте на увазі під "серйозним ударом", є символічне виконання як для C (наприклад, clang + klee), так і санатизаторів (asan, tsan, ubsan, valgrind тощо), які, як правило, дуже корисні для налагодження.
Maciej Piechotka

10

Офіційно, невизначеною поведінкою є точка вказівника поза масивом (за винятком однієї минулої частини), навіть якщо вона ніколи не відмежовується .

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


1
Що має сенс. На жаль, це занадто багато, якщо мені подобається.
wolfPack88

3
Останній пункт - ІМХО найпроблемніший. Оскільки компілятори в цей час не дозволяють просто траплятися, що б платформа "природно" робилася у випадку з UB, але оптимізатори агресивно її використовують , я б не грав з цим так легко.
Маттео Італія

3

По-перше, це невизначена поведінка. Деякі оптимізуючі компілятори сьогодні дуже агресивні щодо невизначеної поведінки. Наприклад, оскільки a-- в цьому випадку невизначена поведінка, компілятор може вирішити зберегти інструкцію та цикл процесора, а не декремент a. Що є офіційно правильним і законним.

Ігноруючи це, ви можете відняти 1, або 2, або 1980 р. Наприклад, якщо у мене є фінансові дані за 1980–2013 роки, я можу відняти 1980. Тепер, якщо взяти float * a = malloc (розмір); напевно є якась велика константа k така, що a - k - це нульовий покажчик. У цьому випадку ми дійсно очікуємо, що щось піде не так.

Тепер візьміть велику структуру, скажімо, мегабайт розміром. Виділіть вказівник p, що вказує на дві структури. p - 1 може бути нульовим покажчиком. p - 1 може обернутися навколо (якщо структура є мегабайт, а блок malloc становить 900 КБ від початку адресного простору). Так що без шкідливості компілятора може бути, що p - 1> p. Речі можуть стати цікавими.


1

... просто декрементація вказівника за межами виділеного діапазону здається мені дуже схематичною. Це "дозволена" поведінка в С?

Дозволено? Так. Гарна ідея? Не зазвичай.

C - це скорочення для мови складання, а в мові складання немає вказівників, а лише адреси пам'яті. Покажчики C - це адреси пам'яті, які мають побічну силу збільшення або зменшення за розміром, на що вони вказують, коли вони піддаються арифметиці. Це робить це просто добре з точки зору синтаксису:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

Масиви - це насправді не річ у С; вони просто вказівки на суміжні діапазони пам'яті, які ведуть себе як масиви. []Оператор є узагальнюючим для виконання арифметичних операцій над покажчиками і разименованія, тому на a[x]самому ділі означає *(a + x).

Для цього є поважні причини, такі як деякі пристрої вводу / виводу, які мають пару зображень double, відображених у 0xdeadbee7та 0xdeadbeef. Для цього знадобиться дуже мало програм.

Коли ви створюєте адресу чогось, наприклад, за допомогою &оператора чи дзвінка malloc(), ви хочете зберегти первинний вказівник недоторканим, щоб ви знали, що те, на що вказує, насправді щось дійсне. Зменшення покажчика означає, що якийсь біт помилкового коду може спробувати знеструмити його, отримавши помилкові результати, щось втрутившись чи, залежно від вашого оточення, допустити порушення сегментації. Це особливо стосується того malloc(), що ви поклали тягар на того, хто закликає free()пам’ятати про те, щоб передати оригінальне значення, а не якусь змінену версію, яка призведе до того, що всякий чорт зламається.

Якщо вам потрібен масив на основі С на C, ви можете це зробити безпечно за рахунок виділення одного додаткового елемента, який ніколи не буде використаний:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

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


Додаток:

Деякі розділи та вірші із проекту C99 (вибачте, це все, на що я можу посилатись):

У § 6.5.2.1.1 сказано, що другий ("інший") вираз, який використовується з оператором підписника, має цілочисельний тип. -1є цілим числом, і це робить p[-1]дійсним, а тому також робить покажчик &(p[-1])дійсним. Це не означає, що доступ до пам’яті в цьому місці створює певну поведінку, але покажчик все-таки є дійсним вказівником.

§6.5.2.2 говорить, що оператор індексів масиву оцінює еквівалент додавання номера елемента до вказівника, тому p[-1]еквівалентний *(p + (-1)). Досі діє, але може не спричинити бажаної поведінки.

§6.5.6.8 говорить (наголос мій):

Коли вираз, який має цілий тип, додається до вказівника або віднімається в ньому, результат має тип операнда вказівника.

... якщо вираз Pвказує на i-й елемент об’єкта масиву, вирази (P)+N(еквівалентно N+(P)) та (P)-N (де Nмає значення n) вказують відповідно на i+n-і та i−n-і елементи об’єкта масиву, за умови їх існування .

Це означає, що результати арифметики вказівника повинні вказувати на елемент масиву. Це не говорить про те, що арифметику потрібно робити все відразу. Тому:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

Я рекомендую робити такі дії? Ні, і моя відповідь пояснює, чому.


8
-1 Визначення "дозволений", що включає код, який стандарт C оголошує як генерування невизначених результатів, не є корисним.
Піт Кіркхем

Інші вказували, що це невизначена поведінка, тому не слід говорити, що це "дозволено". Однак пропозиція виділити зайвий невикористаний елемент 0 є хорошою.
200_успіх

Це дійсно неправильно, принаймні зауважте, що це заборонено стандартом С.
Vality

@PeteKirkham: Я не згоден. Дивіться додаток до моєї відповіді.
Blrfl

4
@Blrfl 6.5.6 стандартних стандартів ISO C11 у разі додавання цілого числа до вказівника: "Якщо і операнд вказівника, і результат вказують на елементи одного об'єкта масиву або один минулий останній елемент об'єкта масиву , оцінка не повинна спричиняти переповнення, інакше поведінка не визначена ".
Vality
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.