¿Por qué se diseñó de esta manera la sintaxis C para matrices, punteros y funciones?

16

Después de haber visto (¡y preguntado!) Tantas preguntas similares a

¿Qué int (*f)(int (*a)[5])significa en C?

e incluso viendo que habían hecho un programa para ayudar a las personas a comprender la sintaxis de C, no puedo evitar preguntarme:

¿Por qué se diseñó la sintaxis de C de esta manera?

Por ejemplo, si estuviera diseñando punteros, traduciría "un puntero a una matriz de punteros de 10 elementos" en

int*[10]* p;

y no

int* (*p)[10];

que creo que la mayoría de la gente estaría de acuerdo es mucho menos directo.

Así que me pregunto, ¿por qué la sintaxis no intuitiva? ¿Hubo un problema específico que resuelve la sintaxis (¿tal vez una ambigüedad?) Que desconozco?

usuario541686
fuente
2
Sabes que no hay una respuesta real a esto, y preguntas similares. ¿Derecho? Lo que obtendrás son solo conjeturas.
partir del
77
@VJo: bien puede haber una respuesta "real" (es decir, objetiva), tanto los autores de lenguaje como los comités de normas han justificado explícitamente (o al menos explicado) muchas de estas decisiones.
detly
No creo que la sintaxis propuesta sea necesariamente más o menos "intuitiva" que la sintaxis C. C es lo que es; una vez que lo hayas aprendido, nunca volverás a tener estas preguntas. Si no lo has aprendido ... bueno, tal vez ese sea el verdadero problema.
Caleb
1
@Caleb: Es curioso cómo se llegó a la conclusión de que tan fácilmente, porque lo aprendí y yo todavía tenía esta pregunta ...
user541686
1
El cdeclcomando es muy útil para decodificar declaraciones complejas de C. También hay una interfaz web en cdecl.org .
Keith Thompson el

Respuestas:

16

Mi comprensión de la historia es que se basa en dos puntos principales ...

En primer lugar, los autores del lenguaje prefirieron hacer la sintaxis centrada en la variable en lugar de centrada en el tipo. Es decir, querían que un programador mirara la declaración y pensara "si escribo la expresión *func(arg), eso dará como resultado int; si escribo *arg[N]tendré un flotante" en lugar de " funcdebe ser un puntero a una función que tome esto y devolviendo eso ".

La entrada C en Wikipedia afirma que:

La idea de Ritchie era declarar identificadores en contextos similares a su uso: "la declaración refleja el uso".

... citando p122 de K & R2 que, por desgracia, no tengo que buscar para encontrar la cotización extendida para usted.

En segundo lugar, es realmente muy difícil encontrar una sintaxis para la declaración que sea consistente cuando se trata de niveles arbitrarios de indirección. Su ejemplo podría funcionar bien para expresar el tipo que pensó de inmediato, pero ¿se adapta a una función que toma un puntero a una serie de esos tipos y devuelve algún otro desastre horrible? (Tal vez lo hace, pero ¿verificó? ¿Puedes probarlo? ).

Recuerde, parte del éxito de C se debe al hecho de que los compiladores se escribieron para muchas plataformas diferentes, por lo que podría haber sido mejor ignorar cierto grado de legibilidad en aras de facilitar la escritura de los compiladores.

Dicho esto, no soy un experto en gramática del lenguaje o redacción de compiladores. Pero sé lo suficiente como para saber que hay mucho que saber;)

desviarse
fuente
2
"haciendo que los compiladores sean más fáciles de escribir" ... excepto que C es conocido por ser difícil de analizar (solo superado por C ++).
Jan Hudec
1
@ JanHudec - Bueno ... sí. Esa no es una declaración hermética. Pero aunque C es imposible de analizar como una gramática libre de contexto, una vez que una persona ha encontrado una forma de analizarlo, deja de ser el paso difícil. Y el hecho es que fue prolífico en sus primeros días debido a que las personas podían sacar compiladores fácilmente, por lo que K&R debe haber alcanzado cierto equilibrio. (De Richard Gabriel infame En el aumento del "peor es mejor" , se da por sentado - y se lamenta -. El hecho de que es fácil escribir un compilador de C para una nueva plataforma)
detly
Estoy feliz de que me corrijan en esto, por cierto, no sé mucho sobre análisis y gramática. Voy más en inferencia del hecho histórico.
detly
12

