O .
operador dot ( ) é usado para acessar um membro de uma estrutura, enquanto o operador de seta ( ->
) em C é usado para acessar um membro de uma estrutura que é referenciada pelo ponteiro em questão.
O ponteiro em si não possui nenhum membro que possa ser acessado com o operador de ponto (na verdade, é apenas um número que descreve um local na memória virtual, portanto não possui nenhum membro). Portanto, não haveria ambiguidade se apenas definíssemos o operador de ponto para desreferenciar automaticamente o ponteiro se ele for usado em um ponteiro (uma informação que é conhecida pelo compilador no momento da compilação).
Então, por que os criadores de idiomas decidiram tornar as coisas mais complicadas adicionando esse operador aparentemente desnecessário? Qual é a grande decisão de design?
fonte
.
operador tem maior precedência do que o*
operador? Caso contrário, poderíamos ter * ptr.member e var.member.Respostas:
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çãoA 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
e name
a
representaria o deslocamento 0, enquanto nameb
representaria o deslocamento 2 (assumindo oint
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 declarare isso seria bom, já que o nome
a
consistentemente representaria o deslocamento 0. Mas essa declaração adicionalseria formalmente inválido, pois tentava "redefinir"
a
como deslocamento 2 eb
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 estasA primeira atribuição foi interpretada pelo compilador como "obter endereço
5
, adicionar deslocamento2
a ele e atribuir42
aoint
valor no endereço resultante". Ou seja, o acima atribuiria42
aoint
valor no endereço7
. 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 fazerjá 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 queE1
é do tipo ponteiro, a expressãoE1−>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 esteNesse caso, o compilador gravaria
55
em umint
valor posicionado no desvio de bytes 2 no bloco de memória contínuo conhecido comoc
, mesmo que typestruct T
não tivesse nenhum campo nomeadob
. O compilador não se importaria com o tipo realc
. Tudo o que importavac
era que esse era um valor: algum tipo de bloco de memória gravável.Agora observe que se você fez isso
o código seria considerado válido (já que
s
também é um lvalue) e o compilador tentaria simplesmente gravar dados no ponteiro ems
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 .)
fonte
*i
um valor l de algum tipo padrão (int?) No endereço 5? Então (* i) .b teria funcionado da mesma maneira.struct stat
) prefixam seus campos (por exemplo,st_mode
).Além das razões históricas (boas e já relatadas), também há um pequeno problema com a precedência dos operadores: o operador ponto tem prioridade mais alta que o operador estrela, portanto, se você possui struct contendo ponteiro para struct contendo ponteiro para estrutura ... Esses dois são equivalentes:
Mas o segundo é claramente mais legível. O operador de seta tem a prioridade mais alta (assim como o ponto) e associa da esquerda para a direita. Eu acho que isso é mais claro do que usar o operador de ponto para ponteiros para estruturar e estruturar, porque conhecemos o tipo da expressão sem precisar olhar para a declaração, que pode até estar em outro arquivo.
fonte
a.b.c.d
como(*(*(*a).b).c).d
, tornando o->
operador inútil. Portanto, a versão do OP (a.b.c.d
) é igualmente legível (em comparação coma->b->c->d
). É por isso que sua resposta não responde à pergunta do OP.a.b.c.d
ea->b->c->d
como duas coisas muito diferentes: A primeira é um acesso de memória única a um subobjeto aninhado (neste caso, existe apenas um único objeto de memória ), o segundo são três acessos à memória, perseguindo ponteiros através de quatro prováveis objetos distintos. Essa é uma enorme diferença no layout da memória, e acredito que C esteja certo ao distinguir esses dois casos de maneira muito visível.C também faz um bom trabalho em não tornar nada ambíguo.
Certamente, o ponto pode estar sobrecarregado para significar as duas coisas, mas a seta garante que o programador saiba que está operando em um ponteiro, assim como quando o compilador não permite que você misture dois tipos incompatíveis.
fonte