¿Por qué los comstackdores C y C ++ permiten longitudes de matriz en firmas de funciones cuando nunca se aplican?

Esto es lo que encontré durante mi período de aprendizaje:

#include using namespace std; int dis(char a[1]) { int length = strlen(a); char c = a[2]; return length; } int main() { char b[4] = "abc"; int c = dis(b); cout << c; return 0; } 

Entonces, en la variable int dis(char a[1]) , el [1] parece no hacer nada y no funciona en
todo, porque puedo usar a[2] . Al igual que int a[] o char *a . Sé que el nombre de la matriz es un puntero y cómo transmitir una matriz, por lo que mi rompecabezas no es sobre esta parte.

Lo que quiero saber es por qué los comstackdores permiten este comportamiento ( int a[1] ). ¿O tiene otros significados que yo no conozco?

Es una peculiaridad de la syntax para pasar arreglos a funciones.

En realidad, no es posible pasar una matriz en C. Si escribe la syntax que parece que debería pasar la matriz, lo que realmente sucede es que se pasa un puntero al primer elemento de la matriz.

Como el puntero no incluye ninguna información de longitud, el contenido de su [] en la lista de parámetros formales de la función se ignora.

La decisión de permitir esta syntax se tomó en la década de 1970 y ha causado mucha confusión desde entonces …

La longitud de la primera dimensión se ignora, pero la longitud de las dimensiones adicionales es necesaria para permitir que el comstackdor calcule las compensaciones correctamente. En el siguiente ejemplo, la función foo pasa un puntero a una matriz bidimensional.

 #include  void foo(int args[10][20]) { printf("%zd\n", sizeof(args[0])); } int main(int argc, char **argv) { int a[2][20]; foo(a); return 0; } 

El tamaño de la primera dimensión [10] se ignora; el comstackdor no le impedirá indexar el final (observe que el formal quiere 10 elementos, pero el real solo proporciona 2). Sin embargo, el tamaño de la segunda dimensión [20] se utiliza para determinar la zancada de cada fila, y aquí, la formal debe coincidir con la real. Una vez más, el comstackdor tampoco le impedirá indexar el final de la segunda dimensión.

El desplazamiento de bytes desde la base de la matriz a un elemento args[row][col] viene determinado por:

 sizeof(int)*(col + 20*row) 

Tenga en cuenta que si col >= 20 , en realidad indexará en una fila posterior (o en el extremo de la matriz completa).

sizeof(args[0]) , devuelve 80 en mi máquina donde sizeof(int) == 4 . Sin embargo, si bash tomar sizeof(args) , sizeof(args) la siguiente advertencia del comstackdor:

 foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument] printf("%zd\n", sizeof(args)); ^ foo.c:3:14: note: declared here void foo(int args[10][20]) ^ 1 warning generated. 

Aquí, el comstackdor advierte que solo dará el tamaño del puntero al que se ha descompuesto la matriz en lugar del tamaño de la matriz.

El problema y cómo superarlo en C ++

El problema ha sido explicado extensamente por pat y Matt . El comstackdor ignora básicamente la primera dimensión del tamaño de la matriz ignorando el tamaño del argumento pasado.

En C ++, por otro lado, puede superar fácilmente esta limitación de dos maneras:

  • usando referencias
  • usando std::array (desde C ++ 11)

Referencias

Si su función solo está tratando de leer o modificar una matriz existente (no copiarla) puede usar referencias fácilmente.

Por ejemplo, supongamos que desea tener una función que restablezca una matriz de diez int s configurando cada elemento a 0 . Puede hacerlo fácilmente usando la siguiente firma de función:

 void reset(int (&array)[10]) { ... } 

No solo esto funcionará bien , sino que también hará cumplir la dimensión de la matriz .

También puede hacer uso de plantillas para hacer que el código anterior sea genérico :

 template void reset(Type (&array)[N]) { ... } 

Y finalmente puedes aprovechar la corrección const . Consideremos una función que imprime una matriz de 10 elementos:

 void show(const int (&array)[10]) { ... } 

Al aplicar el calificador const evitamos posibles modificaciones .


La clase de biblioteca estándar para matrices

Si consideras que la syntax anterior es fea e innecesaria, como yo, podemos lanzarla a la lata y usar std::array lugar (desde C ++ 11).

Aquí está el código refactorizado:

 void reset(std::array& array) { ... } void show(std::array const& array) { ... } 

¿No es maravilloso? Sin mencionar que el truco del código genérico que te he enseñado anteriormente, todavía funciona:

 template void reset(std::array& array) { ... } template void show(const std::array& array) { ... } 

