Какого типа данные хранятся в языке Си?

30

Я знаю, что указатели содержат адреса. Я знаю, что типы указателей «общеизвестны» на основе «типа» данных, на которые они указывают. Но указатели по-прежнему являются переменными, и адреса, которые они содержат, должны иметь тип данных. Согласно моей информации, адреса в шестнадцатеричном формате. Но я до сих пор не знаю, какой "тип" данных это шестнадцатеричный. (Обратите внимание, что я знаю, что такое шестнадцатеричное число, но когда вы 10CBA20, например, говорите , является ли эта строка символов «целыми числами», что? Когда я хочу получить доступ к адресу и манипулировать им ... сама, мне нужно знать его тип. вот почему я спрашиваю.)

Gold_Sky
источник
17
Указатели - это не переменные , а значения . Переменные содержат значения (и если их тип является типом указателя, это значение является указателем и может быть адресом зоны памяти, содержащей что-то значимое). Данная зона памяти может использоваться для хранения различных значений разных типов.
Василий Старынкевич
29
«адреса в шестнадцатеричном формате» Нет, это просто отладчик или биты форматирования библиотеки. С тем же аргументом вы можете сказать, что они в двоичном или восьмеричном виде.
USR
Вам лучше спросить о формате , а не о типе . Следовательно, некоторые ответы вне трассы ниже (хотя у Килиана точный ответ).
Гонки Легкости с Моникой
1
Я думаю, что более глубокая проблема здесь - это понимание OP типа . Когда дело доходит до этого, значения, которыми вы манипулируете в своей программе, просто биты в памяти. Типы - это способ программиста рассказать компилятору, как обрабатывать эти биты, когда он генерирует ассемблерный код.
Джастин Лардинойс
Я полагаю, что уже слишком поздно редактировать его со всеми этими ответами, но этот вопрос был бы лучше, если бы вы ограничили оборудование и / или операционную систему, например, «на x64 Linux».
Hyde

Ответы:

64

Тип переменной указателя: .. указатель.

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

Как только вы примете неопределенное поведение , вы можете посмотреть, каково значение на самом деле. Обычно это машинное слово, то же самое, что и целое число, и обычно оно может быть преобразовано без потерь в целочисленный тип. (Довольно много кода Windows делает это, скрывая указатели в DWORD или HANDLE typedefs).

Есть некоторые архитектуры, где указатели не просты, потому что память не плоская. DOS / 8086 «рядом» и «далеко»; Различная память и кодовые пространства PIC.

pjc50
источник
2
Вам также разрешено принимать разницу между двумя указателями p1-p2. Результатом является целочисленное значение со знаком. В частности,&(array[i])-&(array[j]) == i-j
MSalters
13
На самом деле, преобразование в целочисленный тип также определено, intptr_tи uintptr_tименно оно гарантированно будет «достаточно большим» для значений указателя.
Мэтью М.
3
Вы можете зависеть от преобразования в работу, но отображение между целыми числами и указателями определяется реализацией. (Единственное исключение 0 -> ноль, и даже это указывается, только если 0 является константой IIRC.)
cHao
7
Добавление pспецификатора к printf делает получение читабельного представления указателя void определенным, если поведение зависит от реализации в c.
dmckee
6
Этот ответ имеет в целом правильную идею, но не соответствует конкретным утверждениям. Приведение указателя к целочисленному типу не является неопределенным поведением, и типы данных Windows HANDLE не являются значениями указателей (они не являются указателями, скрытыми в целочисленных типах данных, они являются целыми числами, скрытыми в типах указателей, для предотвращения арифметики).
Бен Фойгт
44

Вы усложняете вещи.

Адреса просто целые числа, точка. В идеале это номер ячейки памяти, на которую ссылаются (на практике это усложняется из-за сегментов, виртуальной памяти и т. Д.).

Шестнадцатеричный синтаксис - это вымысел, который существует только для удобства программистов. 0x1A и 26 - это одно и то же число абсолютно одинакового типа , и компьютер не использует ни то, ни другое - внутренне компьютер всегда использует 00011010 (последовательность двоичных сигналов).

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

