Переменные-указатели - это просто целые числа с некоторыми операторами или они являются символическими?

РЕДАКТИРОВАТЬ: исходный выбор слова сбивал с толку. Термин «символический» намного лучше оригинального («мистического»).

При обсуждении моего предыдущего вопроса о C ++ мне сказали, что указатели

Звучит неверно! Если ничто не является символическим и указатель является его представлением, я могу сделать следующее. Могу я?

#include <stdio.h>
#include <string.h>

int main() {
    int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
    if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
        printf ("pa1 == pb\n");
        *pa1 = 2;
    }
    else {
        printf ("pa1 != pb\n");
        pa1 = &a[0]; // ensure well defined behaviour in printf
    }
    printf ("b = %d *pa1 = %d\n", b, *pa1);
    return 0;
 }

Это вопрос C и C ++.

Тестирование с помощью Compile and Execute C Online с GNU GCC v4.8.3: gcc -O2 -Wall дает

pa1 == pb                                                                                                                                                                                       
b = 1 *pa1 = 2    

Тестирование с помощью Compile and Execute C ++ Online с GNU GCC v4.8.3: g++ -O2 -Wall

pa1 == pb                                                                                                                                                                                       
b = 1 *pa1 = 2        

Итак, модификация b через (&a)[1] не удалась с GCC на C и C ++.

Конечно, хотелось бы получить ответ на основе стандартных цитат.

РЕДАКТИРОВАТЬ: Чтобы ответить на критику UB на &a + 1, теперь a представляет собой массив из 1 элемента.

Связанный: Разыменование указатель вне границы, содержащий адрес объекта (массив массива)

Дополнительное примечание: термин «мистический» был впервые использован, я думаю, Тони Делроем здесь. Я был неправ, одолжив его.


person curiousguy    schedule 17.08.2015    source источник
comment
В вашем примере кода есть UB.   -  person πάντα ῥεῖ    schedule 17.08.2015
comment
Компилятор может свободно упорядочивать переменные, чтобы ваш код мог работать так, как вы ожидаете, а может и нет. Это неопределенное поведение.   -  person Jabberwocky    schedule 17.08.2015
comment
[expr.add] / 5 [для добавления указателя,] если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или на один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено.   -  person TartanLlama    schedule 17.08.2015
comment
@TartanLlama Если это имеет значение, я заменил a на массив.   -  person curiousguy    schedule 17.08.2015
comment
Разыменование &a + 1 не определено, и компилятор может предположить, что это не изменяет b, а вместо этого вставляет значение b.   -  person molbdnilo    schedule 17.08.2015
comment
@curiousguy: Почему? Потому что стандарт не требует от компилятора упорядочивания переменных определенным образом.   -  person Jabberwocky    schedule 17.08.2015
comment
@curiousguy это не имеет значения, b не является элементом массива, поэтому поведение не определено.   -  person TartanLlama    schedule 17.08.2015
comment
@molbdnilo Значит, два указателя с одинаковыми значениями могут иметь разные семантические значения?   -  person curiousguy    schedule 17.08.2015
comment
@curiousguy Да, неверный указатель имеет другую семантику, чем действительный. В частности, разыменование недопустимого указателя делает всю вашу программу неопределенной.   -  person molbdnilo    schedule 17.08.2015
comment
@molbdnilo Что такое недействительный указатель?   -  person curiousguy    schedule 17.08.2015
comment
@curiousguy: указатель недействителен, если он не указывает на объект, член массива или один за концом массива.   -  person Zan Lynx    schedule 17.08.2015
comment
@ZanLynx При замене a на массив из 1 int, указатель действителен.   -  person curiousguy    schedule 17.08.2015
comment
@curiousguy: Вам разрешено ставить указатель на один за концом. Но вам не разрешено разыменовать его. Там ничего нет. Кроме того, компилятору разрешено смотреть на использование указателя и уменьшать все, что он видит. Итак, вы объявляете b и объявляете указатели. Но компилятор может удалить все это и фактически сократить всю вашу программу до одного оператора печати, если он того пожелает.   -  person Zan Lynx    schedule 17.08.2015
comment
@curiousguy значение указателя на гипотетический элемент после того, как массив четко определен, но разыменование его является неопределенным поведением.   -  person TartanLlama    schedule 17.08.2015
comment
@ZanLynx Значит, указатель - это больше, чем его битовый шаблон.   -  person curiousguy    schedule 17.08.2015
comment
@curiousguy: На x86 и x64 это битовый шаблон. Компилятор предполагает, что весь код следует правилам, и может не заметить, что вы изменили битовый шаблон. Или он может переместить вещи в регистры и полностью удалить указатели, в результате чего ваша умная вещь исчезнет. Если вы не следуете правилам, оптимизация компилятора уничтожит вас.   -  person Zan Lynx    schedule 17.08.2015
comment
@curiousguy Да, это больше, чем битовый шаблон, даже если битовый шаблон - это все представление. А также ints, floats и все остальное. Использование значения неинициализированного объекта int также не определено, независимо от хранимого им битового шаблона.   -  person molbdnilo    schedule 17.08.2015
comment
@ZanLynx он может не заметить, что вы изменили битовый шаблон Я не   -  person curiousguy    schedule 17.08.2015
comment
@Jabberwocky Потому что стандарт не требует от компилятора упорядочивания переменных определенным образом. Конечно, компилятор может рандомизировать адреса полных объектов. Но затем во время каждого запуска программы однажды установленные адреса четко определены и могут использоваться для математических вычислений, если адрес - это просто число. Когда компилятор разместил объекты в памяти, он принимает это решение, по крайней мере, во время выполнения этой программы, и я могу играть.   -  person curiousguy    schedule 07.06.2018
comment
@molbdnilo Согласитесь, что два указателя с одинаковым значением либо действительны, либо оба недействительны?   -  person curiousguy    schedule 07.06.2018
comment
@ZanLynx Кроме того, компилятору разрешено смотреть на использование вашего указателя и сокращать все, что он видит Это вопрос юриста по языку. Пожалуйста, предоставьте цитату.   -  person curiousguy    schedule 15.06.2018
comment
@curiousguy Это правило «как если бы», см. en.cppreference.com/w/cpp / language / as_if и stackoverflow.com/a/15718279/13422 там есть ссылка на части стандарта C ++ 11.   -  person Zan Lynx    schedule 15.06.2018
comment
Правило «как если бы» в основном определяет, какие преобразования разрешено выполнять реализации в законной программе C ++ Да, и никто не смог указать на правило, явно разрешающее это преобразование.   -  person curiousguy    schedule 02.07.2018


