Какой провокационный вопрос!
Даже беглое сканирование ответов и комментариев в этой теме покажет, насколько эмоциональным окажется ваш, казалось бы, простой и понятный запрос.
Это не должно удивлять.
Бесспорно, недопонимание вокруг концепции и использования в указателях представляет собой доминирующую причину серьезных сбоев в программировании в целом.
Признание этой реальности становится очевидным в повсеместном распространении языков, разработанных специально для решения и, предпочтительно, чтобы избежать проблем, которые указатели вообще ставят. Думайте 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 в различных вычислительных системах .
Вот почему совершенно правильно отличать его от определения и технической спецификации самого языка. Вопреки тому , что многие , похоже, считают Общностью является антитезой к исключительным и образцовым .
Заключить:
- Изучение и манипулирование самими указателями неизменно является обоснованным и зачастую плодотворным . Интерпретация результатов может иметь или не иметь смысла, но бедствие никогда не приветствуется, пока указатель не будет разыменован ; пока не будет предпринята попытка получить доступ к адресу, связанному с.
Если бы это было не так, программирование в том виде, в котором мы его знаем - и нам это нравится - было бы невозможно.
C
с тем, что является безопасным вC
. Однако всегда можно выполнить сравнение двух указателей с одним и тем же типом (например, с помощью проверки на равенство), используя арифметику указателей и сравнение,>
и<
это безопасно только при использовании в данном массиве (или блоке памяти).