Як працює порівняння вказівників у С? Чи нормально порівнювати покажчики, які не вказують на один і той же масив?


33

У розділі 5 K&R (Мова програмування на C) я прочитав наступне:

По-перше, покажчики можуть бути порівняні за певних обставин. Якщо pі qвказують на елементи одного і того ж масиву, то співвідношення подобається ==, !=, <, >=і т.д. працювати належним чином.

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

Однак коли я спробував цей код

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 друкується на екрані.

Перш за все, я думав, що я отримаю невизначений або якийсь тип чи помилку, тому що ptі pxне вказують на один масив (принаймні, наскільки я розумію).

Також тому, pt > pxщо обидва вказівника вказують на змінні, що зберігаються на стеку, і стек зростає вниз, тому адреса пам'яті tбільше, ніж у x? Чому pt > pxце правда?

Я більше заплутався, коли вводиться malloc. Також у K&R у розділі 8.7 написано наступне:

Однак є одне припущення, що вказівники на різні блоки, повернуті через, sbrkможна змістовно порівняти. Це не гарантується стандартом, який дозволяє зіставляти покажчики лише в масиві. Таким чином, ця версія mallocє портативною лише серед машин, для яких загальне порівняння покажчиків має сенс.

У мене не виникало проблем із порівнянням покажчиків, які вказували на пробіл у купі простору з покажчиками, які вказували на змінні стека.

Наприклад, наступний код спрацював чудово, з 1друком:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

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

І все-таки мене бентежить те, що я читаю в K&R.

Причину, про яку я прошу, - це те, що мій проф. насправді зробив це екзаменаційним питанням. Він надав такий код:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

Що вони оцінюють:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

Відповідь 0, 1і 0.

(Мій професор включає відмову від іспиту про те, що питання стосуються 64-бітового середовища програмування Ubuntu Linux 16.04)

(Примітка редактора: якщо SO дозволяв більше тегів, ця остання частина гарантувала б , та, можливо, . Якщо питанням / класом були конкретні деталі впровадження ОС, а не портативний C.)


17
Ви , може бути заплутаним , що діє в Cс тим, що є безпечним в C. Порівнювати два покажчики на один і той же тип завжди можна (перевіряючи рівність, наприклад), однак, використовуючи арифметику вказівника та порівняння, >і безпечно< лише при використанні в заданому масиві (або блоці пам'яті).
Адріан Моль

13
Як осторонь, ви не повинні вивчати C з K&R. Для початку мова пройшла через багато змін. І, чесно кажучи, приклад коду був ще з часів, коли цінувались швидкість, а не читальність.
paxdiablo

5
Ні, це не гарантовано працює. Це може вийти з ладу на машинах із сегментованими моделями пам'яті. Див. Чи C має еквівалент std :: менше від C ++? На більшості сучасних машин це станеться, незважаючи на UB.
Пітер Кордес

6
@Adam: Закрийте, але це насправді UB (якщо компілятор, який використовував ОП, GCC, не вирішив визначити це. Можливо). Але UB не означає "точно вибухає"; одна з можливих поведінки для UB працює так, як ви очікували !! Це те, що робить UB таким неприємним; він може працювати прямо в налагодженні і не вдається з увімкненою оптимізацією, або навпаки, або зламатися залежно від оточуючого коду. Порівняння інших покажчиків все одно дасть вам відповідь, але мова не визначає, що ця відповідь буде означати (якщо щось є). Ні, збій дозволений. Це справді UB.
Пітер Кордес

3
@Adam: О так, ніколи не забудьте про першу частину мого коментаря, я неправильно прочитав ваш. Але ви стверджуєте, що порівняння інших покажчиків все одно дасть вам відповідь . Це не правда. Це був би не визначений результат , а не повний UB. UB набагато гірший і означає, що ваша програма може сегментуватись або SIGILL, якщо виконання досягне цього твердження з цими входами (у будь-який момент до або після цього насправді відбувається). (Правдоподібно на x86-64, якщо UB видно під час компіляції, але взагалі все може статися.) Частина пункту UB полягає в тому, щоб компілятор робив "небезпечні" припущення під час створення ASM.
Пітер Кордес

Відповіді:


33

Згідно стандарту C11 , реляційні оператори <, <=, >, і >=можуть бути використані тільки на покажчики на елементи одного і того ж масиву або структури об'єкта. Це прописано в розділі 6.5.8p5:

При порівнянні двох покажчиків результат залежить від відносних розташувань у адресному просторі об’єктів, на які вказували. Якщо два вказівника на типи об’єктів обидва вказують на один і той же об'єкт або обидва вказують один на останній елемент того ж об’єкта масиву, вони порівнюються рівними. Якщо об'єкти, на які вказували, є членами одного і того ж сукупного об'єкта, вказівники на члени структури, оголошені пізніше, порівнюють більше, ніж покажчики, на члени, оголошені раніше в структурі, а покажчики на елементи масиву з більшими значеннями індекса порівняно більшими, ніж покажчики на елементи того ж масиву з нижчими значеннями індексів. Усі вказівники на членів одного об'єкта об'єднання порівнюються рівними.

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

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

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

Оператори рівності ==і , !=проте , не мають цього обмеження. Вони можуть використовуватися між будь-якими двома вказівниками на сумісні типи або NULL вказівниками. Таким чином, використовуючи ==або !=в обох ваших прикладах, можна отримати дійсний код C.

Однак, навіть ==і !=ви можете отримати несподівані, але все-таки чітко визначені результати. Див. Чи може порівняння рівності споріднених покажчиків оцінити справжнє? для більш детальної інформації про це.

Що стосується екзаменаційного питання, яке дав ваш професор, воно робить ряд хибних припущень:

  • Модель плоскої пам’яті існує там, де між адресою і цілим значенням є відповідність 1 на 1.
  • Щоб перетворені значення вказівника помістилися всередині цілого типу.
  • Що реалізація просто розглядає покажчики як цілі числа при проведенні порівнянь, не використовуючи свободу, надану невизначеною поведінкою.
  • Що стек використовується і що там зберігаються локальні змінні.
  • Що купа використовується для витягування виділеної пам'яті з.
  • Що стек (і, отже, локальні змінні) з'являється за більш високою адресою, ніж купа (і, отже, виділені об'єкти).
  • Ці рядкові константи з'являються за нижчою адресою, ніж у купі.

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

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


