Як пояснити початківцю покажчики С (декларація проти одинарних операторів)?


141

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

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Для абсолютного початківця вихід може бути дивовижним. У другому рядку він / вона щойно оголосив * bar для & foo, але у рядку 4 виявляється * bar насправді foo замість & foo!

Плутанина, можна сказати, випливає з неоднозначності символу *: У другому рядку він використовується для оголошення вказівника. У рядку 4 він використовується як одинарний оператор, який отримує значення, на яке вказує вказівник. Дві різні речі, правда?

Однак це "пояснення" зовсім не допомагає новачку. Він вводить нову концепцію, вказуючи на тонку невідповідність. Це не може бути правильним способом цього навчити.

Отже, як це пояснили Керніган та Річі?

Одинарний оператор * - це оператор опосередкування або перенаправлення; при застосуванні до вказівника він отримує доступ до об'єкта, на який вказує вказівник. […]

Декларація вказівника ip, int *ipпризначена як мнемонічна; це говорить, що вираз *ipє цілим. Синтаксис оголошення для змінної імітує синтаксис виразів, у яких може відображатися змінна .

int *ipслід читати як " *ipповернеться int"? Але чому тоді призначення після декларації не відповідає цій схемі? Що робити, якщо новачок хоче ініціалізувати змінну? int *ip = 1(читайте: *ipповернеться intта intє 1) не буде працювати, як очікувалося. Концептуальна модель просто не здається узгодженою. Я щось тут пропускаю?


Редагувати: Тут спробували узагальнити відповіді .


15
Найкраще пояснення - намалювати речі на папері та з'єднати їх зі стрілками;)
Марун

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

40
можливо, записуючи це як int* barробить більш очевидним, що зірка насправді є частиною типу, а не частиною ідентифікатора. Звичайно, це створює вам різні проблеми з такими неінтуїтивними матеріалами, як int* a, b.
Ніклас Б.

9
Я завжди вважав, що пояснення K&R нерозумно і непотрібно. Мова використовує один і той же символ для двох різних речей, і ми просто маємо з ним мати справу. *може мати два різних значення залежно від контексту. Так само, як та сама буква може вимовлятися по-різному, залежно від слова, в якому важко навчитися говорити на багатьох мовах. Якби кожна концепція / операція мала свій власний символ, нам знадобляться набагато більші клавіатури, тому символи переробляються, коли це має сенс.
Арт

8
Я неодноразово стикався з тією ж проблемою, коли навчав С інших людей, і, на моєму досвіді, це можна вирішити так, як пропонують більшість людей тут. Спочатку поясніть поняття вказівника без синтаксису С. Потім навчіть синтаксис і підкресліть зірочку як частину типу ( int* p), попереджаючи свого учня проти використання кількох декларацій в одному рядку, коли задіяні покажчики. Коли студент повністю зрозумів поняття покажчиків, поясніть студенту, що int *pсинтаксис is є рівнозначним, а потім поясніть проблему декількома деклараціями.
Теодорос Чатзіґянакікіс

Відповіді:


43

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

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

Коли вони зрозуміють, що контексти різні, можна продовжити пояснення, що коли *символ знаходиться у оголошенні змінної перед ідентифікатором змінної, це означає «оголосити цю змінну як вказівник». Тоді ви можете пояснити, що при використанні в виразі (як унарний оператор) *символ є "оператором скидання", і це означає "значення за адресою", а не його попереднє значення.

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

Загалом, не можна пояснити, що контексти різні. Якщо студент не розуміє, що контексти різні, він не може зрозуміти, чому *символ може означати різні речі.


80

Причина, за якою стенограма:

int *bar = &foo;

у вашому прикладі може бути заплутаним те, що його легко неправильно прочитати як рівнозначне:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

коли це насправді означає:

int *bar;
bar = &foo;

Виписано так, з відокремленою декларацією та призначенням змінної немає такого потенціалу для плутанини, і паралелізм декларації використання, описаний у вашій цитаті K&R, прекрасно працює:

  • Перший рядок оголошує змінну bar, таку *barяк an int.

  • Другий рядок призначає адресу fooдо bar, роблячи *bar(an int) псевдонім для foo(також an int).

