Покажчики на покажчики проти звичайних покажчиків


74

Призначення вказівника - зберегти адресу певної змінної. Тоді структура пам'яті наступного коду повинна виглядати так:

...... адреса пам'яті ...... значення
a ... 0x000002 ................... 5
b ... 0x000010 ..... .............. 0x000002

Гаразд Тоді припустимо, що зараз я хочу зберегти адресу вказівника * b. Тоді ми зазвичай визначаємо подвійний покажчик ** c, як

Тоді структура пам'яті виглядає так:

...... адреса пам'яті ...... значення
a ... 0x000002 ................... 5
b ... 0x000010 ..... .............. 0x000002
c ... 0x000020 ................... 0x000010

Отже ** c посилається на адресу * b.

Зараз моє запитання: чому цей тип коду,

генерувати попередження?

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


13
Окрім стільки хороших відповідей, я можу, будь ласка, залишити простий коментар. Clang компілятор видає це однозначне попередження при спробі скомпілювати під сумнів частина вашого коду: warning: incompatible pointer types initializing 'int *' with an expression of type 'int **'; remove & [-Wincompatible-pointer-types]. Це могло б все зрозуміти.
user3078414

14
Початківці часто плутаються, оскільки вважають "адреси" типом даних як такі. Вони не є такими. Адреса даних типу X є. І вони різні для різних типів. Це привело вас до думки, що int * та int ** однакові.
Мішель Бійо

4
" якщо метою вказівника є просто збереження адреси пам'яті ", це не так. Призначення вказівника - зберегти "адресу пам'яті" об'єкта разом із його типом. Просто починайте розмежування покажчиків, і ви побачите.
Маргарет Блум

8
Вибачте, щоб не бути грубим, а просто цікавим, що робить це питання таким корисним? Це є в будь-якій помірній книзі C, розділі «Показники», другій чи третій статті, щонайбільше, не кажучи вже про це, багато разів обговорюваній у SO Я пропустив щось очевидне?
Sourav Ghosh

3
@SouravGhosh хлопець, ймовірно, підсилив своє запитання для представника, зв’язавши його з яким-небудь густонаселеним форумом для початківців
MM

Відповіді:


90

В

Ви отримуєте попередження, оскільки &bмає тип int **, і намагаєтесь ініціалізувати змінну типу int *. Немає неявних перетворень між цими двома типами, що веде до попередження.

Щоб взяти довший приклад, який ви хочете попрацювати, якщо ми спробуємо здійснити розстановку посилань, fкомпілятор дасть нам int, а не вказівник, на який ми зможемо надалі проводити розмежування.

Також відзначимо , що в багатьох системах intі int*не той же розмір (наприклад, покажчик може бути 64 біт в довжину і AN int32 біта). Якщо ви розблокуєте посилання fта отримаєте значення int, ви втратите половину значення, і тоді ви навіть не зможете передати його на дійсний покажчик.


4
А в системах, для яких був розроблений С (а також у деяких сучасних вбудованих системах), в одній програмі існували різні види вказівників - наприклад, вказівники ближнього та дальнього вказівників або вказівники даних та коду. Вказівник - це не значення int, люди; припиніть робити вигляд, що це просто тому, що "в основному працює" .
Луан

2
@Luaan: C був розроблений для ранніх мінікомп'ютерів DEC і був найпоширенішим на PDP-11 з 1970-х років. Ці машини НЕ мали покажчиків "поблизу" та "далеко". "Близько" та "далеко" - це розширення Microsoft до C, щоб підтримати відморожену сегментовану архітектуру Intel 8086/8088 на комп'ютері IBM. (На момент запуску проекту з ПК IBM виробляла та продавала лабораторний комп’ютер на базі Motorola 68000 на іншому майданчику. Уявіть собі економію лише на аспірині від уникнення головних болів сегментів, якби два сайти говорили ...)
Джон R. Strohm

1
@Luaan насправді не відповідає дійсності. Лише тоді, коли інші постачальники та ANSI / ISO взяли до рук цю проблему щодо переносимості. 1972 С був ідеально сангвінічним щодо взаємозамінних покажчиків та інтів; обидва були 16-розрядними, а покажчики - плоскими. void *навіть не існувало, ви завжди могли просто використовувати char *:)
hobbs

53

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

Так, під час виконання вказівник просто містить адресу. Але під час компіляції також існує тип, пов'язаний з кожною змінною. Як уже говорили інші, int*і int**це дві різні, несумісні типи.

Існує один тип, void*який робить те, що ви хочете: він зберігає лише адресу, ви можете призначити їй будь-яку адресу:

Але коли ви хочете розпізнати a void*, вам потрібно надати інформацію про тип "відсутнього":


