Как работает сравнение указателей в C? Можно ли сравнивать указатели, которые не указывают на один и тот же массив?


33

В главе 5 K & R (язык программирования C, 2-е издание) я прочитал следующее:

Во-первых, указатели могут сравниваться при определенных обстоятельствах. Если 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.

(Мой профессор включает в себя заявление об отказе на экзамене, что вопросы касаются среды программирования Ubuntu Linux 16.04, 64-битная версия)

(примечание редактора: если SO допускает больше тегов, эта последняя часть будет требовать , и, возможно, . Если бы вопрос / класс был конкретно о деталях реализации ОС низкого уровня, а не о переносимых C.)


17
Вы , может быть запутанным , что действует в Cс тем, что является безопасным в C. Однако всегда можно выполнить сравнение двух указателей с одним и тем же типом (например, с помощью проверки на равенство), используя арифметику указателей и сравнение, >и <это безопасно только при использовании в данном массиве (или блоке памяти).
Адриан Моул

13
Кроме того, вы не должны учиться C от K & R. Для начала, язык претерпел много изменений с тех пор. И, если честно, пример кода там был со времени, когда ценилась краткость, а не читабельность.
paxdiablo

5
Нет, работать не гарантировано. На практике это может дать сбой на машинах с сегментированной памятью. См. Есть ли в C эквивалент std :: less из C ++? На большинстве современных машин это будет работать, несмотря на UB.
Питер Кордес

6
@ Adam: Закрыть, но на самом деле это UB (если только компилятор, который использовал OP, GCC, не определил его. Возможно). Но UB не означает «определенно взрывается»; одно из возможных действий UB работает так, как вы ожидали! Это то, что делает UB таким противным; он может работать правильно в отладочной сборке и не работать с включенной оптимизацией, или наоборот, или зависать в зависимости от окружающего кода. Сравнение других указателей все равно даст вам ответ, но язык не определяет, что этот ответ будет означать (если что-нибудь). Нет, сбой разрешен. Это действительно UB.
Питер Кордес

3
@ Адам: О да, не обращайте внимания на первую часть моего комментария, я неправильно понял Ваш. Но вы утверждаете, что сравнение других указателей все равно даст вам ответ . Это не правда. Это был бы неопределенный результат , а не полный UB. UB намного хуже и означает, что ваша программа может вызвать segfault или SIGILL, если выполнение достигает этого оператора с этими входами (в любой момент до или после того, как это действительно происходит). (Правдоподобно на x86-64, если UB виден во время компиляции, но в целом может произойти все что угодно.) Часть UB состоит в том, чтобы позволить компилятору делать «небезопасные» предположения при генерации asm.
Питер Кордес

Ответы:


33

Согласно стандарту C11 , реляционные операторы <, <=, >, и >=могут быть использованы только на указатели на элементы одного и того же массива или структуры объекта. Это прописано в разделе 6.5.8p5:

Когда сравниваются два указателя, результат зависит от относительного расположения в адресном пространстве указанных объектов. Если два указателя на типы объектов оба указывают на один и тот же объект или оба указывают один за последним элементом одного и того же объекта массива, они сравниваются одинаково. Если указанные объекты являются членами одного и того же агрегатного объекта, указатели на элементы структуры, объявленные позже, сравниваются больше, чем указатели, на элементы, объявленные ранее в структуре, а указатели на элементы массива с большими значениями нижнего индекса сравниваются больше, чем указатели, на элементы того же массива. с более низкими значениями индекса. Все указатели на члены одного и того же объекта объединения сравниваются одинаково.

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

В вашем конкретном случае, как для сравнения адресов двух локальных переменных, так и адреса локального и динамического адреса, операция «работала», однако результат мог измениться, внеся, казалось бы, несвязанные изменения в ваш код или даже компилирование одного и того же кода с различными настройками оптимизации. С неопределенным поведением то, что код может привести к сбою или появлению ошибки, не означает, что это произойдет .

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

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

Тем не менее, даже с ==и !=вы можете получить некоторые неожиданные, но все еще четко определенные результаты. См. Может ли сравнение равенства не связанных между собой указателей оценить как истинное? для более подробной информации об этом.

Что касается вопроса об экзамене, заданного вашим профессором, он делает ряд ошибочных предположений:

  • Существует модель плоской памяти, в которой между адресом и целочисленным значением существует взаимно-однозначное соответствие.
  • Что преобразованные значения указателя вписываются в целочисленный тип.
  • Эта реализация просто обрабатывает указатели как целые числа при выполнении сравнений без использования свободы, предоставляемой неопределенным поведением.
  • Что стек используется и что локальные переменные хранятся там.
  • Эта куча используется для извлечения выделенной памяти.
  • Что стек (и, следовательно, локальные переменные) появляется по более высокому адресу, чем куча (и, следовательно, размещенные объекты).
  • Эти строковые константы появляются по более низкому адресу, чем куча.

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

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


3
@Shisui Даже учитывая это, вы все равно не должны зависеть от результатов. Компиляторы могут быть очень агрессивными, когда дело доходит до оптимизации, и будут использовать неопределенное поведение как возможность сделать это. Возможно, что использование другого компилятора и / или разных настроек оптимизации может привести к разным результатам.
dbush

2
@Shisui: В общем, это будет работать на машинах с плоской моделью памяти, например, x86-64. Некоторые компиляторы для таких систем могут даже определять поведение в своей документации. Но если нет, то «безумное» поведение может произойти из-за UB, видимого во время компиляции. (На практике я не думаю, что кто-то этого хочет, так что это не то, что ищут традиционные компиляторы и «пытаются сломать».)
Peter Cordes

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;реализацию ...
суперкат