Вводячи початківцям синтаксис вказівника С, може бути корисним спочатку дотримуватися цього стилю відокремлення декларацій вказівника від призначень і лише вводити комбінований короткочасний синтаксис (з відповідними попередженнями про його потенціал для плутанини), коли основні поняття вказівника використовують у C були адекватно інтерналізовані.


4
Я б спокусився typedef. typedef int *p_int;означає, що змінна типу p_intмає властивість, яка *p_intє int. Тоді маємо p_int bar = &foo;. Заохочення когось створити неініціалізовані дані та пізніше призначити їх як звичку за замовчуванням здається… як погана ідея.
Якк - Адам Невраумон

6
Це просто пошкоджений мозку стиль декларацій С; це не характерно для покажчиків. вважаємо int a[2] = {47,11};, що це не ініціалізація (неіснуючого) елемента a[2]ейхеру.
Марк ван Левен

5
@MarcvanLeeuwen Погодьтеся з пошкодженням мозку. В ідеалі, він *повинен бути частиною типу, не прив’язаною до змінної, і тоді ви зможете написати, int* foo_ptr, bar_ptrщоб оголосити два покажчики. Але він фактично оголошує покажчик і ціле число.
Вармар

1
Йдеться не лише про "скорочені" декларації / завдання. Вся проблема знову виникає в той момент, коли ви хочете використовувати вказівники як аргументи функції.
армін

30

Не вистачає декларацій

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

1. int a; a = 42;

int a;
a = 42;

Ми заявляємо про intімені А . Потім ми ініціалізуємо його, надаючи йому значення 42.

2. int a = 42;

Ми оголошуємо і intназиваємо a і надаємо йому значення 42. Це ініціалізовано з 42. Визначення.

3. a = 43;

Коли ми використовуємо змінні, ми кажемо, що ми працюємо над ними. a = 43- це операція призначення. Присвоюємо число 43 змінній a.

Сказавши

int *bar;

ми оголошуємо смугу покажчиком на int. Сказавши

int *bar = &foo;

ми оголошуємо бар і ініціалізуємо його з адресою foo .

Після ініціалізації панелі ми можемо використовувати той самий оператор, зірочку, для доступу та роботи зі значенням foo . Без оператора ми отримуємо доступ та працюємо за адресою, на яку вказує вказівник.

Крім того, я дозволяю малюнку говорити.

Що

Спрощене ВІДПОВІДЬ щодо того, що відбувається. (І ось версія гравця, якщо ви хочете зробити паузу тощо)

          ОБ'ЄДНАННЯ


22

2-е твердження int *bar = &foo;можна зобразити в пам'яті як,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Тепер barпокажчик типу , intщо містить адресу &з foo. Використовуючи одинарний оператор, *ми затримуємося отримати значення, що міститься у «foo», використовуючи покажчик bar.

EDIT : Мій підхід з початківцями полягає в поясненні memory addressзмінної, тобто

Memory Address:Кожна змінна має пов’язану з нею адресу, надану ОС. В int a;, &aадреса змінної a.

Продовжуйте пояснювати основні типи змінних у C,

Types of variables: Змінні можуть містити значення відповідних типів, але не адреси.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Як сказано вище, наприклад, змінні

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Можна призначити, b = aале ні b = &a, оскільки змінна bможе містити значення, але не адресу, отже, нам потрібні покажчики .

Pointer or Pointer variables :Якщо змінна містить адресу, вона відома як змінна вказівник. Використовуйте *в декларації, щоб повідомити, що це покажчик.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable

3
Проблема полягає в тому, що читаючи int *ipяк "ip - вказівник (*) типу int", ви потрапляєте у проблеми, читаючи щось на зразок x = (int) *ip.
армін

2
@abw Це щось зовсім інше, звідси і дужки. Я не думаю, що людям не складе труднощів зрозуміти різницю між деклараціями та кастингом.
bzeaman

