Vou interpretar sua pergunta como duas perguntas: 1) por que ->
existe mesmo e 2) por .
que não desrefere automaticamente o ponteiro. As respostas para ambas as perguntas têm raízes históricas.
Por que ->
existe mesmo?
Em uma das primeiras versões da linguagem C (que chamarei de CRM para " C Reference Manual ", que veio com a 6ª Edição Unix em maio de 1975), o operador ->
tinha um significado muito exclusivo, não sinônimo *
e .
combinação
A linguagem C descrita pelo CRM era muito diferente da linguagem C moderna em muitos aspectos. No CRM, os membros da estrutura implementaram o conceito global de deslocamento de bytes , que pode ser adicionado a qualquer valor de endereço sem restrições de tipo. Ou seja, todos os nomes de todos os membros da estrutura tinham um significado global independente (e, portanto, tinha que ser único). Por exemplo, você pode declarar
struct S {
int a;
int b;
};
e name a
representaria o deslocamento 0, enquanto name b
representaria o deslocamento 2 (assumindo o int
tipo de tamanho 2 e sem preenchimento). O idioma exigido a todos os membros de todas as estruturas da unidade de tradução possui nomes exclusivos ou representa o mesmo valor de deslocamento. Por exemplo, na mesma unidade de tradução, você também pode declarar
struct X {
int a;
int x;
};
e isso seria bom, já que o nome a
consistentemente representaria o deslocamento 0. Mas essa declaração adicional
struct Y {
int b;
int a;
};
seria formalmente inválido, pois tentava "redefinir" a
como deslocamento 2 e b
deslocamento 0.
E é aqui que o ->
operador entra. Como cada nome de membro struct possui seu próprio significado global auto-suficiente, a linguagem suportava expressões como estas
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
A primeira atribuição foi interpretada pelo compilador como "obter endereço 5
, adicionar deslocamento 2
a ele e atribuir 42
ao int
valor no endereço resultante". Ou seja, o acima atribuiria 42
ao int
valor no endereço 7
. Observe que esse uso de ->
não se importava com o tipo de expressão no lado esquerdo. O lado esquerdo foi interpretado como um endereço numérico de rvalor (seja um ponteiro ou um número inteiro).
Esse tipo de trapaça não era possível com *
e .
combinação. Você não poderia fazer
(*i).b = 42;
já que *i
já é uma expressão inválida. O *
operador, uma vez que é separado .
, impõe requisitos de tipo mais rigorosos ao seu operando. Para fornecer um recurso para contornar essa limitação, o CRM introduziu o ->
operador, que é independente do tipo de operando do lado esquerdo.
Como Keith observou nos comentários, essa diferença entre ->
e *
+ .
é a que o CRM se refere como "relaxamento do requisito" em 7.1.8: Exceto pelo relaxamento do requisito que E1
é do tipo ponteiro, a expressão E1−>MOS
é exatamente equivalente a(*E1).MOS
Mais tarde, no K&R C, muitos recursos originalmente descritos no CRM foram reformulados significativamente. A idéia de "membro de estrutura como identificador de deslocamento global" foi completamente removida. E a funcionalidade do ->
operador tornou-se totalmente idêntica à funcionalidade *
e à .
combinação.
Por que não pode .
desreferenciar o ponteiro automaticamente?
Novamente, na versão CRM do idioma, o operando esquerdo do .
operador precisava ser um valor l . Esse foi o único requisito imposto a esse operando (e foi isso que o tornou diferente ->
, conforme explicado acima). Observe que o CRM não exigiu que o operando esquerdo .
tivesse um tipo de estrutura. Apenas exigia que fosse um lvalue, qualquer lvalue. Isso significa que, na versão CRM do C, você pode escrever um código como este
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
Nesse caso, o compilador gravaria 55
em um int
valor posicionado no desvio de bytes 2 no bloco de memória contínuo conhecido como c
, mesmo que type struct T
não tivesse nenhum campo nomeado b
. O compilador não se importaria com o tipo real c
. Tudo o que importava c
era que esse era um valor: algum tipo de bloco de memória gravável.
Agora observe que se você fez isso
S *s;
...
s.b = 42;
o código seria considerado válido (já que s
também é um lvalue) e o compilador tentaria simplesmente gravar dados no ponteiro em s
si , no desvio de bytes 2. Desnecessário dizer que coisas como essa podem resultar em excesso de memória, mas a linguagem não se preocupou com tais assuntos.
Ou seja, nessa versão da linguagem, sua ideia proposta sobre sobrecarregar o operador .
para tipos de ponteiros não funcionaria: o operador .
já tinha um significado muito específico quando usado com ponteiros (com ponteiros lvalue ou com quaisquer lvalues). Era uma funcionalidade muito estranha, sem dúvida. Mas estava lá na época.
Obviamente, essa funcionalidade estranha não é uma razão muito forte contra a introdução de .
operadores sobrecarregados para ponteiros (como você sugeriu) na versão reformulada do C - K&R C. Mas isso não foi feito. Talvez naquela época houvesse algum código legado escrito na versão CRM do C que tivesse que ser suportado.
(O URL do Manual de Referência C de 1975 pode não ser estável. Outra cópia, possivelmente com algumas diferenças sutis, está aqui .)