Muchas de las rarezas del lenguaje C pueden explicarse por la forma en que funcionaban las computadoras cuando fue diseñado. Había cantidades muy limitadas de memoria de almacenamiento, por lo que era muy importante minimizar el tamaño de los archivos de código fuente . La práctica de programación en los años 70 y 80 consistía en asegurarse de que el código fuente contuviera la menor cantidad posible de caracteres y, preferiblemente, que no hubiera comentarios excesivos sobre el código fuente.

Por supuesto, esto es ridículo hoy en día, con espacio de almacenamiento prácticamente ilimitado en los discos duros. Pero es parte de la razón por la cual C tiene una sintaxis tan extraña en general.


Con respecto a los punteros de matriz específicamente, su segundo ejemplo debería ser int (*p)[10];(sí, la sintaxis es muy confusa). Quizás lo leería como "int puntero a una matriz de diez" ... lo que tiene sentido de alguna manera. Si no fuera por el paréntesis, el compilador lo interpretaría como una matriz de diez punteros, lo que le daría a la declaración un significado completamente diferente.

Dado que los punteros de matriz y los punteros de función tienen una sintaxis bastante oscura en C, lo más sensato es eliminar la rareza. Quizás así:

Ejemplo oscuro:

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);
}

Ejemplo no oscuro, equivalente:

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);
}

Las cosas pueden volverse aún más oscuras si se trata de matrices de punteros de función. O el más oscuro de todos: funciones que devuelven punteros de función (ligeramente útiles). Si no usa typedefs para tales cosas, rápidamente se volverá loco.


fuente
Ah, finalmente una respuesta razonable. :-) Tengo curiosidad por saber cómo la sintaxis particular realmente reduciría el tamaño del código fuente, pero de todos modos es una idea plausible y tiene sentido. Gracias. +1
user541686
Yo diría que se trataba menos del tamaño del código fuente y más sobre la escritura del compilador, pero definitivamente +1 para "Typdef lejos la rareza". Mi salud mental mejoró dramáticamente el día que me di cuenta de que podía hacer esto.
detly
2
[Cita requerida] sobre el tamaño del código fuente. Nunca he oído hablar de tal limitación (aunque tal vez sea algo que "todo el mundo sabe").
Sean McMillan
1
Bueno, codifiqué programas en los años 70 en COBOL, Assembler, CORAL y PL / 1 en el kit IBM, DEC y XEROX y nunca me encontré con una limitación de tamaño del código fuente. Limitaciones en el tamaño de la matriz, el tamaño del ejecutable, el tamaño del nombre del programa, pero nunca el tamaño del código fuente.
James Anderson el
1
@Sean McMillan: No creo que el tamaño del código fuente fuera una limitación (considere que en ese momento los lenguajes detallados como Pascal eran bastante populares). E incluso si este hubiera sido el caso, creo que habría sido muy fácil analizar el código fuente y reemplazar las palabras clave largas por códigos cortos de un byte (como, por ejemplo, algunos intérpretes básicos solían hacerlo). Entonces encuentro el argumento "C es conciso porque fue inventado en un período en que había menos memoria disponible" un poco débil.
Giorgio
7

Es bastante simple: int *psignifica que *pes un int; int a[5]significa que a[i]es un int.

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

Significa que *fes una función, *aes una matriz de cinco enteros, por lo que fes una función que toma un puntero a una matriz de cinco enteros y devuelve int. Sin embargo, en C no es útil pasar un puntero a una matriz.

Las declaraciones de C rara vez se complican tanto.

