Por que a sintaxe C para matrizes, ponteiros e funções foi projetada dessa maneira?

16

Depois de ter visto (e perguntado!) Tantas perguntas semelhantes a

O que int (*f)(int (*a)[5])significa em C?

e mesmo vendo que eles criaram um programa para ajudar as pessoas a entender a sintaxe C, não posso deixar de me perguntar:

Por que a sintaxe de C foi projetada dessa maneira?

Por exemplo, se eu estivesse projetando ponteiros, eu traduziria "um ponteiro para uma matriz de 10 elementos" em

int*[10]* p;

e não

int* (*p)[10];

o que eu acho que a maioria das pessoas concorda que é muito menos direto.

Então, eu estou me perguntando, por que a sintaxe não intuitiva? Houve um problema específico que a sintaxe resolve (talvez uma ambiguidade?) Que eu desconheço?

user541686
fonte
2
Você sabe que não há uma resposta real para essas e outras perguntas. Certo? O que você vai conseguir são apenas suposições.
BЈовић
7
@VJo - pode muito bem haver uma resposta "real" (isto é, objetiva) - autores de linguagem e comitês de normas justificaram explicitamente (ou pelo menos explicaram) muitas dessas decisões.
detly
Não acho que a sintaxe proposta seja necessariamente mais ou menos "intuitiva" que a sintaxe C. C é o que é; depois de aprender, você nunca mais terá essas perguntas. Se você não aprendeu ... bem, talvez esse seja o verdadeiro problema.
Caleb
1
@Caleb: Engraçado como você concluiu que tão facilmente, porque eu aprendi isso e eu ainda tinha essa pergunta ...
user541686
1
O cdeclcomando é muito útil para decodificar declarações C complexas. Há também uma interface da web em cdecl.org .
9301 Keith Thompson #

Respostas:

16

Meu entendimento da história disso é que ele se baseia em dois pontos principais ...

Em primeiro lugar, os autores da linguagem preferiram tornar a sintaxe centrada na variável em vez de centrada no tipo. Ou seja, eles queriam que um programador olhasse para a declaração e pensasse "se eu escrever a expressão *func(arg), isso resultará em um int; se eu escrever *arg[N], terei um float" em vez de " funcdeve ser um ponteiro para uma função que aceita isso". e devolvendo isso ".

A entrada C na Wikipedia afirma que:

A ideia de Ritchie era declarar identificadores em contextos semelhantes ao seu uso: "declaração reflete uso".

... citando a p122 de K & R2 que, infelizmente, não preciso entregar para encontrar a cotação estendida para você.

Em segundo lugar, é realmente muito difícil criar uma sintaxe para declaração que seja consistente quando você estiver lidando com níveis arbitrários de indireção. Seu exemplo pode funcionar bem para expressar o tipo que você pensou lá fora, mas é dimensionado para uma função que leva um ponteiro para uma matriz desses tipos e retorna alguma outra bagunça hedionda? (Talvez sim, mas você verificou? Você pode provar isso? ).

Lembre-se de que parte do sucesso de C se deve ao fato de que os compiladores foram escritos para muitas plataformas diferentes e, portanto, poderia ter sido melhor ignorar algum grau de legibilidade para facilitar a gravação dos compiladores.

Dito isto, não sou especialista em gramática ou redação de compiladores. Mas eu sei o suficiente para saber que há muito o que saber;)