No solo eso, sino que obtienes copia y movimiento semántico de forma gratuita. 🙂

 void copy(std::array array) { // a copy of the original passed array // is made and can be dealt with indipendently // from the original } 

¿Entonces, Qué esperas? Ir a usar std::array .

Es una característica divertida de C que le permite dispararse eficazmente en el pie si así lo desea.

Creo que la razón es que C está un paso por encima del lenguaje ensamblador. La verificación de tamaño y características de seguridad similares se han eliminado para permitir el máximo rendimiento, lo cual no es malo si el progtwigdor está siendo muy diligente.

Además, la asignación de un tamaño al argumento de función tiene la ventaja de que cuando la función es utilizada por otro progtwigdor, existe la posibilidad de que noten una restricción de tamaño. El solo uso de un puntero no transmite esa información al siguiente progtwigdor.

Primero, C nunca verifica los límites de la matriz. No importa si son locales, globales, estáticos, parámetros, lo que sea. Comprobar los límites de la matriz significa más procesamiento, y se supone que C es muy eficiente, por lo que el progtwigdor realiza la comprobación de límites de la matriz cuando es necesario.

En segundo lugar, hay un truco que permite pasar de una matriz a una función. También es posible devolver-por-valor una matriz desde una función. Solo necesita crear un nuevo tipo de datos usando struct. Por ejemplo:

 typedef struct { int a[10]; } myarray_t; myarray_t my_function(myarray_t foo) { myarray_t bar; ... return bar; } 

Tienes que acceder a elementos como este: foo.a [1]. La “.a” adicional puede parecer extraña, pero este truco agrega una gran funcionalidad al lenguaje C.

Para decirle al comstackdor que myArray apunta a una matriz de al menos 10 entradas:

 void bar(int myArray[static 10]) 

Un buen comstackdor debería darle una advertencia si accede a myArray [10]. Sin la palabra clave “estática”, el 10 no significaría nada en absoluto.

Esta es una “característica” bien conocida de C, pasada a C ++ porque se supone que C ++ comstack correctamente el código C.

El problema surge de varios aspectos:

  1. Se supone que un nombre de matriz es completamente equivalente a un puntero.
  2. C se supone que es rápido, originalmente desarrollado para ser una especie de “Ensamblador de alto nivel” (especialmente diseñado para escribir el primer “Sistema operativo portátil”: Unix), por lo que no se supone que debe insertar código “oculto”; la verificación del rango de tiempo de ejecución está, por lo tanto, “prohibida”.
  3. El código de máquina generado para acceder a una matriz estática o dinámica (en la stack o asignada) es realmente diferente.
  4. Como la función llamada no puede conocer el “tipo” de matriz pasado como argumento, todo se supone que es un puntero y se trata como tal.

Se podría decir que las matrices realmente no son compatibles con C (esto no es cierto en realidad, como decía antes, pero es una buena aproximación); una matriz realmente se trata como un puntero a un bloque de datos y se accede usando la aritmética del puntero. Como C no tiene ninguna forma de RTTI, debe declarar el tamaño del elemento de la matriz en el prototipo de la función (para admitir la aritmética del puntero). Esto es incluso “más cierto” para matrices multidimensionales.

De todos modos, todo lo anterior ya no es verdad: p

La mayoría de los comstackdores C / C ++ modernos admiten la comprobación de límites, pero los estándares requieren que esté desactivada de manera predeterminada (para compatibilidad con versiones anteriores). Las versiones razonablemente recientes de gcc, por ejemplo, hacen la comprobación del rango de tiempo de comstackción con “-O3 -Wall -Wextra” y la comprobación de límites de tiempo de ejecución completa con “-fbounds-checking”.

C no solo transformará un parámetro de tipo int[5] en *int ; dada la statement typedef int intArray5[5]; , transformará un parámetro de tipo intArray5 a *int también. Hay algunas situaciones en las que este comportamiento, aunque impar, es útil (especialmente con cosas como va_list definida en stdargs.h , que algunas implementaciones definen como una matriz). Sería ilógico permitir como parámetro un tipo definido como int[5] (ignorando la dimensión) pero no permitiendo que int[5] se especifique directamente.

Considero que el manejo por parte de C de los parámetros del tipo de matriz es absurdo, pero es una consecuencia de los esfuerzos por tomar un lenguaje ad-hoc, que en gran parte no estaban bien definidos o bien pensados, y tratamos de idear comportamientos especificaciones que son consistentes con lo que las implementaciones existentes hicieron para los progtwigs existentes. Muchos de los caprichos de C tienen sentido cuando se los considera bajo esa luz, particularmente si se tiene en cuenta que, cuando se inventaron muchos de ellos, todavía no existían grandes partes del lenguaje que conocemos hoy en día. Por lo que entiendo, en el predecesor de C, llamado BCPL, los comstackdores realmente no hicieron un seguimiento muy bien de los tipos de variables. Una statement int arr[5]; fue equivalente a int anonymousAllocation[5],*arr = anonymousAllocation; ; una vez que la asignación fue apartada. el comstackdor no sabía ni le importaba si arr era un puntero o una matriz. Cuando se accede como arr[x] o *arr , se considera como un puntero independientemente de cómo se haya declarado.

Una cosa que no ha sido respondida todavía es la pregunta real.

Las respuestas ya dadas explican que las matrices no se pueden pasar por valor a una función en C o C ++. También explican que un parámetro declarado como int[] se trata como si tuviera tipo int * , y que una variable de tipo int[] se puede pasar a dicha función.

Pero no explican por qué nunca se ha cometido un error al proporcionar explícitamente una longitud de matriz.

 void f(int *); // makes perfect sense void f(int []); // sort of makes sense void f(int [10]); // makes no sense 

¿Por qué no es el último de estos un error?

Una razón para eso es que causa problemas con typedefs.

 typedef int myarray[10]; void f(myarray array); 

Si fuera un error especificar la longitud de la matriz en los parámetros de la función, no podría usar el nombre de la myarray en el parámetro de la función. Y dado que algunas implementaciones usan tipos de matriz para tipos de biblioteca estándar como va_list , y todas las implementaciones son necesarias para hacer de jmp_buf un tipo de matriz, sería muy problemático si no existiera una forma estándar de declarar parámetros de función usando esos nombres: sin esa habilidad, no podría haber una implementación portátil de funciones como vprintf .