Килиан Фот
источник
26
Адреса, безусловно, не просто целые числа. Точно так же, как числа с плавающей точкой, безусловно, не просто целые числа.
gnasher729
8
Верно. Самый известный контрпример - Intel 8086, где указатели - это два целых числа.
MSalters
5
@Rob В сегментированной модели памяти указатель может быть либо одним значением (адрес относительно начала сегмента; смещение) с подразумеваемым сегментом, либо парой сегмент / селектор и смещение . (Я думаю, что Intel использовала термин «селектор»; мне лень его искать.) На 8086 они были представлены как два 16-разрядных целых числа, которые объединялись в один физический 20-разрядный адрес. (Да, вы могли бы обращаться к одной и той же ячейке памяти многими, разными способами, если бы вы были склонны: address = (сегмент << 4 + смещение) & 0xfffff.) Это переносится через все x86-совместимые при работе в реальном режиме.
CVn
4
Как долгосрочный программист на ассемблере, я могу засвидетельствовать, что память компьютера - это не что иное, как области памяти, содержащие целые числа. Однако важно, как вы относитесь к ним и отслеживаете, что представляют собой эти целые числа. Например, в моей системе десятичное число 4075876853 хранится как x'F2F0F1F5 ', что является строкой' 2015 'в EBCDIC. Десятичное число 2015 будет сохранено как 000007DF, тогда как x'0002015C 'представляет десятичное число 2015 в упакованном десятичном формате. Как программист на ассемблере, вы должны следить за этим; компилятор делает это для языков HL.
Стив Айвз
7
Адреса могут быть приведены в однозначном соответствии с целыми числами, но так же, как и все остальное на компьютере :)
Хоббс
16

Указатель - это просто указатель. Это не что-то еще. Не пытайтесь думать, что это что-то другое.

В таких языках, как C, C ++ и Objective-C, указатели данных имеют четыре вида возможных значений:

  1. Указатель может быть адресом объекта.
  2. Указатель может указывать сразу за последним элементом массива.
  3. Указатель может быть нулевым указателем, что означает, что он ни на что не указывает.
  4. Указатель может иметь неопределенное значение, другими словами, это мусор, и все может произойти (включая плохие вещи), если вы попытаетесь его использовать.

Существуют также указатели на функции, которые либо идентифицируют функцию, либо являются нулевыми указателями на функции, либо имеют неопределенное значение.

Другие указатели являются «указателем на член» в C ++. Это определенно не адреса памяти! Вместо этого они идентифицируют члена любого экземпляра класса. В Objective-C у вас есть селекторы, которые напоминают «указатель на метод экземпляра с заданным именем метода и именами аргументов». Как и указатель на член, он идентифицирует все методы всех классов, если они выглядят одинаково.

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

gnasher729
источник
4
Есть указатели на функции и, в C ++, указатели на члены.
Сденхэм
C ++ указатели на члены не являются адресами памяти? Конечно, они есть. class A { public: int num; int x; }; int A::*pmi = &A::num; A a; int n = a.*pmi;Переменная pmiбыла бы бесполезной, если бы она не содержала адрес памяти, а именно, как устанавливает последняя строка кода, адрес члена numэкземпляра aкласса A. Вы можете привести это к обычному intуказателю (хотя компилятор, вероятно, выдаст вам предупреждение) и успешно разыменовать его (доказав, что это синтаксический сахар для любого другого указателя).
dodgethesteamroller
9

Указатель - это битовый паттерн, адресующий (однозначно идентифицирующий для чтения или записи) слово памяти в ОЗУ. По историческим и общепринятым причинам единица обновления составляет восемь битов, известные на английском языке как «байт» или на французском, более логично, как октет. Это вездесуще, но не присуще; другие размеры существовали.

Если я правильно помню, был один компьютер, который использовал 29-битное слово; это не только степень двойки, это даже простое число. Я думал, что это был SILLIAC, но соответствующая статья в Википедии не поддерживает это. CAN BUS использует 29-битные адреса, но по договоренности сетевые адреса не называются указателями, даже если они функционально идентичны.

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

Иногда эти два не соответствуют. В ранних ПК адресная шина имела ширину 24 бита.

Питер Воне
источник
Nitpick, в наши дни в распространенных ОС, указатель идентифицирует местоположение в виртуальной памяти и не имеет прямого отношения к физическому слову оперативной памяти (местоположение виртуальной памяти может даже не существовать физически, если оно находится на странице памяти, о которой ОС знает все нули) ).
Hyde
@hyde - у вашего аргумента есть смысл в том контексте, в котором вы явно его предполагали, но доминирующей формой компьютера является встроенный контроллер, где чудеса, такие как виртуальная память, являются недавними инновациями с ограниченным развертыванием. Кроме того, то, что вы указали, никак не помогает ОП понимать указатели. Я думал, что некоторый исторический контекст сделает все это намного менее произвольным.
Питер Воне
Я не знаю, поможет ли OP понять OP, так как вопрос именно в том, что на самом деле представляют собой указатели . Ах, еще один кирк в указателе c по определению указывает на байт (может быть безопасно приведен к, char*например, для целей копирования / сравнения памяти и sizeof char==1в соответствии со стандартом C), а не слово (если только размер слова CPU не совпадает с размером байта).
Хайд
Основными указателями являются хеш-ключи для хранения. Это инвариант языка и платформы.
Питер Воне
Вопрос о c указателях . И указатели определенно не являются хеш-ключами, потому что нет хеш-таблицы, нет алгоритма хеширования. Естественно, они являются своего рода ключами карты / словаря (для достаточно широкого определения «карты»), но не ключами хеша .
Hyde
6

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

