Чому в C існує оператор стрілки (->)?


264

Оператор dot ( .) використовується для доступу до члена структури, тоді як оператор стрілки ( ->) в C використовується для доступу до члена структури, на який посилається відповідний покажчик.

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

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


1
Пов’язано: stackoverflow.com/questions/221346/… - також, ви можете перекрити ->
Krease

16
@Chris Це стосується C ++, що, звичайно, має велике значення. Але оскільки ми говоримо про те, чому C був розроблений таким чином, давайте зробимо вигляд, що ми повернулися в 1970-ті - до того, як існував C ++.
Містичний

5
Я найкраще здогадуюсь, що оператор стрілки існує для візуального вираження "дивись! Ти тут маєш справу з вказівником"
Кріс

4
З першого погляду я відчуваю, що це питання дуже дивне. Не всі речі продумано розроблені. Якщо ви будете дотримуватися цього стилю все своє життя, ваш світ був би сповнений питань. Відповідь, яка набрала найбільше голосів, справді інформативна і зрозуміла. Але це не впливає на ключовий момент вашого запитання. Дотримуйтесь стилю свого питання, я можу задати занадто багато питань. Наприклад, ключове слово 'int' є абревіатурою 'integer'; чому ключове слово "подвійний" також не є коротшим?
junwanghe

1
@junwanghe Це питання насправді викликає занепокоєння - чому .оператор має більшу перевагу перед *оператором? Якби цього не було, ми могли б мати * ptr.member та var.member.
тисячоліття

Відповіді:


358

Я розтлумачу ваше запитання як два запитання: 1) чому це ->взагалі існує, і 2) чому .автоматично не відтягує вказівник. Відповіді на обидва запитання мають історичне коріння.

Чому ->навіть існує?

В одній із найперших версій мови C (яку я буду називати CRM для " Довідкового посібника C ", що вийшла з 6-м виданням Unix у травні 1975 року) оператор ->мав дуже ексклюзивне значення, не синонім *і .комбінацію

Мова C, описана CRM, багато в чому відрізнялася від сучасної C. Члени структури CRM реалізували глобальну концепцію зміщення байтів , яку можна було б додати до будь-якої адреси адреси без обмежень типу. Тобто всі імена всіх членів структури мали незалежне глобальне значення (і, отже, повинні були бути унікальними). Наприклад, ви можете заявити

struct S {
  int a;
  int b;
};

а ім'я aозначатиме зміщення 0, тоді як ім'я bозначає зміщення 2 (якщо вважати intтип розміру 2 та відсутність прокладки). Мова, необхідна для всіх членів усіх структур у блоці перекладу, має унікальні імена або означає одне і те ж значення зміщення. Наприклад, у тій же одиниці перекладу, яку ви могли додатково заявити

struct X {
  int a;
  int x;
};

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

struct Y {
  int b;
  int a;
};

формально буде недійсним, оскільки він намагався "переосмислити" aяк зсув 2, так і bяк зсув 0.

І ось тут входить ->оператор. Оскільки кожне ім’я члена структури мало власне глобальне значення, мова підтримує вирази, подібні цим

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

Перше призначення було витлумачено компілятором як «приймати адреса 5, додати зміщення 2до нього і призначити 42до intзначенням за отриманим адресою». Тобто вище буде призначити 42на intзначення за адресою 7. Зауважте, що це використання ->не стосувалося типу виразу зліва. Ліва частина інтерпретується як числова адреса рецензії (будь то вказівник або ціле число).

Цей вид обману не було можливо з *і .комбінації. Ти не міг цього зробити

(*i).b = 42;

оскільки *iце вже недійсний вираз. *Оператор, так як вона відокремлена від .накладає більш суворі вимоги типу на його операнда. Для забезпечення можливості обійти це обмеження CRM представив ->оператора, який не залежить від типу лівого операнда.

Як зазначав Кіт у коментарях, ця різниця між комбінацією ->та *+ .- це те, що CRM посилається на "послаблення вимоги" в 7.1.8: За винятком послаблення вимоги, що E1має тип вказівника, вираз E1−>MOSточно еквівалентний(*E1).MOS

Пізніше в K&R C багато функцій, спочатку описаних у CRM, були значно перероблені. Ідея "члена структури як глобального ідентифікатора зміщення" була повністю вилучена. А функціональність ->оператора стала повністю ідентичною функціональності *та .комбінації.

Чому не можна .автоматично перенаправити покажчик?

Знову ж таки, у CRM-версії мови лівий операнд .оператора повинен був бути значенням . Це була єдина вимога, що висувалася до цього операнда (і це відрізняло його від ->, як було пояснено вище). Зауважте, що CRM не вимагає, щоб лівий операнд .має тип структура. Це просто вимагало, щоб це було значення, будь-яке значення. Це означає, що у CRM-версії C ви могли написати такий код

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

В цьому випадку компілятор буде писати 55в intзначення , розташованому в байті зміщення 2 в безперервному блоці пам'яті , відомий як c, навіть якщо тип struct Tне мало поля з ім'ям b. Компілятор взагалі не перейматиметься фактичним типом c. Все, що його хвилювало, це те, що cбуло значення: якийсь блок пам'яті, що можна записати.

