operaciones - punteros como parametros de funciones en c
¿Por qué las matrices en C decaen a punteros? (3)
[Esta es una pregunta inspirada en una discusión reciente en otro lugar, y le daré una respuesta correcta].
Me preguntaba sobre el extraño fenómeno de C de las matrices "decayendo" a punteros, por ejemplo, cuando se usan como argumentos de función. Eso parece tan inseguro. También es inconveniente pasar la longitud explícitamente con él. Y puedo pasar el otro tipo de agregado - estructuras - perfectamente bien por valor; Las estructuras no se descomponen.
¿Cuál es la razón detrás de esta decisión de diseño? ¿Cómo se integra con el lenguaje? ¿Por qué hay una diferencia en las estructuras?
La respuesta a esta pregunta se puede encontrar en el documento "El desarrollo del lenguaje C" de Dennis Ritchie (consulte la sección "C embrionaria")
Según Dennis Ritchie, las versiones incipientes de C semántica de matrices directamente heredada / adoptada de los lenguajes B y BCPL, predecesoras de C. En esos idiomas, las matrices se implementaron literalmente como punteros físicos. Estos punteros apuntaban a bloques de memoria asignados independientemente que contienen los elementos de la matriz real. Estos punteros se inicializaron en tiempo de ejecución. Es decir, en los días B y BCPL, las matrices se implementaron como objetos "binarios" (bipartitos): un puntero independiente que apunta a un bloque de datos independiente. No hubo diferencia entre la semántica de puntero y matriz en esos idiomas, aparte del hecho de que los punteros de matriz se inicializaron automáticamente. En cualquier momento fue posible reasignar un puntero de matriz en B y BCPL para que apunte a otro lugar.
Inicialmente, este enfoque de la semántica de matrices fue heredado por C. Sin embargo, sus inconvenientes se volvieron inmediatamente obvios cuando se introdujeron los tipos de
struct
en el lenguaje (algo que ni B ni BCPL tenían).
Y la idea era que las estructuras naturalmente deberían poder contener matrices.
Sin embargo, continuar apegándose a la naturaleza "bipartita" anterior de las matrices B / BCPL conduciría inmediatamente a una serie de complicaciones obvias con las estructuras.
Por ejemplo, los objetos de estructura con matrices en el interior requerirían una "construcción" no trivial en el punto de definición.
Sería imposible copiar tales objetos de estructura: una llamada de
memcpy
sin
memcpy
copiaría los punteros de matriz sin copiar los datos reales.
Uno no podría
malloc
estructurar objetos, ya que
malloc
solo puede asignar memoria sin procesar y no desencadena ninguna inicialización no trivial.
Y así sucesivamente y así sucesivamente.
Esto se consideró inaceptable, lo que condujo al rediseño de las matrices C. En lugar de implementar matrices a través de punteros físicos, Ritchie decidió deshacerse de los punteros por completo. La nueva matriz se implementó como un único bloque de memoria inmediata, que es exactamente lo que tenemos en C hoy. Sin embargo, por razones de compatibilidad con versiones anteriores, el comportamiento de las matrices B / BCPL se conservó (emuló) lo más posible a nivel superficial: la nueva matriz C se descompuso fácilmente a un valor de puntero temporal, apuntando al comienzo de la matriz. El resto de la funcionalidad de la matriz se mantuvo sin cambios, confiando en ese resultado fácilmente disponible de la descomposición.
Para citar el trabajo antes mencionado
La solución constituyó el salto crucial en la cadena evolutiva entre BCPL sin tipo y tipo C. Eliminó la materialización del puntero almacenado y, en su lugar, causó la creación del puntero cuando el nombre de la matriz se menciona en una expresión. La regla, que sobrevive en la C de hoy, es que los valores del tipo de matriz se convierten, cuando aparecen en expresiones, en punteros al primero de los objetos que forman la matriz.
Esta invención permitió que la mayoría del código B existente continuara funcionando, a pesar del cambio subyacente en la semántica del lenguaje. Los pocos programas que asignaron nuevos valores a un nombre de matriz para ajustar su origen, posible en B y BCPL, sin sentido en C, se repararon fácilmente. Más importante aún, el nuevo lenguaje retuvo una explicación coherente y viable (si no inusual) de la semántica de las matrices, al tiempo que abrió el camino a una estructura de tipo más completa.
Entonces, la respuesta directa a su pregunta de "por qué" es la siguiente: las matrices en C fueron diseñadas para decaer a los punteros para emular (lo más cerca posible) el comportamiento histórico de las matrices en los lenguajes B y BCPL.
Tome su máquina del tiempo y viaje de regreso a 1970. Comience a diseñar un lenguaje de programación. Desea compilar el siguiente código y hacer lo esperado:
size_t i;
int* p = (int *) malloc (10 * sizeof (int));
for (i = 0; i < 10; ++i) p [i] = i;
int a [10];
for (i = 0; i < 10; ++i) a [i] = i;
Al mismo tiempo, quieres un lenguaje que sea simple. Suficientemente simple como para compilarlo en una computadora de los años 70. La regla de que "a" se descompone en "puntero al primer elemento de a" lo logra muy bien.
Razón fundamental
Examinemos las llamadas a funciones porque los problemas son muy visibles allí: ¿por qué las matrices no se pasan simplemente a las funciones como matrices, por valor, como una copia?
Primero hay una razón puramente pragmática: las matrices pueden ser grandes; Puede que no sea aconsejable pasarlos por valor porque podrían exceder el tamaño de la pila, especialmente en la década de 1970. Los primeros compiladores se escribieron en un PDP-7 con aproximadamente 9 kB de RAM.
También hay una razón más técnica arraigada en el lenguaje. Sería difícil generar código para una llamada a función con argumentos cuyo tamaño no se conoce en tiempo de compilación. Para todas las matrices, incluidas las matrices de longitud variable en C moderna, simplemente las direcciones se colocan en la pila de llamadas. El tamaño de una dirección es, por supuesto, bien conocido. Incluso los lenguajes con tipos de matriz elaborados que llevan información sobre el tamaño del tiempo de ejecución no pasan los objetos propiamente dichos en la pila. Estos lenguajes suelen pasar por "asas", que es lo que C también ha hecho efectivamente durante 40 años. Vea Jon Skeet here y una explicación ilustrada a la que hace referencia (sic) here .
Ahora, un lenguaje podría exigir que una matriz siempre tenga un tipo completo;
es decir, cada vez que se usa, su declaración completa, incluido el tamaño, debe ser visible.
Esto es, después de todo, lo que C requiere de las estructuras (cuando se accede a ellas).
En consecuencia, las estructuras se pueden pasar a funciones por valor.
Requerir el tipo completo para las matrices también haría que las llamadas a funciones fueran fácilmente compilables y obviara la necesidad de pasar argumentos de longitud adicionales:
sizeof()
aún funcionaría como se esperaba dentro del destinatario.
Pero imagina lo que eso significa.
Si el tamaño fuera realmente parte del tipo de argumento de la matriz, necesitaríamos una función distinta para cada tamaño de matriz:
// for user input.
int average_Ten(int arr[10]);
// for my new Hasselblad.
int average_ThreeTrillionThreehundredninetythreeBillionNinehundredtwentyeightMillionEighthundredsixthousandfourhundred(int arr[16544*12400]);
// ...
De hecho, sería totalmente comparable a las estructuras de paso, que difieren en tipo si sus elementos difieren (por ejemplo, una estructura con 10 elementos int y otra con 16544 * 12400). Es obvio que las matrices necesitan más flexibilidad. Por ejemplo, como se demostró, uno no podría proporcionar sensatamente funciones de biblioteca generalmente utilizables que tomen argumentos de matriz.
Este "acertijo de tipeo fuerte" es, de hecho, lo que sucede en C ++ cuando una función toma una referencia a una matriz; esa es también la razón por la que nadie lo hace, al menos no explícitamente. Es totalmente inconveniente hasta el punto de ser inútil, excepto en casos que apuntan a usos específicos, y en código genérico: las plantillas C ++ proporcionan flexibilidad en tiempo de compilación que no está disponible en C.
Si, en C existente, las matrices de tamaños conocidos se deben pasar por valor, siempre existe la posibilidad de envolverlas en una estructura. Recuerdo que algunos encabezados relacionados con IP en Solaris definieron estructuras de familias de direcciones con matrices en ellos, lo que permite copiarlos. Debido a que el diseño de bytes de la estructura era fijo y conocido, eso tenía sentido.
Para algunos antecedentes también es interesante leer El desarrollo del lenguaje C de Dennis Ritchie sobre los orígenes del predecesor de C.C, BCPL no tenía ninguna matriz; la memoria era solo memoria lineal homogénea con punteros en ella.