Якщо я правильно розумію, тип вказівника повідомляє, скільки пам'яті зчитує процесор, починаючи з посилання на адресу пам'яті. Тож вираз 'int * b = & a; printf ("% d", * b); int *, int **, int ***? Чи це відрізняється від системи до системи? Тоді що (компілятор, процесор чи інше?) визначає його розмір?
user42298

1
Розмір типу - це лише одне питання. Є й інші.
Грегорі Каррі

Розглянемо вказівник на щось с. А тепер уявіть, що ви хочете зберегти значення, на яке вказує p. int i = * p; Компілятор повинен знати, який тип вказівника. В пам’яті дубль виглядає набагато інакше як int. Це не має нічого спільного з розміром вказівника, а з тим, як він повинен поводитися з даними, на які вказують.
Грегорі Каррі

1
@ user42298 Розмір вказівників зазвичай залежить від центрального процесора та компілятора. Програма, скомпільована для 64 бітів, що працює на 64 бітному процесорі, має 64 бітові вказівники. Але ви також можете скомпілювати для 32 біт і запустити його на 64-бітному процесорі, тоді покажчики мають 32 біти, і операційна система піклується про те, щоб правильно це запустити. Btw an також intможе мати різні розміри, але int32_tгарантовано мати рівно 32 біти.
alain

Це найбільш вичерпна відповідь серед інших.
edmz

23

Зараз моє запитання: чому цей тип коду,

генерувати попередження?

Потрібно повернутися до основ.

  • змінні мають типи
  • змінні містять значення
  • покажчик - це значення
  • покажчик посилається на змінну
  • if p- значення покажчика, тоді *pце змінна
  • якщо vє змінною, то &vє покажчиком

І тепер ми можемо знайти всі помилки у вашій публікації.

Тоді припустимо, що зараз я хочу зберегти адресу вказівника *b

No. *b- це змінна типу int. Це не вказівник. bє змінною, значення якої є покажчиком. *b- змінна , значення якої є цілим числом.

**cпосилається на адресу *b.

НІ-НІ-НІ. Абсолютно не. Ви повинні правильно це розуміти, якщо збираєтеся розуміти вказівки.

*bє змінною; це псевдонім змінної a. Адреса змінної a- це значення змінної b. **cне посилається на адресу a. Швидше, це змінна, яка є псевдонімом змінної a. (І так само *b.)

Правильне формулювання така: значення змінної cє адреса з b. Або, еквівалентно: значення c- це вказівник, на який посилається b.

Звідки ми це знаємо? Поверніться до основ. Ви сказали це c = &b. Так у чому цінність c? Покажчик. До того, що? b.

Переконайтеся, що ви повністю розумієте основні правила.

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


Я думаю, що кожного разу, коли ОП каже b або ** c у дописі, вони справді намагаються просто сказати b та c. Ви багато чого охоплюєте, але насправді не висвітлюєте останню частину запитання ("чому не може int вказувати на інший int *"), саме цього OP намагається поставити IMO.
mbrig

Чиєю геніальною ідеєю було зробити оператор
деререференції

6
@mbrig: Це була геніальна ідея Денніса Річі. Якщо незрозуміло, чому це геніальна ідея, ви не розумно аналізуєте мову належним чином. Коли ми говоримо, що int * b;ми не просто говоримо " bє змінною типу int*. Ми також говоримо, що *bце змінна типуint . Геніальна ідея тут полягає в тому, що ви можете подумки розглядати це як int* b і int *b, і будь-яке тлумачення є правильним.
Ерік Ліпперт

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

20

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

Якщо ви здійснюєте розмежування типу, int**ви знаєте, який тип ви отримуєте, int*і, подібним чином, якщо ви розрингуєте int*тип, це тип int. З вашою пропозицією тип буде неоднозначним.

На вашому прикладі неможливо зрозуміти, cвказує на a intчи int*:

На який тип вказує c? Компілятор цього не знає, тому наступний рядок виконати неможливо:

У C вся інформація про тип втрачається після компіляції, оскільки кожен тип перевіряється під час компіляції і більше не потрібен. Ваша пропозиція фактично витратить пам'ять і час, оскільки кожен покажчик повинен мати додаткову інформацію про час виконання про типи, що містяться в покажчиках.


17

Покажчики - це абстракції адрес пам’яті з додатковою семантикою типу, і такою мовою, як тип С, має значення.

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

По-друге, тип має значення для арифметики покажчика. Враховуючи вказівник pтипу T *, вираз p + 1видає адресу наступного об’єкта типу T. Отже, припустимо такі декларації:

Вираз cp + 1дає нам адресу наступного charоб'єкта, який буде 0x1001. Вираз sp + 1дає нам адресу наступного shortоб'єкта, який буде 0x1002. ip + 1дає нам 0x1004і lp + 1дає нам 0x1008.

Отже, дано

b + 1дає нам адресу наступного intі c + 1дає нам адресу наступного вказівника на int.