3
@Shisui Навіть враховуючи це, ви все одно не повинні залежати від результатів. Компілятори можуть стати дуже агресивними, коли йдеться про оптимізацію, і використовуватимуть невизначену поведінку як можливість для цього. Можливо, що використання іншого компілятора та / або різних налаштувань оптимізації може генерувати різні результати.
dbush

2
@Shisui: Загалом це станеться для роботи на машинах із плоскою моделлю пам'яті, наприклад x86-64. Деякі компілятори для таких систем можуть навіть визначати поведінку в їх документації. Але якщо ні, то "божевільна" поведінка може статися із-за того, що UB бачить час компіляції. (На практиці я не думаю, що хтось хоче цього, тому це не щось, що компілятори мейнстріму шукають і "намагаються зламати".)
Пітер Кордес

1
Як би, якщо компілятор бачить, що один шлях виконання призведе до <між mallocрезультатом і локальною змінною (автоматичне зберігання, тобто стек), він може припустити, що шлях виконання ніколи не приймається, а просто компілювати всю функцію до ud2інструкції (викликає незаконне -виключення з інструкцією, з яким буде працювати ядро, доставляючи SIGILL до процесу). GCC / clang роблять це на практиці для інших типів UB, як-от падіння в кінці нефункції void. godbolt.org зараз вниз, здається, але спробуйте скопіювати / вставити int foo(){int x=2;}та відзначте відсутністьret
Peter Cordes

4
@Shisui: TL: DR: це не портативний C, незважаючи на те, що він працює добре на Linux x86-64. Хоча робити припущення щодо результатів порівняння просто шалено. Якщо ви не в основному потоці, ваш стек потоку буде динамічно розподілений за допомогою того ж механізму, що mallocвикористовує більше пам'яті в ОС, тому немає причин вважати, що ваші локальні значення (стек потоків) вище mallocдинамічно розподілених зберігання.
Пітер Кордес

2
@PeterCordes: Необхідно визнати різні аспекти поведінки як "необов'язково визначені", таким чином, що впровадження можуть визначати їх чи ні у вільний час, але вони повинні вказувати тестово (наприклад, заздалегідь визначений макрос), якщо вони цього не роблять. Крім того, замість того, щоб охарактеризувати, що будь-яка ситуація, коли наслідки оптимізації будуть помітні як "не визначена поведінка", було б набагато корисніше сказати, що оптимізатори можуть вважати певні аспекти поведінки "неспостережними", якщо вони вказують, що вони зроби так. Наприклад, з огляду на int x,y;, реалізація ...
Supercat

