Почему оператор стрелки (->) в C существует?

264

Оператор dot ( .) используется для доступа к члену структуры, а оператор стрелки ( ->) в C используется для доступа к члену структуры, на которую ссылается рассматриваемый указатель.

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

Так почему создатели языка решили усложнить ситуацию, добавив этот, казалось бы, ненужный оператор? Какое большое дизайнерское решение?

Askaga
источник
1
Связанный: stackoverflow.com/questions/221346/… - также вы можете переопределить ->
Krease
16
@Cris Это касается C ++, который, конечно, имеет большое значение. Но поскольку мы говорим о том, почему C был спроектирован таким образом, давайте представим, что мы вернулись в 1970-е годы - до появления C ++.
Мистика
5
Моя догадка, что оператор стрелка существует , чтобы визуально выразить «смотреть его , вы имеете дело с указателем здесь!»
Chris
4
С первого взгляда я чувствую, что этот вопрос очень странный. Не все вещи продуманно разработаны. Если вы сохраните этот стиль на всю жизнь, ваш мир будет полон вопросов. Ответ, получивший большинство голосов, действительно информативен и понятен. Но это не затрагивает ключевой момент вашего вопроса. Следуйте стилю вашего вопроса, я могу задать слишком много вопросов. Например, ключевое слово int является аббревиатурой от integer; почему ключевое слово "double" также не будет короче?
Junwanghe
1
@junwanghe Этот вопрос на самом деле представляет действительную проблему - почему .оператор имеет более высокий приоритет, чем *оператор? Если это не так, мы могли бы иметь * ptr.member и var.member.
Milleniumbug

Ответы:

358

Я интерпретирую ваш вопрос как два вопроса: 1) почему ->вообще существует, и 2) почему .автоматически не разыменовывается указатель. Ответы на оба вопроса имеют исторические корни.

Почему вообще ->существует?

В одной из самых первых версий языка Си (которую я буду называть CRM для « Справочного руководства по Си », которая вышла с 6-м изданием Unix в мае 1975 года) оператор ->имел очень исключительное значение, а не синоним *и .комбинацию

Язык Си, описанный CRM, во многих отношениях сильно отличался от современного языка Си. В структуре CRM члены реализовали глобальную концепцию смещения байтов , которая может быть добавлена ​​к любому значению адреса без ограничений типа. Т.е. все имена всех членов структуры имели независимое глобальное значение (и, следовательно, должны были быть уникальными). Например, вы могли бы объявить

struct S {
  int a;
  int b;
};

и name aбудет означать смещение 0, в то время как name 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. Обратите внимание, что это использование ->не заботилось о типе выражения в левой части. Левая часть интерпретировалась как числовой адрес rvalue (будь то указатель или целое число).

Этот вид обмана не было возможно с *и .комбинации. Вы не могли сделать

(*i).b = 42;

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

Как отметил Кит в комментариях, это различие между комбинацией « +» ->и « CRM» означает то, что CRM называет «ослаблением требования» в 7.1.8: За исключением ослабления требования, относящегося к типу указателя, выражение в точности эквивалентно*.E1E1−>MOS(*E1).MOS

Позже в K & R C многие функции, первоначально описанные в CRM, были значительно переработаны. Идея «члена структуры как глобального идентификатора смещения» была полностью удалена. И функциональность ->оператора стала полностью идентична функциональности *и .комбинации.

Почему нельзя .разыменовать указатель автоматически?

Опять же, в CRM-версии языка левый операнд .оператора должен был быть lvalue . Это было единственное требование к этому операнду (и именно это отличало его от ->описанного выше). Обратите внимание, что CRM не требует, чтобы левый операнд .имел тип struct. Это просто требовало, чтобы это было lvalue, любое lvalue. Это означает, что в 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то вроде lvalue: какой-то блок памяти с возможностью записи.

Теперь обратите внимание, что если вы сделали это

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

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

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

Конечно, эта странная функциональность не очень веская причина против введения перегруженного .оператора для указателей (как вы предложили) в переработанной версии C - K & R C. Но это не было сделано. Возможно, в то время в CRM-версии C был написан какой-то устаревший код, который нужно было поддерживать.

