Детали реализации указателя в C

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

  • sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)

  • Представленное в памяти представление всех указателей для данной архитектуры одинаково независимо от типа данных, на который указывает.

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

  • Умножение и разделение типов данных указателя запрещается только компилятором. ПРИМЕЧАНИЕ. Да, я знаю, что это бессмысленно. Я имею в виду - есть ли аппаратная поддержка, чтобы запретить это неправильное использование?

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

  • Приращение указателя эквивалентно добавлению sizeof(the pointed data type) к адресу памяти, сохраненному указателем. Если p является int32*, то p+1 равен адресу памяти 4 байта после p.

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

Ответ 1

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

sizeof(int *) == sizeof(char *) == sizeof(void *) == sizeof(func_ptr *)

Я не знаю никаких систем, где я знаю, что это ложь, но подумайте:

Мобильные устройства часто имеют некоторое количество доступной для чтения памяти, в которой программный код и т.д. сохраняются. Значения, доступные только для чтения (константные переменные), возможно, могут храниться в постоянной памяти. И поскольку адресное пространство ROM может быть меньше, чем обычное адресное пространство RAM, размер указателя может быть другим. Аналогично, указатели на функции могут иметь разный размер, так как они могут указывать на эту постоянную память, в которую загружается программа, и которая в противном случае не может быть изменена (поэтому ваши данные не могут быть сохранены в ней).

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

Представленное в памяти представление всех указателей для данной архитектуры одинаково независимо от типа данных, на который указывает.

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

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

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

Зависит от того, как определена длина этого бита.:) int на многих 64-битных платформах все еще 32 бит. Но указатель - 64 бита. Как уже говорилось, процессор с сегментированной моделью памяти будет иметь указатели, состоящие из пары чисел. Аналогично, указатели элементов состоят из пары чисел.

Умножение и разделение типов данных указателя запрещается только компилятором.

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

Однако, чтобы ответить на то, что вы имеете в виду, посмотрите на процессоры Motorola 68000. Я считаю, что у них есть отдельные регистры для целых чисел и адресов памяти. Это означает, что они могут легко запретить такие бессмысленные операции.

Все значения указателя могут быть записаны в одно целое.

Ты там в безопасности. Стандарты C и С++ гарантируют, что это всегда возможно, независимо от расположения пространства памяти, архитектуры процессора и всего остального. В частности, они гарантируют отображение, определенное реализацией. Другими словами, вы всегда можете преобразовать указатель в целое число, а затем преобразовать это целое обратно, чтобы получить исходный указатель. Но языки C/С++ ничего не говорят о том, что должно быть промежуточным целым значением. Это зависит от конкретного компилятора и аппаратного обеспечения, на которое оно нацелено.

Приращение указателя эквивалентно добавлению sizeof (указанный тип данных) в адрес памяти, сохраненный указателем.

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

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

Наконец, как я упоминал в комментарии к вашему вопросу, имейте в виду, что С++ - это просто язык. Ему не важно, с какой архитектурой он скомпилирован. Многие из этих ограничений могут показаться неясными для современных процессоров. Но что, если вы ориентируетесь на прошлые процессоры? Что делать, если вы ориентируетесь на процессоры следующего десятилетия? Вы даже не знаете, как они будут работать, поэтому вы не можете много думать о них. Что делать, если вы нацеливаете виртуальную машину? Уже существуют компиляторы, которые генерируют байт-код для Flash, готовый к запуску с веб-сайта. Что делать, если вы хотите скомпилировать исходный код С++ для Python?

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

Ответ 2