12

Основна проблема при порівнянні покажчиків на два різних масиви одного типу полягає в тому, що самі масиви не потрібно розміщувати у певному відносному розташуванні - один може закінчитися до і після іншого.

Перш за все, я думав, що я отримаю невизначений чи якийсь тип чи помилку, оскільки pt a px не вказує на той самий масив (принаймні, наскільки я розумію).

Ні, результат залежить від реалізації та інших непередбачуваних факторів.

Також є pt> px, тому що обидва вказівника вказують на змінні, що зберігаються на стеку, і стек зростає вниз, тому адреса пам'яті t більша, ніж у x? Через що pt> px справжній?

Не обов'язково є стек . Коли вона існує, їй не потрібно рости. Це може вирости. Це може бути непомітним у чомусь химерному вигляді.

Більше того, я вважаю, що арифметика вказівника між двома вказівниками добре, незалежно від того, куди вони окремо вказують, оскільки арифметика просто використовує адреси пам'яті для зберігання покажчиків.

Давайте розглянемо специфікацію C , §6.5.8 на сторінці 85, де розглядаються реляційні оператори (тобто оператори порівняння, які ви використовуєте). Зауважте, що це не стосується прямого !=чи ==порівняльного використання.

При порівнянні двох покажчиків результат залежить від відносних розташувань у адресному просторі об’єктів, на які вказували. ... Якщо об'єкти, на які вказували, є членами одного і того ж сукупного об'єкта, ... покажчики на елементи масиву з більшими значеннями підрису порівнюють більше, ніж покажчики, на елементи того ж масиву з нижчими значеннями індексів.

У всіх інших випадках поведінка не визначена.

Останнє речення важливе. Хоча я вирізав деякі непов'язані випадки, щоб заощадити простір, є один важливий для нас випадок: два масиви, не частина одного і того ж об’єкта структура / агрегат 1 , і ми порівнюємо вказівники на ці два масиви. Це невизначена поведінка .

Поки ваш компілятор просто вставив якусь машинну інструкцію CMP (порівняйте), яка чисельно порівнює вказівники, і вам тут пощастило, UB - досить небезпечний звір. Буквально все може статися - ваш компілятор може оптимізувати всю функцію, включаючи видимі побічні ефекти. Це може породити носових демонів.

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


1
Що ще важливіше, з тією ж функцією tта xїї визначенням немає нульових причин припускати що-небудь про те, як компілятор, націлений на x86-64, викладе локальних даних у кадр стека для цієї функції. Стек, що зростає вниз, не має нічого спільного з порядком декларування змінних в одній функції. Навіть в окремих функціях, якщо один міг би бути вбудованим в інший, тоді місцеві жителі функції "дитина" все-таки могли б змішатися з батьками.
Пітер Кордес

1
ваш компілятор може оптимізувати всю функцію, включаючи видимі побічні ефекти. Не завищення: для інших видів UB (наприклад, відпадання кінця нефункції void) g ++ і clang ++ дійсно роблять це на практиці: godbolt.org/z/g5vesB вони припустимо, що шлях виконання не приймається, оскільки він веде до UB, і складіть будь-які такі базові блоки до незаконної інструкції. Або взагалі ніяких вказівок, просто мовчки пробиваючись до того, що поруч буде наступним, якщо цю функцію коли-небудь викликали. (Тільки чомусь gccцього не роблять g++).
Пітер Кордес

6

Потім запитав, що

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Оцінити до. Відповідь - 0, 1 і 0.

Ці питання зводяться до:

  1. Чи купа над групою чи під нею.
  2. Чи є купа над або під рядковим буквальним розділом програми.
  3. те саме, що [1].

І відповідь усім трьом - "визначено реалізацію". Питання вашого професора нечіткі; вони базували його в традиційному макеті Unix:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

але декілька сучасних уніцій (та альтернативних систем) не відповідають цим традиціям. Якщо вони не заперечували питання з "станом на 1992 рік"; обов'язково вкажіть -1 на овалі.


3
Не визначено впровадження, не визначено! Подумайте про це таким чином: перші можуть відрізнятися між реалізаціями, але реалізація повинна документувати, як визначається поведінка. Останнє означає, що поведінка може різнитися будь-яким чином, і реалізація не повинна говорити вам присідання :-)
paxdiablo