Тепер зауважте, що якщо ви це зробили

S *s;
...
s.b = 42;

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

Тобто в цій версії мови запропонована вами ідея щодо оператора перевантаження .для типів вказівника не спрацює: оператор .вже мав дуже специфічне значення при використанні вказівників (з покажчиками lvalue або взагалі з будь-якими значеннями). Це був дуже дивний функціонал, без сумніву. Але це було в той час.

Звичайно, ця дивна функціональність не є дуже вагомою причиною проти введення перевантаженого .оператора для покажчиків (як ви запропонували) у переробленій версії C - K&R C. Але цього не було зроблено. Можливо, на той час був якийсь спадковий код, написаний у CRM-версії C, який потрібно було підтримати.

(URL-адреса довідкового посібника 1975 р. Може бути нестійкою. Ще одна копія, можливо, з деякими тонкими відмінностями, є тут .)


10
І в розділі 7.1.8 цитованого довідника С говориться: "За винятком послаблення вимоги, щоб E1 був тип вказівника, вираз" E1−> MOS "" точно еквівалентний '' (* E1) .MOS ' '. "
Кіт Томпсон

1
Чому це не *iбуло значенням якогось типового типу (int?) За адресою 5? Тоді (* i) .b працював би так само.
Випадково832

5
@Leo: Ну, деякі люди уявляють мову С як асемблер вищого рівня. У той період історії C мова насправді була асемблером вищого рівня.
ANT

29
Ага. Отже, це пояснює, чому багато структур у UNIX (наприклад, struct stat) префіксують свої поля (наприклад, st_mode).
icktoofay

5
@ perfectionm1ng: Схоже, bell-labs.com перейняв Alcatel-Lucent, а оригінальних сторінок вже немає. Я оновив посилання на інший сайт, хоча я не можу сказати, наскільки довго він буде стояти. У будь-якому випадку, документ, що знаходиться в "посібнику ritchie c", зазвичай знаходить документ.
ANT

46

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

(*(*(*a).b).c).d

a->b->c->d

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


2
З вкладеними типами даних, що містять і структури, і покажчики на структури, це може ускладнити ситуацію, оскільки вам доведеться подумати про вибір правильного оператора для кожного доступу до підрозділу. У вас може виникнути ab-> c-> d або a-> bc-> d (у мене була ця проблема під час використання бібліотеки вільних типів - мені потрібно було постійно шукати вихідний код). Крім того, це не пояснює, чому не вдалося дозволити компілятору автоматично переводити покажчик під час роботи з покажчиками.
Аскага

3
Хоча факти, які ви констатуєте, є правильними, вони жодним чином не відповідають на моє початкове запитання. Ви пояснюєте рівність a-> і * (a). позначення (що вже було неодноразово пояснено в інших запитаннях), а також давати розпливчасте твердження про те, що мовна конструкція є дещо довільною. Я не знайшов вашу відповідь дуже корисною, тому голосування.
Аскага

16
@effeffe, ОП говорить, що мова могла легко інтерпретуватися a.b.c.dяк (*(*(*a).b).c).d, роблячи ->оператора марним. Тож версія OP ( a.b.c.d) однаково читається (порівняно з a->b->c->d). Ось чому ваша відповідь не відповідає на питання ОП.
Шахбаз

4
@Shahbaz Можливо, це стосується програміста Java, програміст C / C ++ зрозуміє a.b.c.dі a->b->c->dяк дві дуже різні речі: Перший - це єдиний доступ до пам'яті до вкладеного суб'єкта (у цьому випадку є лише один об'єкт пам'яті ), друге - це три доступу до пам'яті, що переслідують покажчики через чотири ймовірні різні об’єкти. Це величезна різниця в компонуванні пам’яті, і я вважаю, що C вірно відрізняє ці два випадки дуже помітно.
cmaster - відновити моніку

2
@Shahbaz Я не мав на увазі, що як образа програмістів Java, вони просто використовуються до мови з повністю неявними покажчиками. Якби мене виховували як програміста Java, я б, ймовірно, думав так само ... У будь-якому разі, я дійсно вважаю, що перевантаження оператора, яку ми бачимо на С, є менш оптимальною. Однак я визнаю, що всі ми були розбещені математиками, які в основному перевантажують своїх операторів майже на все. Я також розумію їх мотивацію, оскільки набір доступних символів досить обмежений. Я думаю, врешті-решт, це лише питання, де ви проводите лінію ...
cmaster - відновіть моніку

19

C також робить хорошу роботу, не роблячи нічого двозначного.

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


4
Це проста і правильна відповідь. Здебільшого C намагається уникнути перевантаження, яка IMO - одна з найкращих речей щодо C.
jforberg

10
Багато речей у С неоднозначні та нечіткі. Існують неявні перетворення типів, математичні оператори перевантажені, ланцюгова індексація робить щось зовсім інше, залежно від того, індексуєш багатовимірний масив чи масив вказівника, і що-небудь може бути макросом, що приховує що-небудь (конвенція про іменування у верхньому регістрі допомагає там, але C не робить це " т).
PSkocik
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.