Що таке "тип" даних, які містять покажчики мовою С?


30

Я знаю, що покажчики містять адреси. Я знаю, що типи покажчиків "загалом" відомі на основі "типу" даних, на які вони вказують. Але покажчики все ще є змінними, і адреси, які вони містять, повинні мати "тип" даних. За моєю інформацією, адреси у шістнадцятковому форматі. Але я досі не знаю, який "тип" даних це шістнадцятковий. (Зверніть увагу, що я знаю, що таке шістнадцятковий, але коли ви скажете 10CBA20, наприклад, це рядок символів? Цілі числа? Що? Коли я хочу отримати доступ до адреси та маніпулювати нею. Я сам повинен знати її тип. Це чому я прошу.)


17
Покажчики - це не змінні , а значення . Змінні містять значення (і якщо їх тип - тип вказівника, це значення - вказівник, і може бути адресою зони пам'яті, що містить щось значуще). Дана зона пам'яті може використовуватися для зберігання різних значень різних типів.
Василь Старинкевич

29
"адреси у шістнадцятковому форматі" Ні, це лише відладчик або біти форматування бібліотеки. З тим самим аргументом можна сказати, що вони в двійковій чи вісімковій формі.
usr

Вам буде краще запитати про формат , а не про тип . Звідси випливають кілька відповідей за межами списку нижче (хоча Кіліана на місці).
Гонки легкості з Монікою

1
Я думаю, що більш глибокою проблемою тут є розуміння ОП типу . Коли справа доходить до цього, значення, якими ви маніпулюєте у своїй програмі, є лише бітами пам'яті. Типи - це спосіб програміста розповісти компілятору, як поводитися з цими бітами, коли він генерує код складання.
Джастін Lardinois

Я вважаю, що вже пізно редагувати його з усіма цими відповідями, але це питання було б краще, якби ви обмежили апаратне та / або операційну систему, наприклад, "на x64 Linux".
Гайда

Відповіді:


64

Тип змінної вказівника - .. покажчик.

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

Після того, як ви приймете невизначене поведінку , ви зможете подивитися на те, що насправді є цінністю. Зазвичай це машинне слово, те саме, що і ціле число, і зазвичай може бути втрачено без втрат на ціле число та з нього. (Досить багато коду Windows робить це, приховуючи покажчики в DWORD або HANDLE typedefs).

Є деякі архітектури, де покажчики не прості, оскільки пам'ять не є простою. DOS / 8086 «поблизу» та «далеко»; Різні простори пам'яті та коду PIC.


2
Вам також дозволено приймати різницю між двома вказівниками p1-p2. Результат - це підписане інтегральне значення. Зокрема,&(array[i])-&(array[j]) == i-j
MSalters

13
На насправді, перетворення в інтегральний тип також вказується, в зокрема , до intptr_tі uintptr_tякі гарантовано будуть «достатньо великий» для значень покажчиків.
Матьє М.

3
Ви можете залежати від перетворення на роботу, але відображення між цілими числами та покажчиками визначено реалізацією. (Єдиний виняток - 0 -> null, і навіть це вказано лише, якщо 0 є постійною IIRC.)
cHao

7
Додавання pспецифікатора до printf робить отримання читаним людиною уявлення про недійсний покажчик визначеним, якщо поведінка залежить від реалізації в c.
dmckee

6
Ця відповідь, як правило, є правильною ідеєю, але не відповідає певним твердженням. Примусовий покажчик на інтегральний тип - це не визначена поведінка, а типи даних Windows HANDLE не є значеннями покажчиків (вони не вказівники, приховані в цілісних типах даних, вони є цілими числами, прихованими у типах вказівників, щоб запобігти арифметиці).
Ben Voigt

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
Адреси можна вводити в листування один до одного з цілими числами, але так можна робити і все на комп’ютері :)
hobbs

16

Вказівник - саме це - вказівник. Це не щось інше. Не намагайтеся думати, що це щось інше.

У таких мовах, як C, C ++ та Objective-C, покажчики даних мають чотири види можливих значень:

  1. Вказівником може бути адреса об'єкта.
  2. Вказівник може вказувати лише на останній елемент масиву.
  3. Вказівник може бути нульовим вказівником, це означає, що він не вказує ні на що.
  4. Вказівник може мати невизначене значення, інакше кажучи, це сміття, і будь-що може статися (включаючи погані речі), якщо ви спробуєте його використовувати.

Існують також функціональні покажчики, які або ідентифікують функцію, або є нульовими покажчиками функції, або мають невизначене значення.