1
@paxdiablo: Відповідно до обґрунтування авторів Стандарту, "Не визначена поведінка ... також визначає області можливого відповідного розширення мови: реалізатор може доповнити мову, надаючи визначення офіційно невизначеної поведінки". Обґрунтування також говорить: "Мета полягає в тому, щоб дати програмісту бойовий шанс зробити потужні програми C, які також є дуже портативними, не здаючись, що вони перешкоджають ідеально корисним програмам C, які, можливо, не є портативними, таким чином, прислівник суворо". Комерційні автори компіляторів це розуміють, але деякі інші автори-компілятори цього не розуміють.
Supercat

Є ще один аспект, визначений реалізацією; порівняння покажчиків підписане , тому залежно від машини / OS / компілятора деякі адреси можуть трактуватися як негативні. Наприклад, 32-бітна машина, яка розмістила стек у 0xc << 28, ймовірно, відображатиме автоматичні змінні за меншою адресою, ніж купа або родата.
mevets

1
@mevets: Чи визначає Стандарт будь-яку ситуацію, в якій підписність покажчиків у порівнянні була б помітна? Я б очікував, що якщо 16-розрядна платформа дозволяє об'єктам, що перевищують 32768 байт, і arr[]є таким об'єктом, то Стандарт покладе максимум на arr+32768порівняння, arrнавіть якщо підписане вказівник порівняння повідомляє про інше.
Supercat

Не знаю; стандарт С орбітає в дев'ятому колі Данте, молячись за евтаназію. ОП спеціально посилалася на K&R та екзаменаційне питання. #UB - сміття від ледачої робочої групи.
mevets

1

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

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

Мені невідомі будь-які компілятори, комерційно розроблені, які роблять щось дивне з порівнянням покажчиків, але, коли компілятори переходять до некомерційних LLVM для їх заднього кінця, вони все частіше обробляють безглуздий код, поведінка якого було визначено раніше компілятори для своїх платформ. Така поведінка не обмежується реляційними операторами, але навіть може впливати на рівність / нерівність. Наприклад, навіть незважаючи на те, що Стандарт визначає, що порівняння між вказівником на один об'єкт та "просто минулим" вказівником на об'єкт, що безпосередньо передує, порівняє рівні, компілятори на основі gcc та LLVM схильні генерувати безглуздий код, якщо програми виконують такі порівняння.

Як приклад ситуації, коли навіть порівняння рівності поводиться безглуздо в gcc і clang, розглянемо:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

І clang, і gcc генерують код, який завжди повертає 4, навіть якщо xце десять елементів, yодразу слідує за ним і iдорівнює нулю, в результаті чого порівняння є істинним і p[0]записується зі значенням 1. Я думаю, що трапляється один пропуск оптимізації переписується функція як би *p = 1;була замінена на x[10] = 1;. Останній код був би еквівалентним, якби компілятор інтерпретувався *(x+10)як еквівалентний *(y+i), але, на жаль, етап оптимізації вниз за течією визнає, що доступ до нього x[10]визначається лише у тому випадку, якщо він xмає принаймні 11 елементів, що унеможливить вплив на цей доступ y.

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


0

Просте: Порівнювати покажчики не має сенсу, оскільки місця пам'яті об’єктів ніколи не гарантуються в тому ж порядку, як ви їх оголосили. Виняток становлять масиви. & масив [0] нижче, ніж & масив [1]. Саме на це вказує K&R. На практиці адреси членів структури є також у тому порядку, коли ви їх оголосили, як показує мій досвід. Ніяких гарантій на це .... Ще один виняток - якщо ви порівнюєте вказівник для рівних. Коли один вказівник дорівнює іншому, ви знаєте, що він вказує на той самий об'єкт. Що б це не було. Неправильне екзаменаційне запитання, якщо ви мене запитаєте. Залежно від Ubuntu Linux 16.04, 64-бітне середовище програмування версій для іспитного питання? Дійсно?


Технічно, масиви не є дійсно виняток , так як ви не розкажете arr[0], arr[1]і т.д. окремо. Ви заявляєте arrв цілому, тому впорядкування окремих елементів масиву є іншим питанням, ніж описане в цьому запитанні.
paxdiablo

1
Елементи структури гарантовано є в порядку, що гарантує, що можна memcpyскопіювати суміжну частину структури і вплинути на всі елементи в ній, а також не впливати ні на що інше. Стандарт недбалий щодо термінології щодо того, які види арифметики покажчика можна виконати зі структурами або malloc()виділеним сховищем. offsetofМакрос буде досить марним , якщо один не міг з таким же арифметика покажчиків з байтами структури , як з char[], але Стандарт не прямо сказати , що байти структури є (або можуть бути використані в якості) об’єкт масиву.
Supercat

