Por que o operador de seta (->) em C existe?

264

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?

Askaga
fonte
1
Related: stackoverflow.com/questions/221346/… - também, você pode substituir ->
Krease
16
@ Chris Esse é sobre C ++, o que obviamente faz uma grande diferença. Mas já que estamos falando sobre por que o C foi projetado dessa maneira, vamos fingir que estamos de volta na década de 1970 - antes que o C ++ existisse.
Mysticial
5
Meu melhor palpite é que o operador seta existe para expressar visualmente "vê-lo você está lidando com um ponteiro aqui!"
Chris
4
De relance, sinto que essa pergunta é muito estranha. Nem todas as coisas são cuidadosamente planejadas. Se você mantiver esse estilo durante toda a sua vida, seu mundo estará cheio de perguntas. A resposta que obteve mais votos é realmente informativa e clara. Mas isso não atinge o ponto principal da sua pergunta. Siga o estilo da sua pergunta, posso fazer muitas perguntas. Por exemplo, a palavra-chave 'int' é a abreviação de 'integer'; por que a palavra-chave 'double' também não é mais curta?
amigos estão dizendo sobre junwanghe
1
@junwanghe Esta questão realmente representa uma preocupação válida - por que o .operador tem maior precedência do que o *operador? Caso contrário, poderíamos ter * ptr.member e var.member.
milleniumbug

Respostas:

358

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 arepresentaria o deslocamento 0, enquanto name brepresentaria o deslocamento 2 (assumindo o inttipo 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 aconsistentemente representaria o deslocamento 0. Mas essa declaração adicional

struct Y {
  int b;
  int a;
};

seria formalmente inválido, pois tentava "redefinir" acomo deslocamento 2 e bdeslocamento 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 2a ele e atribuir 42ao intvalor no endereço resultante". Ou seja, o acima atribuiria 42ao intvalor 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 *ijá é 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 55em um intvalor posicionado no desvio de bytes 2 no bloco de memória contínuo conhecido como c, mesmo que type struct Tnão tivesse nenhum campo nomeado b. O compilador não se importaria com o tipo real c. Tudo o que importava cera 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 stambém é um lvalue) e o compilador tentaria simplesmente gravar dados no ponteiro em ssi , 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 .)

AnT
fonte
10
E a seção 7.1.8 do Manual de Referência C mencionado diz "Exceto pelo relaxamento do requisito de que E1 seja do tipo ponteiro, a expressão '' E1−> MOS '' é exatamente equivalente a '' (* E1) .MOS ' '. "
9788 Keith Thompson #
1
Por que não havia *ium valor l de algum tipo padrão (int?) No endereço 5? Então (* i) .b teria funcionado da mesma maneira.
Random832
5
@ Leo: Bem, algumas pessoas gostam da linguagem C como montadora de nível superior. Naquele período da história C, o idioma era realmente um montador de nível superior.
AnT
29
Hã. Portanto, isso explica por que muitas estruturas no UNIX (por exemplo, struct stat) prefixam seus campos (por exemplo, st_mode).
icktoofay
5
@ perfectionm1ng: Parece que o bell-labs.com foi adquirido pela Alcatel-Lucent e as páginas originais sumiram. Atualizei o link para outro site, embora não saiba quanto tempo ele permanecerá atualizado. De qualquer forma, pesquisar no "manual de referência do ritchie c" geralmente encontra o documento.
AnT
46

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:

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

a->b->c->d

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.

effeffe
fonte
2
Com os tipos de dados aninhados que contêm estruturas e ponteiros para estruturas, isso pode tornar as coisas mais difíceis, pois é necessário pensar em escolher o operador certo para cada acesso de membro. Você pode acabar com ab-> c-> d ou a-> bc-> d (eu tive esse problema ao usar a biblioteca freetype - eu precisava procurar o código fonte o tempo todo). Além disso, isso não explica por que não seria possível deixar o compilador desreferenciar o ponteiro automaticamente ao lidar com ponteiros.
Askaga 13/11/2012
3
Embora os fatos que você está afirmando estejam corretos, eles não respondem à minha pergunta original de forma alguma. Você explica a igualdade de a-> e * (a). anotações (que já foram explicadas várias vezes em outras questões), além de fornecer uma declaração vaga sobre o design da linguagem ser algo arbitrário. Não achei sua resposta muito útil, portanto, o voto negativo.
28630 Askaga
16
@effeffe, o OP está dizendo que o idioma poderia ser facilmente interpretado a.b.c.dcomo (*(*(*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 com a->b->c->d). É por isso que sua resposta não responde à pergunta do OP.
precisa
4
@ Shahbaz Esse pode ser o caso de um programador java, um programador C / C ++ entenderá a.b.c.de a->b->c->dcomo 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.
cmaster - reinstate monica
2
@ Shahbaz Eu não quis dizer que, como um insulto aos programadores java, eles estão simplesmente acostumados a uma linguagem com ponteiros totalmente implícitos. Se eu tivesse sido educado como programador java, provavelmente pensaria da mesma maneira ... De qualquer forma, acho que a sobrecarga do operador que vemos em C é menos do que ideal. No entanto, reconheço que todos nós fomos mimados pelos matemáticos que sobrecarregam liberalmente seus operadores por praticamente tudo. Eu também entendo a motivação deles, pois o conjunto de símbolos disponíveis é bastante limitado. Eu acho que, no final, é apenas a questão onde traçar a linha ...
cmaster - Reintegrar monica
19

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.

mukunda
fonte
4
Esta é a resposta simples e correta. C tenta principalmente para a sobrecarga de evitar que IMO é uma das melhores coisas sobre C.
jforberg
10
Muitas coisas em C são ambíguas e confusas. Há conversões de tipo implícitas, operadores matemáticos estão sobrecarregados, a indexação encadeada faz algo completamente diferente dependendo se você está indexando uma matriz multidimensional ou uma matriz de ponteiro e qualquer coisa pode ser uma macro oculta qualquer coisa (a convenção de nomenclatura em maiúsculas ajuda, mas C não '' t).
PSKocik