detly
fonte
2
"facilitando a compilação de compiladores" ... exceto que C é notório por ser difícil de analisar (apenas coberto por C ++).
Jan Hudec
1
@JanHudec - Bem ... sim. Essa não é uma afirmação estanque. Mas, embora seja impossível analisar C como uma gramática livre de contexto, uma vez que uma pessoa tenha uma maneira de analisá-la, isso deixa de ser o passo mais difícil. E o fato é que foi prolífico em seus primeiros dias devido ao fato de as pessoas poderem usar os compiladores facilmente, então a K&R deve ter atingido algum equilíbrio. (No infame The Rise of "Worse is Better" , de Richard Gabriel , ele considera que é fácil escrever um compilador C para uma nova plataforma.
detly
Fico feliz em ser corrigido sobre isso, a propósito - não sei muito sobre análise e gramática. Vou deduzir mais os fatos históricos.
detly
12

Muitas peculiaridades da linguagem C podem ser explicadas pela maneira como os computadores funcionavam quando foram projetados. Havia quantidades muito limitadas de memória de armazenamento, por isso era muito importante minimizar o tamanho dos arquivos de código-fonte . A prática de programação nos anos 70 e 80 era garantir que o código-fonte contivesse o mínimo de caracteres possível e, de preferência, nenhum comentário excessivo no código-fonte.

Hoje em dia isso é ridículo, com espaço de armazenamento praticamente ilimitado em discos rígidos. Mas é parte da razão pela qual C tem uma sintaxe tão estranha em geral.


Em relação aos ponteiros de array especificamente, seu segundo exemplo deve ser int (*p)[10];(sim, a sintaxe é muito confusa). Talvez eu lesse isso como "int ponteiro para uma série de dez" ... o que faz sentido. Se não fosse o parêntese, o compilador o interpretaria como uma matriz de dez ponteiros, o que daria à declaração um significado totalmente diferente.

Como ponteiros de matriz e ponteiros de função têm sintaxe bastante obscura em C, a coisa mais sensata a fazer é digitar a estranheza. Talvez assim:

Exemplo obscuro:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Exemplo equivalente não obscuro:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

As coisas podem ficar ainda mais obscuras se você estiver lidando com matrizes de ponteiros de função. Ou a mais obscura de todas: funções retornando indicadores de função (levemente úteis). Se você não usar typedefs para essas coisas, ficará rapidamente louco.


fonte
Ah, finalmente, uma resposta razoável. :-) Estou curioso para saber como a sintaxe específica reduziria o tamanho do código-fonte, mas de qualquer maneira é uma idéia plausível e faz sentido. Obrigado. 1
user541686
Eu diria que era menos sobre o tamanho do código fonte e mais sobre a escrita do compilador, mas definitivamente +1 para "digitar a estranheza". Minha saúde mental melhorou dramaticamente no dia em que percebi que poderia fazer isso.
detly
2
[Citação necessário] na coisa do tamanho do código fonte. Eu nunca ouvi falar dessa limitação (embora talvez seja algo "todo mundo sabe").
Sean McMillan
1
Bem, eu codifiquei programas nos anos 70 em COBOL, Assembler, CORAL e PL / 1 no kit IBM, DEC e XEROX e nunca encontrei uma limitação de tamanho de código-fonte. Limitações no tamanho da matriz, tamanho do executável, tamanho do nome do programa - mas nunca o tamanho do código-fonte.
James Anderson
1
@ Sean McMillan: Eu não acho que o tamanho do código fonte seja uma limitação (considere que naquela época linguagens detalhadas como o Pascal eram bastante populares). E mesmo que fosse esse o caso, acho que teria sido muito fácil analisar o código-fonte e substituir as palavras-chave longas por códigos curtos de um byte (como, por exemplo, alguns intérpretes do Basic). Portanto, acho o argumento "C é conciso porque foi inventado em um período em que menos memória estava disponível" um pouco fraco.
Giorgio
7

É bem simples: int *psignifica que *pé um int; int a[5]significa que a[i]é um int.

int (*f)(int (*a)[5])

Significa que *fé uma função, *aé uma matriz de cinco números inteiros, assim fcomo uma função que leva um ponteiro para uma matriz de cinco números inteiros e retorna int. No entanto, em C, não é útil passar um ponteiro para uma matriz.

As declarações C muito raramente ficam complicadas.