Además, puede aclarar usando typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);
Kevin Cline
fuente
44
Disculpas si esto suena grosero (no quiero decir que sea), pero creo que te perdiste todo el punto de la pregunta ...: \
user541686
2
@Mehrdad: No puedo decirte lo que estaba en la mente de Kernighan y Ritchie; Te dije la lógica detrás de la sintaxis. No sé sobre la mayoría de las personas, pero no creo que su sintaxis sugerida sea más clara.
Kevin Cline
Estoy de acuerdo, es inusual ver una declaración tan complicada.
Caleb
El diseño de Declaraciones en C predates typedef, const, volatile, y la capacidad para inicializar las cosas dentro de declaraciones. Muchas de las molestas ambigüedades de la sintaxis de la declaración (p. Ej., Si se int const *p, *q;deben unir constal tipo o al declarante) no pueden surgir en el lenguaje como se diseñó originalmente. Desearía que el lenguaje hubiera agregado dos puntos entre el tipo y el declarante, pero permitió su omisión al usar tipos integrados de "palabra reservada" sin calificadores. El significado de int: const *p,*q;y int const *: p,*q;habría sido claro.
Supercat
3

Creo que debe considerar * [] como operadores que están unidos a una variable. * se escribe antes de una variable, [] después.

Leamos la expresión de tipo

int* (*p)[10];

El elemento más interno es p, una variable, por lo tanto

p

significa: p es una variable.

Antes de la variable hay un *, el operador * siempre se antepone a la expresión a la que se refiere, por lo tanto,

(*p)

significa: la variable p es un puntero. Sin el () el operador [] a la derecha tendría mayor prioridad, es decir

**p[]

sería analizado como

*(*(p[]))

El siguiente paso es []: como no hay más (), [] tiene mayor prioridad que el exterior *, por lo tanto

(*p)[]

significa: (la variable p es un puntero) a una matriz. Luego tenemos el segundo *:

* (*p)[]

significa: ((la variable p es un puntero) a una matriz) de punteros

Finalmente tiene el operador int (un nombre de tipo), que tiene la precedencia más baja:

int* (*p)[]

significa: (((la variable p es un puntero) a una matriz) de punteros) a entero.

Por lo tanto, todo el sistema se basa en expresiones de tipo con operadores, y cada operador tiene sus propias reglas de precedencia. Esto permite definir tipos muy complejos.

Giorgio
fuente
0

No es tan difícil cuando empiezas a pensar y C nunca fue un lenguaje muy fácil. Y int*[10]* prealmente no es más fácil que int* (*p)[10] Y qué tipo de k estaría enint*[10]* p, k;

Dainius
fuente
2
k sería una revisión de código fallida, puedo resolver lo que hará el compilador, incluso puede que me moleste, pero no puedo resolver lo que pretendía el programador - falla ............
mattnz
y por qué k fallaría la revisión del código?
Dainius
1
porque el código es ilegible e imposible de mantener. El código no es correcto para corregir, obviamente correcto y probablemente seguirá siendo correcto durante el mantenimiento. El hecho de que tenga que preguntar qué tipo de k será una señal de que el código no cumple estos requisitos básicos.
mattnz
1
Esencialmente, hay 3 (en este caso) declaraciones de variables de diferentes tipos en la misma fila, por ejemplo, int * p, int i [10] e int k. Eso es inaceptable. Se aceptan múltiples declaraciones del mismo tipo, siempre que las variables tengan alguna forma de relación, por ejemplo, int ancho, alto, profundidad; Tenga en cuenta que muchas personas programan usando int * p, entonces, ¿qué hay en 'int * p, i;'?
mattnz
1
Lo que @mattnz está tratando de decir es que puedes ser tan inteligente como quieras, pero no tiene sentido cuando tu intención no es obvia y / o tu código está mal escrito / ilegible. Este tipo de cosas a menudo resulta en código roto y tiempo perdido. Además, pointer to inty intni siquiera son del mismo tipo, por lo que deben declararse por separado. Período. Escucha al hombre. Tiene 18k rep por una razón.
Braden Best el