Байт состоит из 8 битов, слова 2 байта (или 16 битов), слова 2 слова (или 32 бита) и слова 2 слова (или 64 бита). Это не единственный способ расставить биты. 128-битные и 256-битные манипуляции также происходят, часто в инструкциях SIMD.

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

ALU (арифметико-логические единицы) работают с такими блоками битов, как если бы они представляли целые числа (обычно формат дополнения Two), а FPU - как со значениями с плавающей запятой (обычно в стиле IEEE 754 floatи double). Другие части будут действовать так, как если бы они были связаны данными некоторого формата, символов, записей таблицы, инструкций процессора или адресов.

На типичном 64-битном компьютере пакеты по 8 байт (64 бита) являются адресами. Мы отображаем эти адреса условно в шестнадцатеричном формате (например 0xabcd1234cdef5678), но для людей это простой способ чтения битовых комбинаций. Каждый байт (8 бит) записывается в виде двух шестнадцатеричных символов (эквивалентно, каждый шестнадцатеричный символ - от 0 до F - представляет 4 бита).

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

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

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

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

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

Есть также данные только для чтения (иногда хранящиеся в ПЗУ, которые физически не могут быть записаны в!), Проблемы с выравниванием (некоторые процессоры не могут загрузить doubles из памяти, если они не выровнены определенным образом, или инструкции SIMD, требующие определенного выравнивания), и множество другие особенности архитектуры

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

Даже это ложь.

Каждый уровень абстракции скрывает нижеприведенный уровень и позволяет вам думать о решении проблем без необходимости учитывать диаграммы Фейнмана для распечатки "Hello World".

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

Yakk
источник
3

Люди много думают о том, являются ли указатели целыми числами или нет. На самом деле есть ответы на эти вопросы. Тем не менее, вам придется сделать шаг в страну спецификаций, которая не для слабонервных. Мы собираемся взглянуть на спецификацию C, ISO / IEC 9899: TC2

6.3.2.3. Указатели

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

  2. Любой тип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.

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

Из этого мы можем видеть, что основная форма хранения не указана, кроме того, что может быть преобразование в целочисленный тип. Теперь, по правде говоря, практически каждый компилятор под солнцем представляет указатели под капотом как целочисленные адреса (с небольшим количеством особых случаев, когда он может быть представлен как 2 целых числа вместо 1), но спецификация допускает абсолютно все, например, представление адреса в виде строки из 10 символов!

Если мы переместимся вперед из C и посмотрим на спецификацию C ++, мы получим немного больше ясности reinterpret_cast, но это другой язык, поэтому его значение для вас может отличаться:

ISO / IEC N337: проект спецификации C ++ 11 (у меня есть только проект под рукой)

5.2.10 Переосмыслить приведение

  1. Указатель может быть явно преобразован в любой целочисленный тип, достаточно большой для его хранения. Функция отображения определяется реализацией. [Примечание: предполагается, что это не удивительно для тех, кто знает структуру адресации базовой машины. - примечание] Значение типа std :: nullptr_t может быть преобразовано в целочисленный тип; преобразование имеет то же значение и действительность, что и преобразование (void *) 0 в целочисленный тип. [Примечание: reinterpret_cast нельзя использовать для преобразования значения любого типа в тип std :: nullptr_t. —Конечная записка]

  2. Значение целочисленного типа или типа перечисления может быть явно преобразовано в указатель. Указатель, преобразованный в целое число достаточного размера (если таковое существует в реализации) и обратно в тот же тип указателя, будет иметь свое первоначальное значение; Отображения между указателями и целыми числами определяются реализацией. [Примечание: за исключением случаев, описанных в 3.7.4.3, результатом такого преобразования не будет безопасно полученное значение указателя. —Конечная записка]

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


Теперь, что вы должны от этого отнять?

  • Точное представление указателей определяется реализацией. (на самом деле, просто для того, чтобы сделать его более сложным, некоторые небольшие встроенные компьютеры представляют нулевой указатель (void ) 0 в качестве адреса 255 для поддержки некоторых приемов сглаживания адресов, которые они используют) *
  • Если вам нужно спросить о представлении указателей в памяти, вы, вероятно, не находитесь в той точке своей карьеры программиста, где вы хотите возиться с ними.

Лучшая ставка: приведение к (char *). Спецификации C и C ++ полны правил, определяющих упаковку массивов и структур, и оба всегда допускают приведение любого указателя на символ *. char всегда 1 байт (не гарантируется в C, но в C ++ 11 он стал обязательной частью языка, поэтому относительно безопасно предположить, что он везде 1 байт). Это позволяет вам выполнять некоторую арифметику с указателями на уровне байтов, не прибегая к необходимости знать конкретные представления указателей реализации.