@abw In x = (int) *ip;, отримайте значення, ipвідкинувши покажчик і передавайте це значення intбудь-якому типу ip.
Sunil Bojanapally

1
@BennoZeeman Ви праві: кастинг та декларації - це дві різні речі. Я намагався натякнути на різну роль зірочки: 1-е "це не int, а вказівник на int" 2nd ", це дасть тобі int, але не вказівник на int".
армін

2
@abw: Ось чому викладання int* bar = &foo;має сенс навантаження . Так, я знаю, що це створює проблеми, коли ви оголошуєте кілька покажчиків в одній декларації. Ні, я не думаю, що це взагалі має значення.
Гонки легкості на орбіті

17

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

  • Перш ніж показати будь-який код, використовуйте діаграми, ескізи або анімації, щоб проілюструвати, як працюють покажчики.
  • Представляючи синтаксис, поясніть дві різні ролі символу зірочки . Багато навчальних посібників відсутні або ухиляються від цієї частини. Виникає плутанина ("Коли ви розбиваєте ініціалізовану декларацію вказівника на декларацію та наступне призначення, ви повинні пам’ятати про видалення *" - comp.lang.c FAQ ) Я сподівався знайти альтернативний підхід, але я думаю, що це шлях.

Ви можете написати int* barзамість того, int *barщоб виділити різницю. Це означає, що ви не будете дотримуватися підходу «Міміка використання декларації використання K&R», але підходу Stroustrup C ++ :

Ми не оголошуємо *barцілим числом. Ми оголошуємо barсебе int*. Якщо ми хочемо ініціалізувати новостворену змінну в тому ж рядку, зрозуміло, що ми маємо справу bar, а не *bar.int* bar = &foo;

Недоліки:

  • Ви повинні попередити свого учня про проблему декларування кількох покажчиків ( int* foo, barпроти int *foo, *bar).
  • Ви повинні підготувати їх до світу рани . Багато програмістів хочуть бачити зірочку поруч із назвою змінної, і вони знадобляться, щоб виправдати свій стиль. І багато посібників зі стилів чітко застосовують цю позначення (стиль кодування ядра Linux, керівництво стилем NASA C тощо).

Редагувати: інший підхід , який було запропоновано, полягає в тому, щоб пройти «імітаційний» K&R шлях, але без синтаксису «скорочення» (див. Тут ). Як тільки ви пропустите виконання декларації та завдання в одному рядку , все буде виглядати набагато узгодженіше.

Однак рано чи пізно студенту доведеться мати справу з покажчиками як аргументами функції. І вказівники як типи повернення. І покажчики на функції. Вам доведеться пояснити різницю між int *func();і int (*func)();. Я думаю, рано чи пізно все розвалиться. І, можливо, швидше краще, ніж пізніше.


16

Є причина, чому стиль K&R надає перевагу int *pстилю Stroustrup int* p; обидві є дійсними (і означають те саме) в кожній мові, але як сказав Stroustrup:

Вибір між "int * p;" та "int * p;" йдеться не про правильне і неправильне, а про стиль та акценти. C підкреслені вирази; декларації часто вважалися мало чим необхідним злом. C ++, з іншого боку, робить великий акцент на типи.

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

Тому деяким людям буде легше почати з думки, що int*річ - це інша річ, ніж intі піти звідти.

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

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


1
Мені подобається аргумент Stroustrup, але мені цікаво, чому він вибрав символ & для позначення посилань - ще одну можливу проблему.
армін

1
@abw Я думаю, що він побачив симетрію, якщо ми можемо зробити, int* p = &aто ми можемо зробити int* r = *p. Я майже впевнений, що він висвітлював це в «Дизайні та еволюції C ++» , але минуло давно, як я це прочитав, і я нерозумно передав свою копію комусь.
Джон Ханна

3
Я думаю, ти маєш на увазі int& r = *p. І я думаю, що позичальник все ще намагається перетравити книгу.
армін

@abw, так, саме так я і мав на увазі. На жаль, помилки друку в коментарях не викликають помилок компіляції. Книга насправді досить жваве читання.
Джон Ханна