Вказівники на покажчики потрібні, якщо ви хочете, щоб функція записувала в параметр типу вказівника. Візьміть такий код:

Це справедливо для будь-якого типуT . Якщо ми замінимо Tна тип покажчика P *, код стане

Семантика абсолютно однакова, це лише типи, які різні; формальний параметр p- це завжди ще один рівень опосередкованості, ніж змінна val.


11

Я думаю, що не повинно бути ієрархії, якщо адреса, яку ми збираємося зберегти, стосується змінної, покажчика, подвійного покажчика

Без "ієрархії" було б дуже легко генерувати UB без жодних попереджень - це було б жахливо.

Розглянемо це:

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

Але без "ієрархії", як:

Компілятор не може дати жодної помилки, оскільки немає "ієрархії".

Але коли рядок:

виконується, це UB (невизначена поведінка).

Спочатку *pcчитає charтак, ніби це вказівник, тобто, можливо, читає 4 або 8 байтів, хоча ми зарезервували лише 1 байт. Тобто УБ.

Якщо програма не зазнала збою через UB вище, а просто повернула якесь значення вибору, другим кроком було б розмежування значення викиду. Ще раз УБ.

Висновок

Система типів допомагає нам виявляти помилки, розглядаючи int *, int **, int *** тощо, як різні типи.


10

Якщо метою вказівника є просто збереження адреси пам'яті, я думаю, що не повинно бути ніякої ієрархії, якщо адреса, яку ми збираємося зберегти, стосується змінної, вказівника, подвійного вказівника, ... і т. Д., Тому тип коду нижче має бути дійсним.

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

Особливо, на відміну від вас, інші люди дійсно хочуть мати таку ієрархію, щоб знати, що робити із вмістом пам'яті, на який вказує вказівник.

Саме суть системи покажчиків С має прикріплену до неї інформацію про тип.

Якщо ти зробиш

&aмає на увазі, що те, що ви отримуєте, є int *таким, що, якщо ви переорієнтуєтесь, це буде intзнову.

Виводячи це на наступні рівні,

&bтакож є покажчиком. Але, не знаючи, що за цим ховається, відп. на що вказує, марно. Важливо знати, що розстановка посилання на вказівник виявляє тип оригінального типу, тож *(&b)це int *і **(&b)є оригінальним intзначенням, з яким ми працюємо.

Якщо ви відчуваєте, що у ваших обставинах не повинно існувати ієрархії типів, ви завжди можете працювати void *, хоча пряме використання досить обмежене.


9

Якщо метою вказівника є просто збереження адреси пам'яті, я думаю, що не повинно бути ніякої ієрархії, якщо адреса, яку ми збираємося зберегти, стосується змінної, вказівника, подвійного вказівника, ... і т. Д., Тому тип коду нижче має бути дійсним.

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

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

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

Ви можете запрограмувати на збірку і зробити те, що хочете, але ви будете спостерігати, наскільки це небезпечно, адже ви можете робити майже те, що хочете, особливо дивні речі. Так, змінювати значення адреси небезпечно, припустимо, у вас є автономний автомобіль, який повинен доставити щось за адресою, вираженою у відстані: 1200 вулиць пам’яті (адреса), і припустимо, що вуличні будинки розділені 100 футами (1221 - недійсна адреса), якщо ви можете маніпулювати адресами, як вам подобається, як цілі числа, ви зможете спробувати доставити о 1223 і впустити пакет посередині бруківки.

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


Це не обов'язково вірно і для машини. У старих системах (і деяких сучасних вбудованих системах) у вас були різні типи вказівників - наприклад, архітектура x86 має близькі (16-розрядні) та далекі (32-розрядні) вказівники. C приховує (абстрагує) цей факт від вас, але дуже важливо, щоб програма працювала на комп'ютері x86 у 16-розрядному режимі. Є й інші приклади, наприклад у сегментованому режимі (сегмент + зсув, де нульовий покажчик не дорівнює нулю у фактичному машинному коді). У C не так багато абстракцій високого рівня, але у них багато абстракцій низького рівня - у цьому і полягала вся мета C.
Luaan

@Luaan: Є багато машин, де вказівники на функції та вказівники на дані різні; машини, де цільовий тип вказівника впливає на його розмір, існують, але набагато рідше. Було б корисно, якщо б C визначив необов'язковий тип "вказівника на будь-який тип вказівника на дані", який визначався б у реалізаціях, де всі типи даних використовували одне і те ж подання, оскільки в даний час єдиний спосіб написати функцію, яка може працювати з усі такі вказівники (наприклад, для цілей сортування) - це маніпулювати ними за допомогою memcpy, memmove або типів символів, а також за допомогою будь-якої з цих технік ...
supercat

. визначити що-небудь, з чим не вдалося обробити всі реалізації.
supercat