12

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

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

Нет, результат зависит от реализации и других непредсказуемых факторов.

Также pt> px, потому что оба указателя указывают на переменные, хранящиеся в стеке, и стек увеличивается, так что адрес памяти t больше, чем адрес x? Вот почему pt> px верно?

Там не обязательно стек . Когда оно существует, оно не должно расти. Это может вырасти. Это может быть несмежно каким-то странным образом.

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

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

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

Во всех остальных случаях поведение не определено.

Последнее предложение важно. Хотя я сократил некоторые несвязанные случаи для экономии места, для нас важен один случай: два массива, а не часть одного и того же объекта struct / aggregate 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 на eval.


3
Не определено реализацией, не определено! Подумайте об этом так, первое может варьироваться между реализациями, но реализации должны документировать, как определяется поведение. Последнее поведение средства может изменяться в любой форме и реализация не должен сказать вам приседать :-)
paxdiablo

1
@paxdiablo: Согласно обоснованию авторов Стандарта, «неопределенное поведение ... также определяет области возможного соответствующего расширения языка: разработчик может расширить язык, предоставив определение официально неопределенного поведения». Далее в обосновании говорится: «Цель состоит в том, чтобы дать программисту реальный шанс создать мощные программы на Си, которые также являются очень переносимыми, и при этом они не кажутся совершенно полезными программами на Си, которые оказываются непереносимыми, то есть строгое наречие». Коммерческие авторы компиляторов понимают это, но некоторые другие авторы компиляторов не понимают.
суперкат

Существует еще один аспект, определенный реализацией; сравнение указателей подписано , поэтому в зависимости от машины / ОС / компилятора некоторые адреса могут интерпретироваться как отрицательные. Например, 32-битный компьютер, который поместил стек в 0xc << 28, скорее всего, покажет автоматические переменные по адресу арендодателя, чем куча или родата.
Мевец

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

Я не знаю; стандарт С вращается вокруг девятого круга Данте, молясь об эвтаназии. ОП специально ссылался на K & R и экзаменационный вопрос. #UB - мусор из ленивой рабочей группы.
Мевец

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[], но стандарт не прямо сказать , что байты структуры являются (или могут быть использованы в качестве) объект массива.
Суперкат

-4

Какой провокационный вопрос!

Даже беглое сканирование ответов и комментариев в этой теме покажет, насколько эмоциональным окажется ваш, казалось бы, простой и понятный запрос.

Это не должно удивлять.

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

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

Поэтому более глубокое понимание основополагающих принципов должно быть уместным для каждого человека, стремящегося к совершенству в программировании, особенно на системном уровне .

Я полагаю, это именно то, что ваш учитель хочет продемонстрировать.

И природа C делает его удобным средством для этого исследования. Менее ясно, чем сборка - хотя, возможно, более легко понятное - и все же гораздо более явно, чем языки, основанные на более глубокой абстракции среды выполнения.

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

Проблемы возникают только при разыменовании неверного указателя . Когда мы просим Фрэнка забрать или доставить по неверному, несуществующему адресу.

Дан любой произвольный указатель:

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.

    • Если эта область не принадлежит процессу, segfault должен быть немедленно запущен.

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

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

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

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

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

Переносимость и общность, которую он представляет, является принципиально отдельным соображением, и все, что стандарт стремится решить:

В этом документе указывается форма и устанавливается интерпретация программ, выраженная на языке программирования C. Его цель - способствовать переносимости , надежности, удобству обслуживания и эффективному выполнению программ на языке C в различных вычислительных системах .

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

Заключить:

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

Если бы это было не так, программирование в том виде, в котором мы его знаем - и нам это нравится - было бы невозможно.


3
Этот ответ, к сожалению, по своей сути недействителен. Вы не можете рассуждать о неопределенном поведении. Сравнение не нужно делать на уровне машины.
Антти Хаапала

6
Гии, на самом деле нет. Если вы посмотрите на C11 Приложение J и 6.5.8, сам акт сравнения - UB. Разыменование является отдельной проблемой.
paxdiablo

6
Нет, UB все еще может быть вредным даже до разыменования указателя. Компилятор может полностью оптимизировать функцию с UB в одну NOP, даже если это явно меняет видимое поведение.
нанофарад

2
@Ghii, Приложение J (бит, который я упомянул) - это список вещей, которые имеют неопределенное поведение, поэтому я не уверен, как это поддерживает ваш аргумент :-) 6.5.8 явно вызывает сравнение как UB. Что касается вашего комментария к supercat, то при печати указателя сравнения не происходит, поэтому вы, вероятно, правы, что он не потерпит крах. Но это не то, о чем спрашивал ОП. 3.4.3это также раздел, на который вам следует обратить внимание: он определяет UB как поведение, «к которому настоящий международный стандарт не предъявляет никаких требований».
paxdiablo

3
@GhiiVelte, ты продолжаешь заявлять о вещах, которые просто ошибочны, несмотря на то, что тебе на это указывают. Да, размещенный вами фрагмент должен компилироваться, но ваше утверждение о том, что он работает без заминки, неверно. Я предлагаю вам на самом деле прочитать стандарт, в частности (в данном случае) C11 6.5.6/9, помня, что слово «должен» указывает на требование «Когда вычитаются два указателя, оба должны указывать на элементы одного и того же объекта массива или один после последнего элемент массива объекта ".
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]снова, ...
суперкат

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