4
Однією з причин, що я віддаю перевагу синтаксису Паскаля (як поширене в народі) над C, є те, що Var A, B: ^Integer;стає зрозумілим, що тип "покажчик на ціле число" стосується і обох, Aі B. Використання K&Rстилю int *a, *bтакож працездатне; однак декларація на зразок int* a,b;, однак, виглядає як би aі bобидва оголошуються як int*, але насправді вона декларується aяк int*і bяк int.
supercat

9

tl; dr:

З: Як пояснити початківцю покажчики С (декларація проти одинарних операторів)?

Відповідь: ні. Поясніть початківцям покажчики та покажіть їм, як представити їх поняття вказівника у синтаксисі C після.


Нещодавно я мав задоволення пояснювати покажчики початківцю програмування на С і натрапляв на наступні труднощі.

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

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

  • Поясніть їх діаграмами вікна та стрілки. Ви можете зробити це без шістнадцяткових адрес, якщо вони не стосуються, просто покажіть стрілки, що вказують або на інше поле, або на якийсь символ нуля.

  • Поясніть за допомогою псевдокоду: просто запишіть адресу foo та значення, що зберігаються в барі .

  • Потім, коли ваш новачок розуміє, що таке покажчики, і чому, і як ними користуватися; потім покажіть відображення на синтаксисі C.

Я підозрюю, що причина K&R в тексті не дає концептуальної моделі в тому, що вони вже розуміли покажчики , і, ймовірно, припускав, що це робив і кожен інший компетентний програміст. Мнемоніка - це лише нагадування про відображення від добре зрозумілого поняття до синтаксису.


Дійсно; Почніть спочатку з теорії, синтаксис з’являється пізніше (і це не важливо). Зауважимо, що теорія використання пам'яті не залежить від мови. Ця модель із стрілками та стрілками допоможе вам із завданнями на будь-якій мові програмування.
оɔɯǝɹ

Дивіться тут для деяких прикладів (хоча Google допоможе також) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ

7

Це питання дещо заплутане, коли починаєш вивчати С.

Ось основні принципи, які можуть допомогти вам почати:

  1. У С є лише кілька основних типів:

    • char: ціле значення з розміром 1 байт.

    • short: ціле значення з розміром 2 байти.

    • long: ціле значення розміром 4 байти.

    • long long: ціле значення розміром 8 байт.

    • float: неціле значення з розміром 4 байти.

    • double: неціле значення з розміром 8 байт.

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

    Цілі типи short, longа за long longними зазвичай дотримуються int.

    Однак це не обов'язково, і ви можете використовувати їх без цього int.

    Крім того, ви можете просто констатувати int, але це може трактуватися різними компіляторами по-різному.

    Отже, підсумовуючи це:

    • shortте саме, short intале не обов'язково те саме, що int.

    • longте саме, long intале не обов'язково те саме, що int.

    • long longте саме, long long intале не обов'язково те саме, що int.

    • У даному компіляторі intє short intабо long intабо long long int.

  2. Якщо ви оголошуєте змінну якогось типу, то ви також можете оголосити іншу змінну, що вказує на неї.

    Наприклад:

    int a;

    int* b = &a;

    Отже, по суті, для кожного базового типу ми також маємо відповідний тип вказівника.

    Наприклад: shortі short*.

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

    • Ви можете розглядати bяк змінну типу int*.

    • Ви можете розглядати *bяк змінну типу int.

    Отже, деякі декларують int* b, тоді як інші декларують int *b.

    Але справа в тому, що ці дві декларації однакові (пробіли безглузді).

    Ви можете використовувати або bяк вказівник на ціле число, або *bяк фактичне вказане ціле значення.

    Ви можете отримати (прочитати) загострене значення: int c = *b.

    І ви можете встановити (запис) загострене значення: *b = 5.

  3. Вказівник може вказувати на будь-яку адресу пам'яті, а не лише на адресу певної змінної, яку ви раніше оголосили. Однак ви повинні бути обережними при використанні покажчиків, щоб отримати або встановити значення, розташоване на вказаній адресі пам'яті.

    Наприклад:

    int* a = (int*)0x8000000;

    Тут ми маємо змінну, яка aвказує на адресу пам'яті 0x8000000.

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

    Ви можете сміливо змінювати значення a, але вам слід дуже обережно змінювати значення *a.

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