-4

Що за провокаційне запитання!

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

Це не повинно дивувати.

Безперечно, нерозуміння навколо концепції і використання в покажчиках є домінуючу причину серйозних збоїв в програмуванні в цілому.

Визнання цієї реальності легко проявляється в повсюдності мов, розроблених спеціально для вирішення, і бажано, щоб уникнути проблем, які вказують вказівники взагалі. Подумайте, що C ++ та інші похідні C, Java та її відносин, Python та інших сценаріїв - лише як найвидатніші та найпоширеніші, і більш-менш упорядковані всерйоз вирішення цього питання.

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

Я уявляю, що це саме те, що має показати ваш вчитель.

А природа С робить його зручним транспортним засобом для цієї розвідки. Менш чітко, ніж збірка - хоча і, можливо, більш зрозуміла - і все ж набагато чіткіше, ніж мови, засновані на більш глибокій абстракції середовища виконання.

Створений для полегшення детермінованого перекладу намірів програміста в інструкції, які машини можуть зрозуміти, C - це система системного рівня . Хоча класифікується як високий рівень, він дійсно відноситься до категорії «середній»; але оскільки такого не існує, позначення "системи" повинно бути достатньо.

Ця характеристика значною мірою відповідає за те, що вона стала мовою вибору для драйверів пристроїв , коду операційної системи та вбудованих реалізацій. Крім того, заслужена альтернатива в додатках, де оптимальна ефективність є найважливішою; де це означає різницю між виживанням та вимиранням, а тому є необхідністю на відміну від розкоші. У таких випадках приваблива зручність переносимості втрачає всю її привабливість, а вибір недолікової продуктивності найменшого загального знаменника стає немислимим згубним варіантом.

Що робить C - та деякі його похідні - зовсім особливими, це те, що він дозволяє своїм користувачам повний контроль - коли це те, чого вони хочуть - без покладаючи на них пов'язаних обов'язків, коли вони цього не роблять. Тим не менш, він ніколи не пропонує більше, ніж найтонший ізолятор від машини , тому правильне використання вимагає глибокого розуміння концепції покажчиків .

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

  • Акти вивчення, порівняння та маніпулювання покажчиками завжди і обов'язково справедливі, тоді як висновки, отримані з результату, залежать від обгрунтованості значень, що містяться, і, отже, не повинні бути.

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

Звичайно, частина плутанини виникає внаслідок ефекту рекурсії, притаманної в рамках принципу вказівника, - і проблем, що виникають при диференціації змісту від адреси.

Ви цілком правильно переконали,

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

І кілька учасників підтвердили: покажчики - це просто цифри. Іноді щось ближче до складних чисел, але все ж не більше числа.

Кумедна прискіпливість, в якій ця суперечка була отримана тут, розкриває більше про природу людини, ніж програмування, але залишається достойною уваги та деталізації. Можливо, ми це зробимо пізніше ...

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

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

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

Як вказували декілька: термін вказівник - це лише особлива назва того, що є просто індексом , і, таким чином, не більше ніж будь-яке інше число .

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

Технічно, як ви зазначили, покажчики є більш точними адресами ; очевидне розуміння, яке природно вводить корисну аналогію співвіднесення їх з "адресами" будинків або ділянок на вулиці.

  • У моделі плоскої пам’яті: вся системна пам’ять організована в єдиній лінійній послідовності: всі будинки міста лежать на одній дорозі, і кожен будинок однозначно ідентифікується лише за своєю кількістю. Чудово простий.

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

    • Деякі реалізації все ще більш суперечливі, і сукупність різних "доріг" не повинна дорівнювати суміжній послідовності, але жодне з цього нічого не змінює базового.
    • Нам обов'язково вдається розкласти кожну таку ієрархічну зв'язок назад в рівну організацію. Чим складніша організація, тим більше обручів нам доведеться перестрибувати, щоб це зробити, але це повинно бути можливим. Дійсно, це стосується і «реального режиму» на x86.
    • Інакше відображення посилань на локації не буде біективним , оскільки надійне виконання - на системному рівні - вимагає цього ПОВИНЕН бути.
      • повинно бути декілька адрес невідображатись у єдиних місцях пам'яті;
      • Сингулярні адреси ніколи не повинні відображатись у кількох місцях пам'яті.