Інші покажчики є "покажчиком на члена" в C ++. Це, безумовно, не адреси пам'яті! Натомість вони ідентифікують члена будь-якого примірника класу. У Objective-C у вас є селектори, які є чимось на зразок "вказівник на метод екземпляра з заданою назвою методу та назвами аргументів". Як і член-вказівник, він ідентифікує всі методи всіх класів до тих пір, поки вони виглядають однаково.

Ви можете дослідити, як конкретний компілятор реалізує покажчики, але це зовсім інше питання.


4
Є вказівники на функції, а в C ++ - покажчики на членів.
sdenham

Показники 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-бітові адреси, але за умовами мережеві адреси не називаються покажчиками, навіть коли вони функціонально однакові.

Люди постійно стверджують, що покажчики - цілі числа. Це не є ні сутнісним, ні істотним, але якщо ми інтерпретуємо бітові шаблони як цілі числа, з'являється корисна якість звичайності, що дозволяє дуже безпосередньо (і, отже, ефективно на невеликому апаратному забезпеченні) реалізувати такі конструкції, як "string" та "масив". Поняття суміжної пам'яті залежить від порядкової суміжності, і можливе відносне розташування; ціле порівняння та арифметичні операції можуть бути змістовно застосовані. З цієї причини майже завжди існує сильна кореляція між розміром слова для адресного зберігання та ALU (те, що робить цілу математику).

Іноді вони не відповідають. На ранніх комп'ютерах шина адреси була шириною 24 біт.


Nitpick, в ці дні в загальних ОС, вказівник визначає місце розташування у віртуальній пам'яті і не має нічого спільного з фізичним словом оперативної пам'яті (місце розташування віртуальної пам'яті може навіть не існувати фізично, якщо воно знаходиться на сторінці пам'яті, яка, як відомо, всі нулі в ОС ).
Гайд

@hyde - Ваш аргумент має заслугу в контексті, в якому ви, очевидно, це задумали, але домінуючою формою комп'ютера є вбудований контролер, де дивовижі, як віртуальна пам'ять, - це останні нововведення з обмеженим розгортанням. Також те, на що ви вказали, жодним чином не допомагає ОП зрозуміти покажчики. Я думав, що деякий історичний контекст зробить це набагато менш довільним.
Пітер Вун

Я не знаю, якщо розмова про оперативну пам’ять допоможе ОП зрозуміти, оскільки питання полягає саме в тому, що саме є покажчиками . О, ще одна нитка, в покажчику c за визначенням вказує на байт (може бути безпечно передана в char*напр. Для копіювання / порівняння пам’яті та sizeof char==1як визначено стандартом C), а не слово (якщо тільки розмір слова CPU не такий, як розмір байта).
Гайд

Основними вказівниками є хеш-ключі для зберігання. Це інваріантна мова та платформа.
Пітер Вун

Питання стосується c покажчиків . І покажчики, безумовно, не є хеш-ключами, оскільки немає хеш-таблиці, ані алгоритму хешування. Вони, природно, являють собою якусь карту / словник ключів (для досить широкого визначення поняття "карта"), але не хеш- ключі.
Гайд

6

В основному кожен сучасний комп’ютер - це машина, що підштовхує біт. Зазвичай він натискає на біти в кластерах даних, що називаються байтами, словами, dwords або qwords.

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

Інструкції по збірці діють на регістри, а адреси пам'яті, як правило, працюють в одній з вищевказаних форм.

АЛУ (арифметичні логічні одиниці) працюють на таких пучках бітів, як якщо б вони представляли цілі числа (зазвичай формат Доповнення двох), а ФПУ так, ніби вони там, де знаки з плаваючою комою (як правило, стилі IEEE 754 floatта double). Інші частини будуть діяти так, ніби вони вбудовані в певний формат, символи, записи в таблиці, інструкції CPU або адреси.

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

Що насправді відбувається (на деякому рівні насправді), це те, що є біти, які зазвичай зберігаються в реєстрі або зберігаються в сусідніх місцях у банку пам'яті, і ми просто намагаємось описати їх іншому людині.

Після вказівника складається з того, щоб попросити контролер пам'яті надати нам деякі дані в цьому місці. Ви, як правило, запитуєте в контролер пам'яті певну кількість байт у певному місці (ну, неявно діапазон локацій, як правило, суміжних), і він доставляється через різні механізми, в які я не потрапляю.

Код зазвичай визначає призначення для даних, до яких потрібно отримати - реєстр, іншу адресу пам'яті тощо - і зазвичай погана ідея завантажувати дані плаваючої точки в регістр, що очікує ціле число, або навпаки.

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