7

Можливо, переходити через нього трохи більше, це полегшує:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

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

На моєму досвіді, це переходить через те, "чому це друкує саме так?" горб, а потім одразу показуючи, чому це корисно в параметрах функцій при ручному ігранні (як прелюдія до деяких основних матеріалів K&R, таких як обробка рядків / обробка масивів), що робить урок не просто сенсом, а стирком.

Наступний крок - змусити їх пояснити вам, як це i[0]стосується &i. Якщо вони зможуть це зробити, вони цього не забудуть, і ви можете почати говорити про структури, навіть трохи раніше часу, просто так воно зануриться.

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


Це гарна вправа. Але питання, яке я хотів підняти, - це специфічна синтаксична ситуація, яка може вплинути на ментальну модель, яку будують студенти. Розглянемо це: int foo = 1;. Тепер це нормально: int *bar; *bar = foo;. Це не в порядку:int *bar = foo;
армін

1
@abw Єдине, що має сенс - це те, що студенти намовляють говорити собі. Це означає "бачити одного, робити одного, навчати одного". Ви не можете захистити від або передбачити, який синтаксис чи стиль вони побачать там у джунглях (навіть ваші старі репости!), Тож вам доведеться показати достатньо перестановок, що основні поняття розуміються незалежно від стилю - і потім почніть навчати їх, чому певні стилі були влаштовані. Як і викладання англійської мови: основний вираз, ідіоми, стилі, певні стилі в певному контексті. Непросто, на жаль. У будь-якому випадку, удачі!
zxq9

6

Тип виразу *bar є int; таким чином, тип змінної (і виразу) barє int *. Оскільки змінна має тип вказівника, її ініціалізатор також повинен мати тип вказівника.

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


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

3
@abw: правила ініціалізації відрізняються від правил присвоєння; для скалярних арифметичних типів різниці незначні, але вони мають значення для вказівних та агрегатних типів. Це те, що вам потрібно буде пояснити разом із усім іншим.
Джон Боде

5

Я б краще прочитав це як перше, що *стосується intбільш ніж bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value

2
Тоді ви повинні пояснити, чому int* a, bне робить те, що, на їхню думку, робить.
Фарап

4
Щоправда, але я не думаю, що це int* a,bвзагалі слід використовувати. Для кращої видимості, оновлення тощо ... повинно бути лише одне оголошення змінної на рядок і ніколи більше. Початківцям це теж можна пояснити, навіть якщо компілятор може це впоратися.
grorel

Це одна думка чоловіка. Є мільйони програмістів, які абсолютно добре заявляють більше однієї змінної на рядок і роблять це щодня в рамках своєї роботи. Ви не можете приховати учнів від альтернативних способів ведення справ, краще покажіть їм усі альтернативи та дозвольте їм вирішити, у який бік вони хочуть займатися, тому що якщо вони коли-небудь приймуть на роботу, від них очікується дотримуватися певного стилю, який їм може бути або не бути комфортним. Для програміста універсальність - це дуже хороша риса.
Фарап

1
Я згоден з @grorel. Простіше мислити *як частину типу і просто відволікати int* a, b. Якщо ви не вважаєте за краще сказати, що *aмає тип, intа не aвказівник на int...
Кевін Ушей

@grorel вірно: int *a, b;не слід використовувати. Оголошення двох змінних різних типів в одному і тому самому висловлюванні є досить поганою практикою і сильним кандидатом у питаннях технічного обслуговування. Можливо, це відрізняється від тих із нас, хто працює у вбудованому полі, де частоти int*а та an intчасто мають різний розмір, а іноді зберігаються в абсолютно різних місцях пам'яті. Це один із багатьох аспектів мови С, який найкраще викладати як "це дозволено, але не робіть цього".
Злий собачий пиріг

5
int *bar = &foo;