Ответы (4)


C был задуман как язык, в котором указатели и целые числа были очень тесно связаны, причем точное соотношение зависело от целевой платформы. Связь между указателями и целыми числами сделала язык очень подходящим для целей низкоуровневого или системного программирования. Поэтому для целей обсуждения ниже я назову этот язык «Низкоуровневый C» [LLC].

Комитет по стандартам C написал описание другого языка, где такие отношения прямо не запрещены, но не признаются каким-либо полезным образом, даже когда реализация генерирует код для поля цели и приложения. где такие отношения были бы полезны. Я назову этот язык «Только высокий уровень C» [HLOC].

В те дни, когда был написан Стандарт, большинство вещей, которые называли себя реализациями C, обрабатывали диалект LLC. Большинство полезных компиляторов обрабатывают диалект, который определяет полезную семантику в большем количестве случаев, чем HLOC, но не так много, как LLC. Будут ли указатели вести себя больше как целые числа или как абстрактные мистические сущности, зависит от того, какой именно диалект используется. Если кто-то занимается системным программированием, разумно рассматривать C как обрабатывающий указатели и целые числа как тесно связанные, потому что диалекты LLC, подходящие для этой цели, делают это, а диалекты HLOC, которые этого не делают, не подходят для этой цели. Однако при выполнении высокопроизводительного вычисления чисел гораздо чаще используются диалекты HLOC, которые не распознают такую ​​взаимосвязь.

Настоящая проблема и источник стольких разногласий заключается в том факте, что LLC и HLOC становятся все более расходящимися, и все же оба упоминаются под именем C.

person supercat    schedule 14.06.2018

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

c ++ (и c) - это языки общего назначения, созданные с целью обеспечения переносимости. т.е. правильно сформированная программа, написанная на C ++ в одной системе, должна работать в любой другой (запрещая обращения к системным службам).

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