Корт Аммон - Восстановить Монику
источник
Вы можете обязательно привести указатель на функцию char *? Я имею в виду гипотетическую машину с отдельными адресными пространствами для кода и данных.
Филипп Кендалл
@PhilipKendall Хороший вопрос. Я не включил эту часть спецификации, но указатели на функции трактуются как совершенно отличные от указателей данных в спецификации из-за именно той проблемы, которую вы поднимаете. С указателями на членов также обращаются по-разному (но они также действуют очень по-разному)
Корт Аммон - Восстановить Монику
A charвсегда равен 1 байту в C. Цитируя из стандарта C: «Оператор sizeof возвращает размер (в байтах) своего операнда» и «Когда sizeof применяется к операнду с типом char, unsigned char или char со знаком, (или его квалифицированная версия) результат 1 ". Возможно, вы думаете, что байт имеет длину 8 бит. Это не обязательно так. Для соответствия стандарту байт должен содержать не менее 8 бит.
Дэвид Хаммен
Спецификация описывает преобразование между указателем и целочисленными типами. Всегда следует помнить, что «преобразование» между типами не подразумевает равенство типов, и даже то, что двоичное представление двух типов в памяти будет иметь одинаковый битовый шаблон. (ASCII может быть «преобразован» в EBCDIC. Big-endian может быть «преобразован» в little-endian. И т. Д.)
user2338816
1

На большинстве архитектур тип указателя перестает существовать после того, как он был переведен в машинный код (за исключением, возможно, «жирных указателей»). Следовательно, указатель на a intбудет неотличим от указателя на a double, по крайней мере, сам по себе. *

[*] Хотя вы все еще можете делать предположения, основываясь на видах операций, которые вы к нему применяете.

Rufflewind
источник
1

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

int var = -1337;

В зависимости от архитектуры, целому числу обычно дается 32 бита для хранения этого значения. Это означает, что пространство в памяти, где хранится var, будет выглядеть примерно так: «11111111 11111111 11111010 11000111» или в шестнадцатеричном формате «0xFFFFFAC7». Вот и все. Это все, что хранится в этом месте. Все типы делают, это говорят компилятору, как интерпретировать эту информацию. Указатели ничем не отличаются. Если я сделаю что-то вроде этого:

int* var_ptr = &var;   //the ampersand is telling C "get the address where var's value is located"

Затем компилятор получит расположение переменной var, а затем сохранит этот адрес таким же образом, как первый фрагмент кода сохраняет значение -1337. Нет разницы в том, как они хранятся, просто в том, как они используются. Не имеет значения, что я сделал var_ptr указателем на int. Если бы вы хотели, вы могли бы сделать.

unsigned int var2 = *(unsigned int*)var_ptr;

Это скопирует вышеупомянутое шестнадцатеричное значение переменной var (0xFFFFFAC7) в папку, в которой хранится значение var2. Если бы мы тогда использовали var2, мы нашли бы, что значение было бы 4294965959. Байты в var2 такие же, как var, но числовое значение отличается. Компилятор интерпретировал их по-разному, потому что мы сказали, что эти биты представляют длинную без знака. Вы можете сделать то же самое для значения указателя тоже.

unsigned int var3 = (unsigned int)var_ptr;

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

Надеюсь, это прояснит вам и даст вам лучшее представление о том, как работает C. Обратите внимание, что вы НЕ ДОЛЖНЫ делать сумасшедшие вещи, которые я делал в двух приведенных ниже строках в реальном производственном коде. Это было просто для демонстрации.

ЗНАЧЕНИЕ NULL
источник
1

Integer.

Адресное пространство в компьютере нумеруется последовательно, начиная с 0, и увеличивается на 1. Таким образом, указатель будет содержать целое число, соответствующее адресу в адресном пространстве.

Галуа
источник
1

Типы сочетаются.

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

Переменная, которая объявлена ​​для хранения указателя на символ, имеет тип «указатель на символ». Переменная, которая объявлена ​​для хранения указателя на указатель на int, имеет тип «указатель на указатель на int».

Тип (указатель на) указатель на указатель на int может быть изменен на указатель на int с помощью операции разыменования. Таким образом, понятие типа - это не просто слова, а математически значимая конструкция, определяющая, что мы можем делать со значениями типа (такими как разыменование, передача в качестве параметра или присвоение переменной; она также определяет размер (число байтов) операции индексации, арифметики и увеличения / уменьшения).

PS Если вы хотите углубиться в типы, попробуйте этот блог: http://www.goodmath.org/blog/2015/05/13/expressions-and-arity-part-1/

Эрик Эйдт
источник