Я интерпретирую ваш вопрос как два вопроса: 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: За исключением ослабления требования, относящегося к типу указателя, выражение в точности эквивалентно*
.
E1
E1−>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 года может быть нестабильным. Другая копия, возможно, с некоторыми незначительными различиями, находится здесь .)