Оператор dot ( .
) используется для доступа к члену структуры, а оператор стрелки ( ->
) в C используется для доступа к члену структуры, на которую ссылается рассматриваемый указатель.
Сам указатель не имеет каких-либо членов, к которым можно получить доступ с помощью оператора точки (на самом деле это всего лишь число, описывающее местоположение в виртуальной памяти, поэтому у него нет никаких членов). Таким образом, не было бы никакой двусмысленности, если бы мы просто определили оператор точки, чтобы автоматически разыменовывать указатель, если он используется для указателя (информация, которая известна компилятору во время компиляции afaik).
Так почему создатели языка решили усложнить ситуацию, добавив этот, казалось бы, ненужный оператор? Какое большое дизайнерское решение?
источник
.
оператор имеет более высокий приоритет, чем*
оператор? Если это не так, мы могли бы иметь * ptr.member и var.member.Ответы:
Я интерпретирую ваш вопрос как два вопроса: 1) почему
->
вообще существует, и 2) почему.
автоматически не разыменовывается указатель. Ответы на оба вопроса имеют исторические корни.Почему вообще
->
существует?В одной из самых первых версий языка Си (которую я буду называть CRM для « Справочного руководства по Си », которая вышла с 6-м изданием Unix в мае 1975 года) оператор
->
имел очень исключительное значение, а не синоним*
и.
комбинациюЯзык Си, описанный CRM, во многих отношениях сильно отличался от современного языка Си. В структуре CRM члены реализовали глобальную концепцию смещения байтов , которая может быть добавлена к любому значению адреса без ограничений типа. Т.е. все имена всех членов структуры имели независимое глобальное значение (и, следовательно, должны были быть уникальными). Например, вы могли бы объявить
и name
a
будет означать смещение 0, в то время как nameb
будет означать смещение 2 (при условии, чтоint
размер размера 2 и заполнение отсутствует). Язык требовал, чтобы все члены всех структур в единице перевода либо имели уникальные имена, либо обозначали одно и то же значение смещения. Например, в той же единице перевода вы могли бы дополнительно объявитьи это было бы хорошо, так как имя
a
будет последовательно означать смещение 0. Но это дополнительное объявлениебудет формально недействительным, поскольку он попытался «переопределить»
a
как смещение 2 иb
как смещение 0.И вот
->
тут-то и появляется оператор. Так как каждое имя члена структуры имеет свое собственное самодостаточное глобальное значение, язык поддерживает такие выражения, как этиПервое назначение было истолковано компилятором как «принимать адрес
5
, добавить смещение2
к нему и назначить42
кint
значению по полученному адресу». Т.е. выше будет назначить42
наint
значение по адресу7
. Обратите внимание, что это использование->
не заботилось о типе выражения в левой части. Левая часть интерпретировалась как числовой адрес rvalue (будь то указатель или целое число).Этот вид обмана не было возможно с
*
и.
комбинации. Вы не могли сделатьпоскольку
*i
это уже недопустимое выражение.*
Оператор, так как она отделена от.
налагает более строгие требования типа на его операнда. Для обеспечения возможности обойти это ограничение в CRM введен->
оператор, который не зависит от типа левого операнда.Как отметил Кит в комментариях, это различие между комбинацией « +»
->
и « CRM» означает то, что CRM называет «ослаблением требования» в 7.1.8: За исключением ослабления требования, относящегося к типу указателя, выражение в точности эквивалентно*
.
E1
E1−>MOS
(*E1).MOS
Позже в K & R C многие функции, первоначально описанные в CRM, были значительно переработаны. Идея «члена структуры как глобального идентификатора смещения» была полностью удалена. И функциональность
->
оператора стала полностью идентична функциональности*
и.
комбинации.Почему нельзя
.
разыменовать указатель автоматически?Опять же, в CRM-версии языка левый операнд
.
оператора должен был быть lvalue . Это было единственное требование к этому операнду (и именно это отличало его от->
описанного выше). Обратите внимание, что CRM не требует, чтобы левый операнд.
имел тип struct. Это просто требовало, чтобы это было lvalue, любое lvalue. Это означает, что в CRM-версии C вы можете написать такой кодВ этом случае компилятор записывает
55
вint
значение, расположенное со смещением в 2 байта в непрерывном блоке памяти, известном какc
, даже если типstruct T
не имеет названного поляb
. Компилятор не будет заботиться о фактическом типеc
вообще. Все, о чем он заботился, это что-c
то вроде lvalue: какой-то блок памяти с возможностью записи.Теперь обратите внимание, что если вы сделали это
код будет считаться действительным (так как
s
это также именующее) и компилятор просто попытка записи данных в указательs
сам , в байтовое смещение 2. Излишне говорить, что такие вещи , как это легко может привести к перерасходу памяти, но язык не занимался такими вопросами.Т.е. в этой версии языка предложенная вами идея об операторе перегрузки
.
для типов указателей не будет работать: оператор.
уже имеет очень специфическое значение при использовании с указателями (с указателями lvalue или вообще с любыми lvalue). Это была очень странная функциональность, без сомнения. Но это было там в то время.Конечно, эта странная функциональность не очень веская причина против введения перегруженного
.
оператора для указателей (как вы предложили) в переработанной версии C - K & R C. Но это не было сделано. Возможно, в то время в CRM-версии C был написан какой-то устаревший код, который нужно было поддерживать.(URL-адрес Справочного руководства C 1975 года может быть нестабильным. Другая копия, возможно, с некоторыми незначительными различиями, находится здесь .)
источник
*i
было lvalue некоторого типа по умолчанию (int?) По адресу 5? Тогда (* i) .b работал бы так же.struct stat
) имеют префикс своих полей (например,st_mode
).Помимо исторических (хороших и уже сообщенных) причин, есть также небольшая проблема с приоритетом операторов: оператор точки имеет более высокий приоритет, чем оператор звезды, поэтому, если у вас есть структура, содержащая указатель на структуру, содержащая указатель на структуру ... Эти два эквивалентны:
Но второе явно более читабельно. Стрелка имеет самый высокий приоритет (как точка) и ассоциируется слева направо. Я думаю, что это понятнее, чем использовать оператор точки для указателей на структуру и структуру, потому что мы знаем тип из выражения, не обращая внимания на объявление, которое может быть даже в другом файле.
источник
a.b.c.d
как(*(*(*a).b).c).d
, что делает->
оператор бесполезен. Таким образом, версия ОП (a.b.c.d
) одинаково читаема (по сравнению сa->b->c->d
). Вот почему ваш ответ не отвечает на вопрос ОП.a.b.c.d
иa->b->c->d
как две очень разные вещи: Во - первых, единственный доступ к памяти для вложенного подъобекта (есть только один объект памяти в этом случае ), второй - три обращения к памяти, преследующие указатели через четыре вероятных различных объекта. Это огромная разница в разметке памяти, и я считаю, что Си прав в различении этих двух случаев очень наглядно.С также делает хорошую работу, не делая ничего двусмысленного.
Конечно, точка может быть перегружена, чтобы означать обе вещи, но стрелка гарантирует, что программист знает, что он работает с указателем, точно так же, как когда компилятор не позволяет смешивать два несовместимых типа.
источник