У меня нет конкретных примеров реального мира, но "авторитет" - это стандарт С. Если в стандарте что-то не требуется, вы можете создать соответствующую реализацию, которая намеренно не соответствует каким-либо другим предположениям. Некоторые из этих предположений являются истинными большую часть времени только потому, что удобно реализовать указатель как целое число, представляющее адрес памяти, который может быть непосредственно извлечен процессором, но это просто следствие "удобства" и не может быть проведено как универсальная правда.

  • Не требуется по стандарту (см. этот вопрос). Например, sizeof(int*) может быть неравным с size(double*). void* гарантированно сможет сохранить любое значение указателя.
  • Не требуется по стандарту. По определению размер является частью представления. Если размер может быть другим, представление может быть другим.
  • Не обязательно. Фактически, "длина бит архитектуры" является неопределенным выражением. Что такое 64-битный процессор? Это адресная шина? Размер регистров? Шина данных? Что?
  • Не имеет смысла "размножать" или "делить" указатель. Это запрещено компилятором, но вы можете, разумеется, размножать или делить базовое представление (что на самом деле не имеет для меня смысла), и это приводит к поведению undefined.
  • Возможно, я не понимаю вашу точку зрения, но все на цифровом компьютере - это всего лишь двоичное число.
  • Да; вид. Он гарантированно указывает на местоположение, которое sizeof(pointer_type) дальше. Это не обязательно эквивалентно арифметическому добавлению числа (то есть дальше - это логическая концепция здесь. Фактическое представление является специфичным для архитектуры)

Ответ 3

Для 6.: указатель не обязательно является адресом памяти. См. Например: Заговор Великого указателя "пользователем Qaru jalf

Да, я использовал слово "адрес" в комментарии выше. Важно понять, что я имею в виду. Я не имею в виду "адрес памяти, на котором данные физически хранятся", а просто абстрактный "все, что нам нужно, чтобы найти значение. Адрес я может быть любым, но как только мы его получим, мы всегда можем найти и изменить i."

и

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

Ответ 4

Дополнительная информация о указателях из стандарта C99:

  • 6.2.5 §27 гарантирует, что void* и char* имеют одинаковые представления, т.е. они могут быть взаимозаменяемы без преобразования, т.е. тот же адрес обозначается одним и тем же шаблоном бита (который не обязательно должен быть истинным для других типов указателей)
  • 6.3.2.3 В § 1 указано, что любой указатель на неполный или тип объекта может быть перенесен в (и из) void* и обратно и все еще будет действительным; это не включает указатели на функции!
  • 6.3.2.3 §6 гласит, что void* можно отличить от (и от) целых чисел и 7.18.1.4. § 1 содержит подходящие типы intptr_t и uintptr_t; проблема: эти типы являются необязательными - стандарт явно указывает, что не должно быть целочисленного типа, достаточно большого, чтобы фактически удерживать значение указателя!

Ответ 5