Приводячи нас до подальшого повороту, який перетворює головоломку в такий захоплююче складний клубок . Вище було доцільним припустити, що покажчики - це адреси, для простоти та ясності. Звичайно, це не правильно. Вказівник - це не адреса; покажчик - це посилання на адресу , він містить адресу . Як і конверт, має посилання на будинок. Якщо замислитись над цим, це може призвести до того, що ви зрозумієте, що малося на увазі з пропозицією рекурсії, що міститься в концепції. Все-таки; у нас є тільки стільки слів, і ми говоримо про адреси посилань на адреси і таке, незабаром зупиняє більшість мізків за недійсним винятком оп-коду . І здебільшого наміри легко вибираються з контексту, тому повернемося до вулиці.

Поштові працівники цього нашого уявного міста дуже схожі на тих, кого ми знаходимо у «реальному» світі. Ніхто, ймовірно, не постраждає від інсульту, коли ви розмовляєте чи запитуєте про недійсну адресу, але кожен останній буде лаяти, коли ви попросите їх діяти на цій інформації.

Припустимо, на нашій особливій вулиці всього 20 будинків. Далі зробіть вигляд, що якась хибна чи дислексична душа скерувала лист, дуже важливий, на номер 71. Тепер ми можемо запитати у нашого перевізника Френка, чи є така адреса, і він просто і спокійно повідомить: ні . Ми навіть можемо очікувати , що він оцінити , наскільки далеко за межами вулиці це місце буде лежати , якщо вона дійсно існує: приблизно в 2,5 рази далі , ніж в кінці. Ніщо з цього не викличе у нього ніякого роздратування. Однак, якби ми попросили його доставити цей лист або забрати предмет з того місця, він, швидше за все, буде відвертим щодо свого незадоволення та відмови. виконувати його.

Покажчики - це лише адреси, а адреси - просто числа.

Перевірте висновок наступного:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

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

Тепер, оскільки покажчики - це просто цифри, їх неминуче справедливо порівнювати. В одному сенсі саме це демонструє ваш вчитель. Усі наступні твердження цілком справедливі - і належні! - C, і коли компіляція буде працювати без проблем , навіть якщо жоден вказівник не потребує ініціалізації, і значення, які вони містять, можуть бути невизначені :

  • Ми обчислюємо лише result чітко для ясності , і друкуємо його, щоб змусити компілятора обчислити те, що в іншому випадку було б зайвим, мертвим кодом.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Звичайно, програма неправильно формується, коли або a або b не визначено (читати: неправильно ініціалізовано ) в момент тестування, але це абсолютно не має значення для цієї частини нашої дискусії. Ці фрагменти, як і наступні твердження, гарантуються - «стандартним» - для компіляції та запуску бездоганно, незважаючи на IN- недійсність будь-якого вказівника.

Проблеми виникають лише тоді, коли недійсний покажчик буде відмежований . Коли ми просимо Френка забрати або доставити за недійсною, неіснуючою адресою.

Дано будь-який довільний вказівник:

int *p;

Хоча ця операція повинна компілювати та запускати:

printf(“%p”, p);

... як це має бути:

size_t foo( int *p ) { return (size_t)p; }

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

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Наскільки тонкі зміни? Різниця полягає в різниці між значенням покажчика - який є адреса, а значення змісту: будинки на цей номер. Жодна проблема не виникає, поки покажчик не буде відмежований ; поки не буде зроблена спроба отримати доступ до адреси, на яку він посилається. Намагаючись доставити або забрати пакет за межі ділянки дороги ...

У більш широкому сенсі , той же принцип обов'язково відноситься до більш складним прикладів, включаючи вищезгадану необхідність в створенні необхідної достовірності:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

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

У C масив - це суміжний буфер, безперебійний лінійний ряд пам'яті. Порівняння та арифметика, застосована до покажчиків, які посилаються на місця в такому сингулярному ряду, мають природний характер і, очевидно, значущі як стосовно один одного, так і до цього "масиву" (який просто ідентифікується базою). Точно те саме стосується кожного блоку, виділеного через malloc, або sbrk. Оскільки ці зв’язки є неявними , компілятор може встановити дійсні зв’язки між ними, а тому може бути впевнений, що розрахунки забезпечать очікувані відповіді.