Все це цілком правильно, я знаю, іноді потрібно спростити речі. Я мав би сказати, що в машинах-сировинах речі менш жорсткі, ніж у мовах, і що ми не можемо мислити, як у машині-сировині, коли перебуваємо на мові.
Жан-Батіст Юнес

9

Існують різні типи. І для цього є вагома причина:

Маючи ...

... вираз ...

… Дійсний, тоді як вираз…

не має сенсу.

Велика справа не в тому, як зберігаються покажчики чи покажчики на покажчики, а в тому, на що вони посилаються.


9

Мова С сильно набрана. Це означає, що для кожної адреси існує тип , який повідомляє компілятору, як інтерпретувати значення за цією адресою.

У вашому прикладі:

Тип ais intі тип bis int *(читається як "вказівник на int"). На вашому прикладі пам’ять міститиме:

Тип НЕ на насправді зберігаються в пам'яті, це просто , що компілятор знає , що, коли ви читаєте aви знайдете int, і коли ви читаєте bви знайдете адресу місця , де ви можете знайти int.

У другому прикладі:

Тип cis int **, читається як "вказівник на вказівник на int". Це означає, що для компілятора:

  • c є покажчиком;
  • читаючи c, ви отримуєте адресу іншого вказівника;
  • читаючи той інший покажчик, ви отримуєте адресу int.

Це,

  • cє покажчиком ( int **);
  • *cтакож є покажчиком ( int *);
  • **cє int.

І пам’ять містила б:

Оскільки "тип" не зберігається разом зі значенням, і вказівник може вказувати на будь-яку адресу пам'яті, спосіб, яким компілятор знає тип значення за адресою, в основному полягає в тому, щоб взяти тип вказівника та видалити крайній правий *.


До речі, це для звичайної 32-розрядної архітектури. Для більшості 64-розрядних архітектур ви будете мати:

Адреси зараз складають 8 байт, тоді як an intвсе ще становить лише 4 байти. Оскільки компілятор знає тип кожної змінної, він легко впорається з цією різницею і прочитає 8 байт для вказівника та 4 байти для int.


6

Чому цей тип коду генерує попередження?

&Оператор дає покажчик на об'єкт, тобто &aмає типу , int *щоб призначити (через ініціалізацію) це , bякий має також тип int *є дійсним. &bдає покажчик на об'єкт b, який &bмає тип покажчика int *, я .e., int **.

C говорить у обмеженнях оператора присвоєння (які виконуються для ініціалізації), що (C11, 6.5.16.1p1): "обидва операнди є вказівниками на кваліфіковані або некваліфіковані версії сумісних типів" . Але у визначенні C визначено, що є сумісним типом, int **а що int *не є сумісними типами.

Отже, в int *c = &b;ініціалізації є порушення обмеження, що означає, що компілятор вимагає діагностики.

Одним із обґрунтувань правила тут є відсутність гарантії стандартом, що два різні типи покажчиків мають однаковий розмір (крім void *і типів покажчиків на символи), тобто sizeof (int *)іsizeof (int **) можуть бути різними значеннями.


4

Це було б тому, що будь-який вказівник T*насправді має тип pointer to a T(або address of a T), де Tвказаний тип. У цьому випадку *може бути прочитаний як pointer to a(n), і Tє вказаним типом.

Це використовується для розмежування посилань, коли оператор розмежування бере а T*і повертає значення, тип якого T. Тип повернення можна розглядати як скорочення крайнього лівого "вказівника на a (n)" і як усе, що залишилось.

Зверніть увагу, як для кожної з вищезазначених операцій тип повернення оператора перенаправлення відповідає типу вихідної T*декларації T.

Це значно допомагає як первісним компіляторам, так і програмістам у синтаксичному аналізі типу вказівника: Для компілятора оператор адреса-оператора додає *тип до типу, оператор розмежування видаляє a *із типу, і будь-яке невідповідність є помилкою. Для програміста число *s є прямим показником того, на скільки рівнів непрямості ви маєте справу ( int*завжди вказує на int, float**завжди вказує на float*які, в свою чергу, завжди вказує floatтощо).


Тепер, беручи це до уваги, є дві основні проблеми з використанням лише одного, *незалежно від кількості рівнів опосередкованості:

  1. Вказівник набагато складніше для розпізнавання компілятора, оскільки він повинен посилатися на останнє призначення, щоб визначити рівень непрямості та визначити тип повернення належним чином.
  2. Вказівник важче зрозуміти програмісту, оскільки легко втратити відстеження того, скільки шарів непрямої дії є.

В обох випадках єдиним способом визначити фактичний тип значення було б повернути його назад, змусивши вас шукати десь ще, щоб знайти його.

Це ... є дуже небезпечною проблемою, і її легко вирішити, якщо кількість *s прямо представляє рівень непрямості.

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