Например, я писал код в системе 6809, где половина памяти выгружалась через PIA, адресуемую в невыгружаемой части карты памяти. Мой компилятор c смог справиться с этим, потому что указатели для этого компилятора были «мистическим» типом, который знал, как писать в PIA.

Семейство 80386 имеет режим адресации, при котором адреса организованы в группы по 16 байтов. Посмотрите FAR указатели, и вы увидите другую арифметику указателей.

Это история развития указателей в C ++. Не все производители микросхем ведут себя «хорошо», и язык позволяет их всех (обычно) без необходимости переписывать исходный код.

person Richard Hodges    schedule 17.08.2015
comment
Сгенерированный компилятор просто является иллюстрацией того факта, что GCC не поддерживает эту безумную идею. Он не используется как доказательство чего-либо, и он не работает с модифицированным кодом (с массивом). - person curiousguy; 18.08.2015
comment
C был разработан таким образом, чтобы язык мог быть перенесен на многие машины, и чтобы программист, знакомый с C и знакомый с общими характеристиками конкретной архитектуры, мог писать код C для этой архитектуры. Дизайн языка враждебен написанию архитектурно-независимого кода. С другой стороны, причина популярности C заключается в том, что он не пытался быть одним языком, а вместо этого представлял собой семейство диалектов, которые могли бы использовать различные сильные стороны разных архитектур. - person supercat; 12.07.2018
comment
@supercat при написании. Дизайн языка враждебен написанию архитектурно-независимого кода. Я должен сказать, что это противоречит моему жизненному опыту. Как написано выше, я написал C для систем на базе Z80, 6502, 6809, 68000, 80x86 и TMS9900, как с выгружаемой памятью, так и без нее, а также со всеми видами сопоставлений ввода-вывода. Язык C (и пара макросов переносимости) позволял компилировать один и тот же исходный код в функциональные программы (и мини-ОС) для всех этих систем. Единственными точками настройки были несколько определений макросов, драйверов устройств и карт компоновщика. - person Richard Hodges; 12.07.2018
comment
@RichardHodges: Есть разница между написанием кода, который будет работать на конкретном наборе архитектур, и написанием кода, который действительно не зависит от архитектуры. Препроцессор может очень помочь с проблемами переносимости, но язык, разработанный для облегчения архитектурно-независимого кода, будет указывать, что математика будет вести себя как дополнение до двух, даже если это означает использование беззнаковой математики в базовой архитектуре и последующее добавление кода для обработки ситуации, когда он ведет себя не так, как подписанный. В нем также будут указаны независимые от архитектуры правила продвижения для .. - person supercat; 12.07.2018
comment
... типы фиксированного размера. Было бы интересно написать реализацию Java для чего-то вроде 36-битной машины, но если платформа поддерживает сравнение и обмен или если реализация работает на одном ядре и может управлять планированием своих потоков, я думаю, что это было бы можно достичь приличной производительности. Напротив, большинство программ C, написанных для обычных микропроцессоров, были бы совершенно бесполезны на 36-битной машине. - person supercat; 12.07.2018
comment
@supercat Я согласен с тем, что не все программы на C написаны хорошо. Стоит отметить, что компиляторы C существовали для архитектур DEC и IBM, которые имели 9-битные символы и 36-битные слова. Именно по этой причине интегральный размер шрифта в C был намеренно расплывчатым. Авторы переносимых программ, как правило, не стремятся зависеть от поведения целочисленного переполнения. - person Richard Hodges; 12.07.2018
comment
@RichardHodges: Я написал стек TCP на платформе с 16-битным символом, и использование языка, который был очень похож на обычный C, за исключением 16-битного символа, определенно было лучше, чем писать все в ассемблерном коде TMS3205x. , но язык никак не помог сделать мой код архитектурно-независимым. Язык, предназначенный для написания архитектурно-независимого кода, должен включать типы данных с архитектурно-независимой семантикой, даже если их нужно эмулировать или даже сделать определенные программы несовместимыми с некоторыми платформами. Для производительности это может также ... - person supercat; 12.07.2018
comment
... иметь собственные типы данных, но моя работа была бы намного проще, если бы существовали средства объявления 16-битных данных, хранящихся в виде двух октетов с прямым порядком байтов, и компилятор сгенерировал бы код, который разделил бы запись такого значение в две записи размером с символ [с использованием нижних 8 бит каждого символа]. Если бы такой тип существовал, стек TCP для ПК, который использовал такие типы, можно было бы легко перенести в часть TMS. Некоторые из них могли работать неприемлемо медленно с использованием таких эмулированных типов, и поэтому их нужно было вручную настраивать для использования собственных типов, но это было бы лучше ... - person supercat; 12.07.2018
comment
@supercat не может с этим не согласиться. Мне пришлось определить псевдотипы для таких понятий, как индексирование в массив, поскольку подписанные / беззнаковые типы 16/8 бит подразумевали значительную разницу в производительности и пространственных характеристиках между Z80, 6809 и т. Д. Тем не менее, конечный результат был 100% переносимым всего за 2 часа. конфигурации. - person Richard Hodges; 13.07.2018
comment
@RichardHodges: Возможно, он был на 100% переносимым среди качественных универсальных реализаций, подходящих для низкоуровневого программирования на определенном подмножестве платформ, но он не был бы переносимым в том смысле, в котором стандарт использует этот термин, и не обязательно был бы надежно переносится среди современных компиляторов для этих платформ. - person supercat; 13.07.2018