Виконуючи подібну гімнастику на покажчиках, які посилаються на окремі блоки чи масиви, не пропонують такої притаманної та очевидної корисності. Тим більше, що будь-яке відношення існує в один момент, може бути визнано недійсним шляхом перерозподілу, що випливає, де це велика ймовірність змінитись, навіть перевернутись. У таких випадках компілятор не може отримати необхідну інформацію для встановлення впевненості, яку він мав у попередній ситуації.

Ви , однак, як програміст, може мати такі знання! І в деяких випадках це зобов’язано використовувати.

Там ЯВЛЯЮТЬСЯ Таким чином, обставини , при яких навіть це повністю ДІЙСНИЙ і зовсім PROPER.

Насправді, саме це mallocдоводиться робити всередині країни, коли настає час спробувати об'єднати меліоровані блоки - на переважній більшості архітектур. Те саме стосується і розподільника операційної системи, як і позаду sbrk; якщо більш очевидно , часто для більш розрізнених організацій, то більш критично - і доречно також на платформах, де цього mallocможе не бути. І скільки з них не написано на С?

Обґрунтованість, безпека та успішність дії неминуче є наслідком рівня розуміння, на якому вона передує та застосовується.

У запропонованих вами цитатах Керніган та Річі займаються тісно пов’язаною, але, тим не менш, окремою проблемою. Вони визначають ті обмеження на мову , і пояснити , як ви можете скористатися наявними можливостями компілятора , щоб захистити вас , принаймні виявлення потенційно помилкові конструкції. Вони описують довжини, на які механізм може розробитись , щоб допомогти вам у вирішенні завдань програмування. Укладач - твій слуга, ти - господар. Мудрий господар, однак, той, хто глибоко знайомий з можливостями різних своїх слуг.

У цьому контексті невизначена поведінка служить для вказівки на потенційну небезпеку та можливість заподіяння шкоди; не означати неминучої, незворотної приреченості чи кінця світу, як ми його знаємо. Це просто означає, що ми, «маючи на увазі компілятор», - не в змозі зробити будь-яку думку про те, якою може бути ця річ, або представляти, і з цієї причини ми вирішимо помити свої справи. Ми не будемо нести відповідальність за будь-які нещасні випадки, які можуть бути наслідком використання або неправильного використання цього засобу .

Насправді це просто говорить: "Поза цим моментом, ковбой : ти сам ..."

Ваш професор прагне продемонструвати вам найтонші нюанси .

Зауважте, яку велику обережність вони поставили під час створення їх прикладу; і як крихкий він все ще є. За адресою a, в

p[0].p0 = &a;

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

Просто зміна цього рядка:

char a = 0;

до цього:

char a = 1;  // or ANY other value than 0

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

Тепер код запрошує катастрофи.

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

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

p1Покажчик був инициализирован до блоку рівно 10 байт.

  • Якщо aвипадково буде розміщено в кінці блоку і процес не має доступу до наступного, наступне читання p0 [1] - призведе до сегмента за замовчуванням. Цей сценарій навряд чи в архітектурі x86, але можливий.

  • Якщо область за межами адреси a є доступною, чи не будуть відбуватися ніяких помилок читання, але програма все ще не врятована від нещастя.

  • Якщо нульовий байт трапиться протягом десяти, що починається за адресою a, він все одно може вижити, оскільки тоді strcpyвін зупиниться і, принаймні, ми не зазнаємо порушення запису.

  • Якщо він НЕ порушений для читання негаразд, але не нульовий байт не відбувається в цьому проміжку 10, strcpyбуде продовжувати і намагатися писати за межами блоку , виділеним malloc.

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

    • Ще більш катастрофічна - і тонка --- ситуація виникає , коли наступний блок знаходиться в власності процесу, то помилка не може бути виявлена, сигнал не може бути підвищена, і таким чином це може «з'явитися» ще «працювати» , хоча він фактично буде перезаписати інші дані, структури управління алокатора або навіть код (у певних операційних середовищах).

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

Тим не менш , програма все одно повинна складатись, оскільки вона залишається абсолютно дійсною та стандартною відповідною C.

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

Paranoid люди постійно прагнуть змінити на природу в C , щоб позбутися від цих проблемних можливостей і так врятувати нас від самих себе; але це нечесно . Це відповідальність, яку ми зобов’язані взяти на себе, коли вирішимо переслідувати владу та отримати свободу, яку нам пропонує більш прямий та всебічний контроль над машиною. Промоутери та переслідувачі досконалості у виконанні ніколи не приймуть нічого менше.

Переносність та загальність, яку він представляє, - це принципово окремий розгляд, і все , до чого прагне стандарт :