Question 1: Що таке bar?

Ans: Це змінна вказівник (на тип int). Вказівник повинен вказувати на деяке дійсне місце в пам'яті, а пізніше його слід відмінити (* бар), використовуючи одинарний оператор *, щоб прочитати значення, збережене в цьому місці.

Question 2: Що таке &foo?

Ans: foo - це змінна тип. intКотрий зберігається в якомусь дійсному місці пам'яті, і це місце ми отримуємо від оператора, &тож тепер у нас є дійсне місце в пам'яті &foo.

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

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


5

Ви повинні вказати на початківця, що * має різний зміст у декларації та виразі. Як відомо, * у виразі є одинарним оператором, а * У декларації - це не оператор, а лише такий синтаксис, який поєднується з типом, щоб повідомити компілятору, що це тип вказівника. краще сказати початківцю, "* має інше значення. Для розуміння значення * ви повинні знайти, де використовується *


4

Думаю, чорт у космосі.

Я б написав (не тільки для початківця, але й для себе): int * bar = & foo; замість int * bar = & foo;

це повинно виявити, який взаємозв'язок між синтаксисом і семантикою


4

Вже зазначалося, що * має декілька ролей.

Є ще одна проста ідея, яка може допомогти новачку зрозуміти речі:

Подумайте, що "=" також має кілька ролей.

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

Коли ви бачите:

int *bar = &foo;

Подумайте, що це майже рівнозначно:

int *bar(&foo);

Дужки мають перевагу над зірочкою, тому "& foo" набагато легше інтуїтивно віднести до "бар", а не "* бар".


4

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

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

int x;

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

Таким чином, декларації

int *p;
int a[3];

констатуйте, що p - вказівник на int, оскільки '* p' має тип int, і що a - це масив ints, оскільки тип [3] (ігноруючи конкретне значення індексу, яке карається розміром масиву) має тип int.

(Далі описано, як поширити це розуміння на функціонування покажчиків тощо)

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


3

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

template<typename T>
using ptr = T*;

Потім це можна використовувати як

ptr<int> bar = &foo;

Після цього порівняйте нормальний синтаксис / C із цим підходом лише для C ++. Це також корисно для пояснення показників const.


2
Для початківців це буде набагато заплутаніше.
Карстен

Моє те, що ви не показали б визначення ptr. Просто використовуйте його для оголошення вказівників.
MI3Guy

3

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

У декларації

int *bar = &foo;  

*символ НЕ оператор разименованія . Натомість це допомагає вказати тип barінформування компілятора, який barє вказівником наint . З іншого боку, коли він з'являється у висловлюванні, *символ (коли використовується як одинарний оператор ) виконує непряме. Тому заява

*bar = &foo;

було б неправильно, оскільки воно призначає адресу fooоб'єкта, на який barвказує, а не на barсебе.


3

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

"Звичайно, це створює різні проблеми з неінтуїтивними матеріалами, такими як int * a, b."


2

Тут ви повинні використовувати, розуміти і пояснювати логіку компілятора, а не людську логіку (я знаю, ви людина, але тут ви повинні імітувати комп'ютер ...).

Коли пишеш

int *bar = &foo;

групи компіляторів, які як

{ int * } bar = &foo;

Тобто: тут нова змінна, її назва bar, її тип - вказівник на int, а її початкове значення - &foo.

І потрібно додати: =вищезазначене позначає ініціалізацію, а не афектацію, тоді як у наступних виразах *bar = 2;це є облудою

Редагувати за коментарем:

Остерігайтеся: у випадку багаторазового оголошення декларація *пов'язана лише з такою змінною:

int *bar = &foo, b = 2;

bar - вказівник на int, ініціалізований адресою foo, b - int, ініціалізований на 2, а в

int *bar=&foo, **p = &bar;

bar у нерухомому покажчику на int, а p - вказівник на покажчик на int, ініціалізований на адресу або рядок.


2
Насправді компілятор не групує це так: int* a, b;оголошує a вказівником на int, але b - an int. *Символ тільки має два значення: У декларації, то це вказує на тип покажчика, і в виразі це унарний оператор разименованія.
tmlen

@tmlen: Що я мав на увазі, це те, що при ініціалізації він *переходить на тип, так що покажчик ініціалізується, тоді як в афекті впливає загострене значення. Але принаймні ти подарував мені гарний капелюх :-)
Серж Баллеста