sizeof(char*) != sizeof(void(*)(void)? - Не на x86 в режиме 36-разрядной адресации (поддерживается практически во всех процессорах Intel с Pentium 1)

"Представление указателя внутри памяти такое же, как целое число одной и той же длины бит" - нет представления в памяти в любой современной архитектуре; помеченная память никогда не попадала и уже устарела до того, как C была стандартизирована. На самом деле память даже не содержит целых чисел, просто битов и, возможно, слов (не байтов, большая физическая память не позволяет читать только 8 бит.)

"Умножение указателей невозможно" - 68000 семей; адресные регистры (те, которые содержат указатели) не поддерживают этот IIRC.

"Все указатели могут быть отнесены к целым числам" - не на PIC.

"Приращение T * эквивалентно добавлению sizeof (T) в адрес памяти" - true по определению. Также эквивалентно &pointer[1].

Ответ 6

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

Я думаю, что это предположение неверно, потому что на 80186, например, 32-разрядный указатель удерживается в двух регистрах (регистр смещения - регистр сегментов), и какое полуслое идет, в каком регистре имеет значение во время доступа.

Умножение и разделение типов данных указателя запрещается только компилятором.

Вы не можете размножать или делить типы.; Р

Я не уверен, почему вы хотели бы умножить или разделить указатель.

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

Стандарт C99 позволяет хранить указатели в intptr_t, который является целым типом. Итак, да.

Приращение указателя эквивалентно добавлению sizeof (указанный тип данных) в адрес памяти, сохраненный указателем. Если p является int32 *, то p + 1 равен адресу памяти 4 байта после p.

x + y где x является T *, а y является целым числом, равным (T *)((intptr_t)x + y * sizeof(T)), насколько я знаю. Выравнивание может быть проблемой, но дополнение можно указать в sizeof. Я не уверен.

Ответ 7

Я не знаю о других, но для DOS предположение в № 3 неверно. DOS 16 бит и использует различные трюки для отображения памяти более чем на 16 бит.

Ответ 8

В общем, ответ на все вопросы "да", и потому, что только те машины, которые реализуют популярные языки, непосредственно видели свет дня и сохранялись в нынешнем веке. Хотя языковые стандарты оставляют за собой право изменять эти "инварианты" или утверждения, это никогда не случалось в реальных продуктах, за исключением, возможно, пунктов 3 и 4, которые требуют некоторого повторения, чтобы быть универсальным.

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

Помимо сегментированных/функциональных MMU, которые часто имеют большие указатели, более экстремальные проекты пытались кодировать типы данных в указателях. Мало кто из них когда-либо строился. (Этот вопрос воспитывает все альтернативы базовым, ориентированным на слова, архитектуре с указателем и одним словом.)

В частности:

  • Внутри памяти представление всех указателей для данной архитектуры одинаково независимо от типа данных, на который указывает. Истина, за исключением крайне сумасшедших прошлых проектов, которые пытались реализовать защиту не на строго типизированных языках, а на аппаратных средствах.
  • Представление указателя внутри памяти такое же, как целое число с той же длиной бита, что и архитектура. Возможно, что-то вроде интегрального типа одно и то же, см. LP64 vs LLP64.
  • Умножение и разделение типов данных указателя запрещается только компилятором. Right.
  • Все значения указателя могут быть записаны в одно целое. Другими словами, какие архитектуры все еще используют сегменты и смещения? Ничто не использует сегменты и смещения сегодня, но C int часто недостаточно велик, вам может понадобиться long или long long, чтобы удерживать указатель.
  • Приращение указателя эквивалентно добавлению sizeof (указанный тип данных) в адрес памяти, сохраненный указателем. Если p является int32 *, то p + 1 равен адресу памяти 4 байта после p. Да.

Интересно отметить, что каждый процессор архитектуры Intel, т.е. каждый PeeCee, содержит сложную секцию сегментации эпической, легендарной сложности. Однако он эффективно отключен. Всякий раз, когда ОС ПК загружается, он устанавливает базы сегментов 0 и длины сегментов равными ~ 0, обнуляя сегменты и давая плоскую модель памяти.

Ответ 9

В 1950-х, 1960-х и 1970-х годах существовало множество архитектур с "адресованными адресами". Но я не могу вспомнить какие-либо основные примеры, в которых был компилятор C. Я вспоминаю ICL/Three Rivers PERQ machines в 1980-х годах, который был адресован словом, и имел доступный для записи контроллер (микрокод). В одном из его экземпляров был компилятор C и аромат Unix, называемый PNX, но компилятор C требовал специального микрокода.

Основная проблема заключается в том, что типы char * на машинах, написанных на языке, неудобны, однако вы их реализуете. Вы часто используете sizeof(int *) != sizeof(char *)...

Интересно, что до C существовал язык под названием BCPL, в котором основным типом указателя был адрес слова; то есть приращение указателя дало вам адрес следующего слова, а ptr!1 дал вам слово в ptr + 1. Был другой оператор для обращения к байту: ptr%42, если я помню.

Ответ 10

РЕДАКТИРОВАТЬ: Не отвечайте на вопросы, когда уровень сахара в крови низкий. Ваш мозг (конечно, мой) работает не так, как вы ожидаете.:-(

Малая нить:

p - int32 *, тогда p + 1

неверно, он должен быть неподписанным int32, иначе он будет завершен на 2 ГБ.

Интересная странность - я получил это от автора компилятора C для чипа Transputer - он сказал мне, что для этого компилятора NULL был определен как -2GB. Зачем? Поскольку у Transputer был подписанный диапазон адресов: от -2 до + 2 ГБ. Вы можете это сделать? Удивительно, не правда ли?

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

Я думаю, что большинство из нас может быть рад, что мы не работаем над транспьютерами!

Ответ 11

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

Я вижу, что Стивен С упоминал машины PERQ, а MSalters упомянул 68000 и PIC.

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

sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)?

Не обязательно. Некоторые примеры:

Большинство компиляторов для 8-битных процессоров Harvard-архитектуры - PIC и 8051 и M8C - делают sizeof (int *) == sizeof (char *), но отличается от sizeof (func_ptr *).

Некоторые из очень маленьких чипов в этих семействах имеют 256 байт ОЗУ (или меньше), но несколько килобайт PROGMEM (Flash или ROM), поэтому компиляторы часто делают sizeof (int *) == sizeof (char *) равный 1 (один 8-разрядный байт), но sizeof (func_ptr *), равный 2 (два 8-битных байта).

Компиляторы для многих более крупных чипов в семействах с несколькими килобайтами ОЗУ и 128 или около килобайт PROGMEM делают sizeof (int *) == sizeof (char *) равным 2 (два 8-битных байта), но sizeof (func_ptr *), равный 3 (три 8-битных байта).

Несколько микросхем Harvard-архитектуры могут хранить полностью 2 ^ 16 ( "64 Кбайт" ) PROGMEM (Flash или ПЗУ) и еще 2 ^ 16 ( "64 Кбайт" ) RAM + памяти с отображением ввода-вывода. Компиляторы для такого чипа make sizeof (func_ptr *) всегда равны 2 (два байта); но часто имеют способ сделать другие типы указателей sizeof (int *) == sizeof (char *) == sizeof (void *) в aa "long ptr" 3-байтовый общий указатель, который имеет дополнительный магический бит, который указывает, указывает ли этот указатель на RAM или PROGMEM. (Чтобы указать тип указателя, который вы должны передать функции print_text_to_the_LCD(), когда вы вызываете эту функцию из множества разных подпрограмм, иногда с адресом переменной строки в буфере, которая может находиться где угодно в ОЗУ, а иногда и с одним из многих постоянных строк, которые могут быть в любом месте PROGMEM). Такие компиляторы часто имеют специальные ключевые слова ( "короткие" или "близкие", "длинные" или "далекие" ), чтобы программисты конкретно указывали три разных типа указателей char в одной и той же программе - постоянные строки, для которых требуется всего 2 байта укажите, где в PROGMEM они расположены, непостоянные строки, которым требуется только 2 байта, чтобы указать, где они находятся в ОЗУ, и тип 3-байтовых указателей, которые принимает "print_text_to_the_LCD()".

Большинство компьютеров, построенных в 1950-х и 1960-х годах, используют 36-разрядную длину слова или 18-разрядная длина слова, с 18-разрядной (или менее) адресной шиной. Я слышал, что компиляторы C для таких компьютеров часто используют 9-битные байты, с sizeof (int *) == sizeof (func_ptr *) = 2, который дает 18 бит, поскольку все целые числа и функции должны быть выровнены по порядку; но sizeof (char *) == sizeof (void *) == 4, чтобы воспользоваться специальными инструкциями PDP-10, в которых хранятся такие указатели в полном 36-битном слове. Это полное 36-битное слово включает в себя 18-битный адрес слова и еще несколько бит в других 18-битах, которые (среди прочего) указывают положение бита заостренного символа внутри этого слова.

Представленное в памяти представление всех указателей для данной архитектуры то же самое независимо от типа данных, указанного на?

Не обязательно. Некоторые примеры:

В любой из архитектур, упомянутых выше, указатели бывают разных размеров. Итак, как они могут иметь "то же" представление?

Некоторые компиляторы в некоторых системах используют "дескрипторы" для реализации указателей символов и других указателей. Такой дескриптор отличается от указателя, указывающего на первый "char" в "char big_array[4000]", чем для указателя, указывающего на первый "char" в "char small_array[10]", которые, возможно, являются разными данными типы, даже если малый массив запускается в точно таком же месте в памяти, ранее занятой большим массивом. Дескрипторы позволяют таким машинам ловить и ловушки переполнения буфера, которые вызывают такие проблемы на других машинах.

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

Представление указателя внутри памяти такое же, как целое число такая же длина бит, что и архитектура?

Не обязательно. Некоторые примеры:

В "tagged architecture" машины, каждое слово памяти имеет некоторые биты, указывающие, является ли это целое число, или указателем, или что-то другое. С такими машинами просмотр битов тега подскажет вам, было ли это слово целым или указателем.

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

Умножение и разделение типов данных указателя запрещены компилятором. ПРИМЕЧАНИЕ. Да, я знаю, что это бессмысленно. Я имею в виду - есть ли аппаратная поддержка, чтобы запретить это неправильное использование?

Да, некоторые аппаратные средства напрямую не поддерживают такие операции.

Как уже упоминалось, команда "умножить" в 68000 и 6809 работает только с (некоторыми) "регистрами данных"; они не могут быть непосредственно применены к значениям в "адресных регистрах". (Компилятору было бы легко обойти такие ограничения - MOV эти значения из регистра адресов в соответствующий регистр данных, а затем использовать MUL).

Все значения указателя могут быть записаны в один тип данных?

Да.

Для memcpy() для правильной работы, в стандарте C указывается, что каждое значение указателя каждого типа может быть переведено в указатель void ( "void *" ).

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

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

Я не уверен.

Я подозреваю, что все значения указателя могут быть переданы в интегральные типы данных "size_t" и "ptrdiff_t", определенные в "<stddef.h>".

Приращение указателя эквивалентно добавлению sizeof (заостренные данные type) на адрес памяти, сохраненный указателем. Если p является int32 * то p + 1 равен адресу памяти 4 байта после p.

Непонятно, что вы здесь задаете.

Q: Если у меня есть массив какой-то структуры или примитивного типа данных (например, "#include <stdint.h> ... int32_t example_array[1000]; ..." ), и я увеличиваю указатель, который указывает на этот массив (например, "int32_t p = & example_array [99];... p ++;..." ), указывает ли теперь указатель на следующий следующий член этого массива, который является размером size4 (указанным типом данных) в дальнейшем?

A: Да, компилятор должен сделать указатель, после его приращения один раз указать следующий независимый последовательный int32_t в массиве, sizeof (указанный тип данных), далее в памяти, чтобы соответствовать стандартам.

Q: Итак, если p является int32 *, то p + 1 равен адресу памяти 4 байта после p?

A: Когда sizeof (int32_t) фактически равен 4, да. В противном случае, например, для некоторых переносимых по словам машин, включая некоторые современные DSP, где sizeof (int32_t) может равняться 2 или даже 1, тогда p + 1 равен адресу памяти 2 или даже 1 "C байтам" после p.

Q: Поэтому, если я беру указатель и передаю его в "int"...

A: Один тип "во всем мире ересь VAX".

Q:... и затем верните это "int" обратно в указатель...

A: Другой тип "Весь мир - ересь VAX".

Q: Поэтому, если я беру указатель p, который является указателем на int32_t, и перечислил его в некоторый целочисленный тип, который достаточно большой, чтобы содержать указатель, а затем добавить sizeof( int32_t ) к этому интегральному типу, а затем позже верните этот интегральный тип обратно в указатель - когда я все это сделаю, результирующий указатель будет равен p + 1?

Не обязательно.

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

Некоторые из компиляторов C для таких чипов набирают 2 символа в каждое слово, но для хранения int32_t требуется 2 таких слова, поэтому они сообщают, что sizeof( int32_t ) равно 4. (Я слышал слухи, что есть компилятор C для 24-bit Motorola 56000, который делает это).

Компилятор должен устраивать такие вещи, что выполнение "p ++" с указателем на int32_t увеличивает указатель на следующее значение int32_t. Для компилятора есть несколько способов сделать это.

Один стандартный способ - хранить каждый указатель на int32_t как "родной адрес слова". Поскольку для хранения одного значения int32_t требуется 2 слова, компилятор C компилирует "int32_t * p; ... p++" на некоторый язык ассемблера, который увеличивает значение указателя на 2. С другой стороны, если это делает "int32_t * p; ... int x = (int)p; x += sizeof( int32_t ); p = (int32_t *)x;", этот компилятор C для 56000, скорее всего, скомпилирует его на языке ассемблера, который увеличивает значение указателя на 4.

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

Несколько PIC и 8086 и другие системы имеют несмежное ОЗУ - несколько блоков ОЗУ по адресам, которые "упростили аппаратное обеспечение". С вводом-выводом памяти с отображением или вообще ничего не связано с пробелами в адресном пространстве между этими блоками.

Это еще более неудобно, чем кажется.

В некоторых случаях - например, с оборудованием для бит-диапазона, используемым для предотвращения проблем, вызванных read-modify-write - точный бит в ОЗУ можно прочитать или записать с использованием двух или более разных адресов.