Цей документ визначає форму та встановлює інтерпретацію програм, виражених мовою програмування C. Її метою є сприяти портативності , надійності, ремонтопридатності та ефективному виконанню мовних програм C на різних обчислювальних системах .

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

Прийти до висновку:

  • Вивчення та маніпулювання самими покажчиками незмінно справедливі та часто плідні . Інтерпретація результатів може бути або не може бути осмисленою, але лихо ніколи не запрошується до вказівника відмежований ; поки не буде зроблена спроба отримати доступ до адреси, пов'язаної з цим.

Це не було правдою, програмуючи так, як ми це знаємо - і любимо - не було б можливим.


3
На жаль, ця відповідь, по суті, є неправдивою. Ви не можете нічого пояснити щодо невизначеної поведінки. Порівняння не потрібно проводити на рівні машини.
Антті

6
Ghii, насправді ні. Якщо ви подивитеся на додаток J 11 та 6.5.8, сам акт порівняння є UB. Перенаправлення є окремим питанням.
paxdiablo

6
Ні, UB все ще може бути шкідливим навіть перед тим, як покажчик буде скасовано. Компілятор вільний повністю оптимізувати функцію з UB в єдиний NOP, хоча це очевидно змінює видиму поведінку.
нанофарад

2
@Ghii, Додаток J (біт я згадував) список речей, які НЕ визначені поведінку, тому я не впевнений , як це підтримує ваш аргумент :-) 6.5.8 явно волає порівняння як UB. Щоб ваш коментар був суперкотом, порівняння не відбувається, коли ви друкуєте покажчик, так що ви, мабуть, праві, що він не вийде з ладу. Але це не те, про що запитували ОП. 3.4.3це також розділ, на який слід звернути увагу: він визначає UB як поведінку, "до якої цей Міжнародний стандарт не пред'являє жодних вимог".
paxdiablo

3
@GhiiVelte, ви продовжуєте констатувати речі, які явно не так, незважаючи на те, що вам вказували. Так, фрагмент, який ви опублікували, повинен скласти, але ваше твердження про те, що він працює без зачеплення, є невірним. Я пропоную вам прочитати стандарт, зокрема (в даному випадку) C11 6.5.6/9, маючи на увазі, що слово "повинен" вказує на вимогуL "Коли два покажчики віднімаються, обидва вказують на елементи одного об'єкта масиву або один минулий останній елемент об’єкта масиву ".
paxdiablo

-5

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

У вашому прикладі змінних, що виділяються стеком, компілятор вільний виділяти ці змінні регістрам або адресам пам'яті стека, і в будь-якому порядку вибирає це. Порівняння , такі як <і , >отже , не будуть відповідати за укладачам або архітектур. Однак ==і !=не настільки обмежений, порівняння рівності вказівників є дійсною і корисною операцією.


2
Стек слів з’являється рівно нуль разів у стандарті C11. А невизначена поведінка означає, що все може статися (включаючи збої програми).
paxdiablo

1
@paxdiablo Чи я це сказав?
nickelpro

2
Ви згадали змінні, виділені стеком. У стандарті немає стека, це лише деталь реалізації. Більш серйозним питанням у цій відповіді є твердження, що ви можете порівнювати покажчики без шансу на аварію - це просто неправильно.
paxdiablo

1
@nickelpro: Якщо ви хочете написати код, сумісний з оптимізаторами в gcc і clang, необхідно перестрибнути через безліч нерозумних обручів. Обидва оптимізатори будуть наполегливо шукати можливості зробити висновки про те, до чого до речі звертатимуться вказівники, коли є якийсь спосіб, коли Стандарт може бути скручений для їх виправдання (і навіть іноді, коли його немає). Беручи під увагу int x[10],y[10],*p;, якщо код оцінює y[0], потім оцінює p>(x+5)і записи *pбез зміни pв проміжний період , і , нарешті , оцінює y[0]знову ...
Supercat

1
nickelpro, погоджуйся погодитись не погоджуватися, але твоя відповідь досі принципово неправильна. Мені подобається ваш підхід до тих людей, які використовують (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')замість того, isalpha()що для чого розумна реалізація призведе до припинення цих персонажів? Суть полягає в тому, що, навіть якщо жодна з програм, які ви знаєте, не має проблем, вам слід якомога більше кодувати стандарт, якщо ви цінуєте портативність. Я вдячний етикетці "standard maven", хоча, за це дякую. Я можу
долучитись
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.