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

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

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

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

Ответ 1

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

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

В одной из первых версий языка C (которую я буду называть CRM для Справочное руководство по C", который пришел с 6-м Edition 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. Обратите внимание, что это использование -> не заботилось о типе выражения в левой части. Левая сторона была интерпретирована как числовой адрес rvalue (будь то указатель или целое число).

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

(*i).b = 42;

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

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

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

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

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

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

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

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

Ответ 2

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

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

a->b->c->d

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

Ответ 3

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

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