Além disso, você pode esclarecer usando typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);
Kevin Cline
fonte
4
Desculpas se isso parece rude (eu não pretendo que seja), mas acho que você perdeu todo o ponto da pergunta ...: \
user541686
2
@ Mehrdad: Não sei dizer o que estava na mente de Kernighan e Ritchie; Eu contei a lógica por trás da sintaxe. Não conheço a maioria das pessoas, mas não acho que sua sintaxe sugerida seja mais clara.
Kevin cline
Eu concordo - é incomum ver uma declaração tão complicada.
Caleb
O design do C declarações antecede typedef, const, volatile, e a capacidade de inicializar as coisas dentro de declarações. Muitas das ambiguidades irritantes da sintaxe da declaração (por exemplo, se int const *p, *q;devem se vincular constao tipo ou ao declarand) não poderiam surgir no idioma como originalmente projetado. Eu gostaria que o idioma tivesse adicionado dois pontos entre o tipo e o declarando, mas permitia sua omissão ao usar tipos internos de "palavra reservada" sem qualificadores. O significado de int: const *p,*q;e int const *: p,*q;teria sido claro.
Supercat
3

Eu acho que você deve considerar * [] como operadores anexados a uma variável. * é escrito antes de uma variável, [] depois.

Vamos ler a expressão de tipo

int* (*p)[10];

O elemento mais interno é p, uma variável, portanto

p

significa: p é uma variável.

Antes da variável existir um *, o operador * é sempre colocado antes da expressão a que se refere, portanto,

(*p)

significa: a variável p é um ponteiro. Sem o (), o operador [] à direita teria maior precedência, ou seja,

**p[]

seria analisado como

*(*(p[]))

O próximo passo é []: como não há mais (), [] tem maior precedência que o externo *, portanto

(*p)[]

significa: (a variável p é um ponteiro) para uma matriz. Então nós temos o segundo *:

* (*p)[]

significa: ((variável p é um ponteiro) para uma matriz) de ponteiros

Finalmente, você tem o operador int (um nome de tipo), que tem a menor precedência:

int* (*p)[]

significa: (((variável p é um ponteiro) para uma matriz) de ponteiros) para inteiro.

Portanto, todo o sistema é baseado em expressões de tipo com operadores, e cada operador possui suas próprias regras de precedência. Isso permite definir tipos muito complexos.

Giorgio
fonte
0

Não é tão difícil quando você começa a pensar e C nunca foi uma linguagem muito fácil. E int*[10]* prealmente não é mais fácil do que int* (*p)[10] E que tipo de k seriaint*[10]* p, k;

Dainius
fonte
2
k seria uma revisão de código com falha, posso descobrir o que o compilador fará, posso até me incomodar, mas não consigo descobrir o que o programador pretendia - falhar ............
mattnz
e por que k falhou na revisão de código?
Dainius
1
porque o código é ilegível e impossível de manter. O código não está correto para corrigir, obviamente está correto e provavelmente permanecerá correto durante a manutenção. O fato de você precisar perguntar qual será o tipo k é um sinal de que o código não atende a esses requisitos básicos.
mattnz
1
Essencialmente, existem 3 (neste caso) declarações de variáveis ​​de tipos diferentes na mesma linha, por exemplo, int * p, int i [10] e int k. Isso é inaceitável. Várias declarações do mesmo tipo são aceitáveis, desde que as variáveis ​​tenham alguma forma de relacionamento, por exemplo, int width, height, depth; Lembre-se de que muitas pessoas programam usando int * p, então o que está em 'int * p, i;'.
mattnz
1
O que @mattnz está tentando dizer é que você pode ser tão esperto quanto quiser, mas tudo não faz sentido quando sua intenção não é óbvia e / ou seu código é mal escrito / ilegível. Esse tipo de coisa geralmente resulta em código quebrado e perda de tempo. Além disso, pointer to inte intnem são do mesmo tipo, portanto devem ser declarados separadamente. Período. Escute o homem. Ele tem 18k representantes por um motivo.
Braden Best