З цього є винятки. Є архітектури, де певні речі є різними видами бітів. Найпоширеніший приклад - захищені сторінки виконання - в той час як вказівки, які вказують процесору, що робити, це біти, під час виконання сторінки (пам'ять), що містять код для виконання, позначені спеціально, не можуть бути змінені, і ви не можете виконувати сторінки, які не позначені як сторінки виконання.

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

Навіть вищевказаний рівень деталізації - брехня. Комп'ютери не "насправді" нагнітаються навколо біт, вони дійсно штовхають напруги та струм. Ці напруги та струм іноді не роблять те, що їм "належить" робити на рівні абстракції біт. Мікросхеми призначені для виявлення більшості таких помилок і виправлення їх, без абстракції вищого рівня.

Навіть це брехня.

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

Тож при достатньому рівні чесності комп'ютери натискають біти, і цим бітам надається сенс у тому, як вони використовуються.


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 на інтегральний тип. [Примітка: Повторний_кайт не може бути використаний для перетворення значення будь-якого типу у тип std :: nullptr_t. —Закінчити примітку]

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

Як ви бачите тут, з ще декількома роками під поясом C ++ виявив, що можна впевнено припустити, що відображення цілих чисел існує, тому більше не йдеться про невизначене поведінку (хоча між частинами 4 та 4 існує цікава суперечність 5 із фразою "якщо така є в реалізації")


Тепер, що вам слід від цього забрати?

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

Найкраща ставка: кастинг на (char *). Специфікації C і C ++ рясніють правилами, що визначають упаковку масивів і структур, і обидва завжди дозволяють накидати будь-який вказівник на знак char *. char завжди є 1 байтом (не гарантується в C, але C ++ 11 він став частиною мови, настільки обов'язковою, тому порівняно безпечно вважати, що він скрізь 1 байт). Це дозволяє виконати деяку арифметику вказівника на рівні байт-байт, не вдаючись до того, що насправді потрібно знати конкретні уявлення щодо покажчиків.


Чи можете ви обов'язково закинути покажчик функції на a char *? Я думаю про гіпотетичну машину з окремими адресними просторами для коду та даних.
Філіп Кендалл

@PhilipKendall Добре. Я не включив цю частину специфікації, але покажчики функцій трактуються як зовсім інша річ, ніж покажчики даних у специфікації через саме поставлену проблему. Показники членів також трактуються по-різному (але вони також діють дуже по-різному)
Корт Аммон - Відновлення Моніки

A charзавжди 1 байт у C. Цитуючи зі стандарту C: "Оператор sizeof дає розмір (у байтах) свого операнда" та "Коли sizeof застосовується до операнду, який має тип char, неподписаний char або підписаний char, (або її кваліфікована версія) результат дорівнює 1. " Можливо, ви думаєте, що байт становить 8 біт. Це не обов'язково так. Щоб відповідати стандарту, байт повинен містити щонайменше 8 біт.
Девід Хаммен

Специфікація описує перетворення між вказівними та цілими типами. Завжди слід пам’ятати, що «перетворення» між типами не означає рівність типів, а також навіть те, що двійкове представлення двох типів у пам’яті матиме однаковий біт. (ASCII можна "перетворити" на EBCDIC. Big-endian можна "перетворити" на little-endian. І т.д.)
user2338816

1

У більшості архітектур тип вказівника припиняє своє існування після їх перекладу в машинний код (за винятком, можливо, "жирових покажчиків"). Отже, вказівник на значення intбуло б не відрізнятись від вказівника на a double, принаймні, самостійно. *

[*] Хоча ви все ще можете здогадуватися на основі видів операцій, які ви застосовуєте до нього.


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 як неподписаний int.

Сподіваємось, це пояснює для вас речі та дає вам краще уявлення про те, як працює C. Зверніть увагу, що ви НЕ БУДЕТЕ робити жодного з шалених речей, які я робив у наведених нижче двох рядках у фактичному виробничому коді. Це було лише для демонстрації.


1

Цілий.

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


1

Типи поєднуються.

Зокрема, певні типи поєднуються, майже так, як якщо б вони були параметризовані за допомогою заповнювачів. Типи масивів і вказівників такі; вони мають один такий заповнювач, який є типом елемента масиву або відповідної речі. Типи функцій також такі; вони можуть мати кілька заповнювачів для параметрів та заповнювач для типу повернення.

Змінна, яка оголошена, що містить покажчик на char, має тип "покажчик на char". Змінна, яка оголошена таким, що містить покажчик на покажчик на int, має тип "pointer to pointer to int".

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

PS Якщо ви хочете заглибитись у типи, спробуйте цей блог: http://www.goodmath.org/blog/2015/05/13/expressions-and-versity-part-1/

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.