Похищение цитаты из TartanLlama:

[expr.add] / 5 "[для добавления указателя,] если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или на один за последним элементом объекта массива, оценка не должна вызывать переполнения; в противном случае поведение не определено. "

Таким образом, компилятор может предположить, что ваш указатель указывает на массив a или на один за концом. Если он указывает кому-то за конец, вы не можете отнестись к нему с уважением. Но, как вы это делаете, он, конечно же, не может быть дальше конца, поэтому он может быть только внутри массива.

Итак, теперь у вас есть код (сокращенный)

b = 1;
*pa1 = 2;

где pa указывает внутри массива a, а b - отдельная переменная. И когда вы их распечатываете, вы получаете точно 1 и 2, значения, которые вы им присвоили.

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

person Bo Persson    schedule 17.08.2015
comment
Если он указывает на один за концом, вы не можете уважать его Это не ясно; что значит точка? - person curiousguy; 18.08.2015
comment
Вы хорошо знаете, что это значит. Он содержит адрес гипотетического a[N], т.е. если бы массив был на 1 элемент больше, он указывал бы на последний элемент. Настоящие вопросы: почему вы задали так много вопросов об этой концепции? Было бы полезно для чего-нибудь, если бы не UB? - person underscore_d; 27.02.2016
comment
@underscore_d Если указатель является тривиальным типом, то два указателя с одинаковым представлением должны указывать на один и тот же набор вещей. Таким образом, один за конечным указателем с тем же представлением, что и указатель на объект после массива, должен указывать на этот объект. Вы делаете вид, что указатели на самом деле не тривиальные типы? - person curiousguy; 14.06.2018
comment
Я ни на что не претендую, но вы, кажется, притворялись, что существует второй указатель, хотя в этом обсуждении ничего не было. но не до разыменования. - person underscore_d; 14.06.2018
comment
@underscore_d Вы утверждаете, что указатель не может находиться сразу за концом и указывать на объект? Я борюсь с этим. - person curiousguy; 02.07.2018
comment
@curiousguy - Да, наверное, он так говорит. Если у вас есть указатель и вы переместите его за конец, он больше не будет указывать на объект. Теперь у вас также может быть какой-нибудь другой указатель, который указывает на объект, и этот объект может иметь тот же адрес, что и последний. Но они по-прежнему являются разными указателями и не взаимозаменяемы. - person Bo Persson; 02.07.2018
comment
@BoPersson У меня нет проблем с идеей, что два объекта могут быть равны при любом разрешенном программном измерении и при этом быть разными. (Это просто означает, что средства измерения ограничены.) Труднее согласиться с тем, что два объекта, хранящие значения, могут быть разными, если они хранят одинаковые значения. Мы знаем, что указатели являются объектами хранения значений во всех используемых в настоящее время компиляторах. В представлении указателя нет скрытого флага, который не мог бы измерить ==. (Это может быть подтверждено memcmp.) Это моя трудность. - person curiousguy; 02.07.2018
comment
Мало того, мы также знаем, что значение указателя может быть преобразовано в целое число и обратно в указатель, поэтому целое число должно полностью представлять полное значение указателя. Таким образом, два указателя с одинаковым представлением будут преобразованы в равные целочисленные значения. Вы хотите сказать, что целые числа могут иметь одно и то же значение, но при этом различаться? - person curiousguy; 02.07.2018
comment
@curiousguy - Таковы правила. :-) Правила были установлены в то время, когда сегментированная память еще была обычным явлением. И сегменты могли перекрываться, поэтому memcmp не было надежным - разные битовые комбинации segment:offset могли означать один и тот же адрес. И наоборот - с массивами, размещенными в отдельных сегментах, один и тот же битовый шаблон указателя означал разные объекты в зависимости от того, какой сегмент использовался в качестве основы. - person Bo Persson; 02.07.2018
comment
@BoPersson Я понимаю, что данное значение типа может иметь много разных представлений. Это также может быть в случае с классом дробей, где различные представления дробей неотличимы при любом разрешенном измерении, но при этом не сравниваются равные через memcmp, что не является допустимым измерением для такого типа. Но две дроби с одинаковым представлением должны быть равны. Это подразумевается тем фактом, что внутренняя ценность объекта дроби определяется ТОЛЬКО состоянием его членов. - person curiousguy; 02.07.2018
comment
один и тот же битовый шаблон указателя означал разные объекты Как компиляции удалось бы осуществить доступ к нужному объекту при неоднозначном значении указателя? - person curiousguy; 02.07.2018
comment
@curiousguy - Это часть segment:offset адресации. Сегментную часть нужно было загрузить в сегментный регистр, а затем вы могли использовать только смещение в качестве указателя на массив, хранящийся в этом сегменте. Чтобы перейти к другому массиву, компилятор должен перезагрузить регистр сегмента, а затем использовать другой набор указателей. - person Bo Persson; 02.07.2018
comment
@curiousguy: за исключением случаев использования указателей huge (которые используются редко, потому что они очень медленные и неэффективные), все обращения к конкретному объекту будут использовать один и тот же сегмент, и компилятор будет предполагать, что два указателя с разными сегментами не могут идентифицировать тот же объект или его части. Следовательно, отдельные объекты обычно ограничиваются 65520 (т.е. 65536-16) байтами. Ответ на вопрос, когда компилятор должен изменить сегментную часть не огромного указателя на объект, прост: никогда. - person supercat; 13.07.2018