0

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

"char * pstr" схоже так

"char str [80]"

Але, важливі речі, покажчик трактується як просто ціле число в нижньому рівні компілятора.

Розглянемо приклади:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Результатам сподобається це 0x2a6b7ed0 - адреса str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

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


-1

Я б пояснив, що ints - це об'єкти, як floats і т. Д. Покажчик - це тип об'єкта, значення якого представляє адресу в пам'яті (отже, чому вказівник за замовчуванням NULL).

При першому оголошенні вказівника ви використовуєте синтаксис типу-pointer-name. Він читається як "цілочисельний покажчик, названий ім'ям, який може вказувати на адресу будь-якого цілого об'єкта". Цей синтаксис ми використовуємо лише під час декларування, подібно до того, як ми оголошуємо int як 'int num1', але використовуємо 'num1' лише тоді, коли хочемо використовувати цю змінну, а не 'int num1'.

int x = 5; // цілий об'єкт зі значенням 5

int * ptr; // ціле число зі значенням NULL за замовчуванням

Для вказівки на адресу об'єкта ми використовуємо символ "&", який можна прочитати як "адресу".

ptr = & x; // тепер значення - це адреса 'x'

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

std :: cout << * ptr; // роздрукувати значення за адресою

Ви можете коротко пояснити, що " " - це "оператор", який повертає різні результати з різними типами об'єктів. При використанні з покажчиком оператор " " більше не означає "помножений на".

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


-1

Вказівник - це лише змінна, яка використовується для зберігання адрес.

Пам'ять у комп'ютері складається з байтів (Байт складається з 8 біт), розташованих послідовно. Кожен байт має пов'язане з ним число так само, як індекс чи індекс у масиві, який називається адресою байта. Адреса байта починається від 0 до одного менше, ніж розмір пам'яті. Наприклад, в 64 Мб оперативної пам’яті є 64 * 2 ^ 20 = 67108864 байт. Тому адреса цих байтів починатиметься від 0 до 67108863.

введіть тут опис зображення

Давайте подивимося, що станеться при оголошенні змінної.

int позначки;

Як ми знаємо, int займає 4 байти даних (якщо використовувати 32-розрядний компілятор), тож компілятор резервує 4 послідовних байти з пам'яті для зберігання цілого числа. Адреса першого байту з 4 виділених байтів відома як адреса знаків змінної. Скажімо, що адреса чотирьох послідовних байтів - 5004, 5005, 5006 і 5007, тоді адреса змінних знаків буде 5004. введіть тут опис зображення

Декларування змінних вказівника

Як уже було сказано, покажчик - це змінна, яка зберігає адресу пам'яті. Як і будь-які інші змінні, вам потрібно спочатку оголосити змінну вказівника, перш ніж використовувати її. Ось як можна оголосити змінну вказівника.

Синтаксис: data_type *pointer_name;

data_type - тип вказівника (також відомий як базовий тип вказівника). pointer_name - це ім'я змінної, яка може бути будь-яким дійсним ідентифікатором C.

Візьмемо кілька прикладів:

int *ip;

float *fp;

int * ip означає, що ip - змінна вказівник, здатна вказувати на змінні типу int. Іншими словами, вказівник змінної ip може зберігати адресу змінних типу int. Аналогічно, вказівна змінна fp може зберігати лише адресу змінної float типу. Тип змінної (також відомий як базовий тип) ip - вказівник на int, а тип fp - вказівник на плавання. Змінна вказівника типу вказівника на int може бути символічно представлена ​​як (int *). Аналогічно, змінну вказівника типу вказівника на плавати можна представити у вигляді (float *)

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

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Джерело: thecguru - це найпростіше, але детальне пояснення, яке я коли-небудь знаходив.

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