(URL-адрес Справочного руководства C 1975 года может быть нестабильным. Другая копия, возможно, с некоторыми незначительными различиями, находится здесь .)

AnT
источник
10
А в разделе 7.1.8 цитируемого Справочного руководства C написано «За исключением ослабления требования, согласно которому E1 должен иметь тип указателя, выражение« E1−> MOS »в точности эквивалентно« (* E1) .MOS ». »«.
Кит Томпсон
1
Почему это не *iбыло lvalue некоторого типа по умолчанию (int?) По адресу 5? Тогда (* i) .b работал бы так же.
Random832
5
@Leo: Некоторые люди предпочитают язык Си как ассемблер высокого уровня. В тот период истории C язык действительно был ассемблером более высокого уровня.
AnT
29
Да. Таким образом, это объясняет, почему многие структуры в UNIX (например, struct stat) имеют префикс своих полей (например, st_mode).
icktoofay
5
@ perfectionm1ng: Похоже, что bell-labs.com был захвачен Alcatel-Lucent, а исходные страницы исчезли. Я обновил ссылку на другой сайт, хотя я не могу сказать, как долго он прослужит. В любом случае, поиск в Google по «справочнику по ritchie c» обычно находит документ.
13
46

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

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

a->b->c->d

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

effeffe
источник
2
С вложенными типами данных, содержащими как структуры, так и указатели на структуры, это может усложнить задачу, поскольку вы должны подумать о выборе правильного оператора для каждого доступа к подчиненному элементу. Вы можете получить ab-> c-> d или a-> bc-> d (у меня была эта проблема при использовании библиотеки freetype - мне нужно было постоянно искать ее исходный код). Кроме того, это не объясняет, почему компилятору не разрешается автоматически разыменовывать указатель при работе с указателями.
Аскага
3
Хотя изложенные вами факты верны, они никак не отвечают на мой первоначальный вопрос. Вы объясняете равенство a-> и * (a). нотации (которая уже была объяснена несколько раз в других вопросах), а также дать смутное утверждение о том, что языковой дизайн является несколько произвольным. Я не нашел ваш ответ очень полезным, поэтому отрицательный голос.
Аскага
16
@effeffe, ОП говорят , что язык мог бы легко интерпретировать , a.b.c.dкак (*(*(*a).b).c).d, что делает ->оператор бесполезен. Таким образом, версия ОП ( a.b.c.d) одинаково читаема (по сравнению с a->b->c->d). Вот почему ваш ответ не отвечает на вопрос ОП.
Шахбаз
4
@Shahbaz Это может быть случай для Java программист, С / С ++ программист поймет a.b.c.dи a->b->c->dкак две очень разные вещи: Во - первых, единственный доступ к памяти для вложенного подъобекта (есть только один объект памяти в этом случае ), второй - три обращения к памяти, преследующие указатели через четыре вероятных различных объекта. Это огромная разница в разметке памяти, и я считаю, что Си прав в различении этих двух случаев очень наглядно.
cmaster - восстановить
2
@ Shahbaz Я не имел в виду, что как оскорбление программистов на Java они просто привыкли к языку с полностью неявными указателями. Если бы я вырос как программист java, я бы, наверное, думал так же ... В любом случае, я на самом деле считаю, что перегрузка операторов, которую мы видим в C, не оптимальна. Однако я признаю, что все мы были избалованы математиками, которые щедро перегружают своих операторов практически всем. Я также понимаю их мотивацию, так как набор доступных символов довольно ограничен. Я думаю, в конце концов, это просто вопрос, где вы рисуете линию ...
cmaster - восстановить
19

С также делает хорошую работу, не делая ничего двусмысленного.

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

мукунда
источник
4
Это простой и правильный ответ. C в основном старается избегать перегрузок, что IMO является одной из лучших вещей о C.
jforberg
10
Многое в С неоднозначно и нечетко. Существуют неявные преобразования типов, математические операторы перегружены, цепная индексация делает что-то совершенно другое, в зависимости от того, индексируете ли вы многомерный массив или массив указателей, и все, что угодно, может быть макросом, скрывающим что-либо (соглашение об именах в верхнем регистре помогает, но C не делает этого) т).
PSkocik