Если вы выключите оптимизатор, код работает должным образом.

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

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

Изменить:

Пересмотренный код по-прежнему UB. Стандарт не позволяет ссылаться на a[1], даже если значение указателя совпадает с другим значением указателя. Таким образом, оптимизатору разрешено сохранять значение b в регистре.

person Klas Lindbäck    schedule 17.08.2015
comment
Комментарии не подлежат расширенному обсуждению; этот разговор был переехал в чат. - person Martijn Pieters; 18.08.2015
comment
Оптимизаторы в gcc и clang рассматривают указатели как мистические. Они также рассматривают значения типа uintptr_t как мистические. Если int *p может использоваться для доступа к объекту и int *q имеет тот же битовый шаблон, но не может использоваться для идентификации объекта, оптимизатор gcc в некоторых случаях даже зайдет так далеко, чтобы сказать предположить в некоторых случаях, когда известно, что uintptr_t uptr равно (uintptr_t)q, доступ к (int*)uptr не повлияет на *p, даже если значение в uptr фактически получено из (uintptr)p. - person supercat; 13.07.2018
comment
@supercat даже если значение (...) когда это произойдет? - person curiousguy; 14.04.2019
comment
@curiousguy: данный #include <stdint.h> extern int x,y[]; int test(uintptr_t z) { x = 1; if (z == (uintptr_t)(1+y)) { *(int*)z=2; } return x; } gcc проигнорирует возможность того, что *(int*)z может идентифицировать x, даже если поведение test((uintptr_t)&x) должно быть определено как всегда либо возвращающее 1 без побочного эффекта, либо записывающее 2 в x и затем возвращающее 2. - person supercat; 15.04.2019