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

131

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

#include<iostream>
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]), [1]parece no hacer nada y no funciona en
absoluto, porque puedo usar a[2]. Justo como 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 se trata de esta parte.

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

Fanl
fuente
66
Esto se debe a que en realidad no puede pasar matrices a funciones.
Ed S.
37
Creo que la pregunta aquí era por qué C le permite declarar que un parámetro es de tipo matriz cuando simplemente se comportará exactamente como un puntero de todos modos.
Brian
8
@Brian: No estoy seguro de si este es un argumento a favor o en contra del comportamiento, pero también se aplica si el tipo de argumento es un typedeftipo de matriz. Por lo que la "decadencia de puntero" en los tipos de argumentos no es sólo azúcar sintáctico reemplazar []con *, que realmente va a través del sistema de tipos. Esto tiene consecuencias en el mundo real para algunos tipos estándar como los va_listque se pueden definir con tipo de matriz o sin matriz.
R .. GitHub DEJA DE AYUDAR A ICE
44
@songyuanyao Puede lograr algo no del todo diferente en C (y C ++) usando un puntero: int dis(char (*a)[1]). A continuación, se pasa un puntero a una matriz: dis(&b). Si está dispuesto a usar características de C que no existen en C ++, también puede decir cosas como void foo(int data[static 256])y int bar(double matrix[*][*]), pero esa es una lata completamente diferente de gusanos.
Stuart Olsen
1
@StuartOlsen El punto no es qué estándar definió qué. El punto es por qué quien lo definió definió de esa manera.
user253751

Respuestas:

156

Es una peculiaridad de la sintaxis para pasar matrices a funciones.

En realidad, no es posible pasar una matriz en C. Si escribe una sintaxis 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, los contenidos de su []en la lista de parámetros formales de la función se ignoran realmente.

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

MM
fuente
21
Como programador que no es C, encuentro esta respuesta muy accesible. +1
asteri
21
+1 para "La decisión de permitir esta sintaxis se tomó en la década de 1970 y ha causado mucha confusión desde entonces ..."
NoSenseEtAl
8
Esto es cierto, pero también es posible pasar una matriz de ese tamaño utilizando la void foo(int (*somearray)[20])sintaxis. en este caso, 20 se aplica en los sitios de la persona que llama.
v.oddou
14
-1 Como programador en C, encuentro esta respuesta incorrecta. []no se ignoran en matrices multidimensionales como se muestra en la respuesta de pat. Por lo tanto, era necesario incluir la sintaxis de la matriz. Además, nada impide que el compilador emita advertencias incluso en matrices unidimensionales.
user694733
77
Por "el contenido de su []", estoy hablando específicamente sobre el código en la Pregunta. Esta peculiaridad de sintaxis no era necesaria en absoluto, lo mismo se puede lograr utilizando la sintaxis de puntero, es decir, si se pasa un puntero, entonces se requiere que el parámetro sea un declarador de puntero. Por ejemplo, en el ejemplo de Pat, void foo(int (*args)[20]);también, estrictamente hablando, C no tiene matrices multidimensionales; pero tiene matrices cuyos elementos pueden ser otras matrices. Esto no cambia nada.
MM
143

Se ignora la longitud de la primera dimensión, pero la longitud de las dimensiones adicionales son necesarias para permitir que el compilador calcule las compensaciones correctamente. En el siguiente ejemplo, la foofunción pasa un puntero a una matriz bidimensional.

#include <stdio.h>

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 compilador no le impedirá indexar al final (tenga en cuenta que el formal quiere 10 elementos, pero el real solo proporciona 2). Sin embargo, el tamaño de la segunda dimensión [20]se usa para determinar el paso de cada fila, y aquí, el formal debe coincidir con el real. Nuevamente, el compilador 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]está determinado por:

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

Tenga en cuenta que si col >= 20, en realidad se indexará en una fila posterior (o fuera del final de toda la matriz).

sizeof(args[0]), vuelve 80a mi máquina donde sizeof(int) == 4. Sin embargo, si intento tomar sizeof(args), me sale la siguiente advertencia del compilador:

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 compilador advierte que solo dará el tamaño del puntero en el que la matriz se ha descompuesto en lugar del tamaño de la matriz misma.

palmadita
fuente
Muy útil: la coherencia con esto también es plausible como la razón de la peculiaridad en el caso 1-d.
jwg
1
Es la misma idea que el caso 1-D. Lo que parece una matriz 2-D en C y C ++ es en realidad una matriz 1-D, cada elemento del cual es otra matriz 1-D. En este caso tenemos una matriz con 10 elementos, cada uno de los cuales es una "matriz de 20 pulgadas". Como se describe en mi publicación, lo que realmente se pasa a la función es el puntero al primer elemento de args. En este caso, el primer elemento de args es una "matriz de 20 pulgadas". Los punteros incluyen información de tipo; lo que se pasa es "puntero a una matriz de 20 pulgadas".
MM
9
Sí, ese es el int (*)[20]tipo; "puntero a una matriz de 20 ints".
Pat
33

El problema y cómo superarlo en C ++

El problema ha sido explicado ampliamente por Pat y Matt . El compilador básicamente ignora la primera dimensión del tamaño de la matriz, ignorando efectivamente el tamaño del argumento pasado.

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

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

Referencias

Si su función solo intenta leer o modificar una matriz existente (sin copiarla), puede usar referencias fácilmente.

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

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

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

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

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

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

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

Al aplicar el constcalificador estamos evitando posibles modificaciones .


La clase de biblioteca estándar para matrices.

Si considera que la sintaxis anterior es fea e innecesaria, como lo hago yo, podemos lanzarla a la lata y usarla std::arrayen su lugar (desde C ++ 11).

Aquí está el código refactorizado:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

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

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

No solo eso, sino que obtienes copias y movimientos semánticos gratis. :)

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

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

Zapato
fuente
2
@kietz, lamento que se haya rechazado su edición sugerida, pero asumimos automáticamente que se está utilizando C ++ 11 , a menos que se especifique lo contrario.
Zapato
esto es cierto, pero también se supone que debemos especificar si alguna solución es solo C ++ 11, según el enlace que proporcionó.
Trly
Por cierto, estoy de acuerdo. He editado la respuesta en consecuencia. Gracias por mencionarlo.
Zapato
9

Es una característica divertida de C que te permite dispararte efectivamente en el pie si estás tan inclinado.

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

Además, asignar un tamaño al argumento de la función tiene la ventaja de que cuando otro programador utiliza la función, existe la posibilidad de que noten una restricción de tamaño. Solo usar un puntero no transmite esa información al siguiente programador.

cuenta
fuente
3
Si. C está diseñado para confiar en el programador sobre el compilador. Si está indexando tan descaradamente el final de una matriz, debe estar haciendo algo especial e intencional.
John
77
Me corté los dientes en programación en C hace 14 años. De todo lo que dijo mi profesor, la frase que más me ha quedado grabada a mí es "C fue escrita por programadores, para programadores". El lenguaje es extremadamente poderoso. (Prepárese para el cliché) Como el tío Ben nos enseñó: "Con gran poder, viene una gran responsabilidad".
Andrew Falanga
6

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

En segundo lugar, hay un truco que hace posible pasar una matriz por valor a una función. También es posible devolver por valor una matriz de 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 los elementos de esta manera: foo.a [1]. El ".a" adicional puede parecer extraño, pero este truco agrega una gran funcionalidad al lenguaje C.

usuario34814
fuente
77
Está confundiendo la verificación de límites de tiempo de ejecución con la verificación de tipo en tiempo de compilación.
Ben Voigt
@Ben Voigt: solo estoy hablando de la verificación de límites, como es la pregunta original.
user34814
2
@ user34814 la verificación de límites en tiempo de compilación está dentro del alcance de la verificación de tipos. Varios idiomas de alto nivel ofrecen esta característica.
Leushenko
5

Para decirle al compilador que myArray apunta a una matriz de al menos 10 ints:

void bar(int myArray[static 10])

Un buen compilador debería avisarle si accede a myArray [10]. Sin la palabra clave "estática", el 10 no significaría nada en absoluto.

gnasher729
fuente
1
¿Por qué debería advertir un compilador si accede al elemento 11 y la matriz contiene al menos 10 elementos?
nwellnhof
Presumiblemente esto se debe a que el compilador solo puede exigir que tenga al menos 10 elementos. Si intenta acceder al undécimo elemento, no puede estar seguro de que exista (aunque pueda).
Dylan Watson
2
No creo que sea una lectura correcta del estándar. [static]permite que el compilador advierta si llama bar con un int[5]. No dicta lo que puede acceder dentro bar . La responsabilidad es totalmente del lado de la persona que llama.
tab
3
error: expected primary-expression before 'static'Nunca he visto esta sintaxis. es poco probable que sea C o C ++ estándar.
v.oddou
3
@ v.oddou, se especifica en C99, en 6.7.5.2 y 6.7.5.3.
Samuel Edwin Ward
5

Esta es una "característica" bien conocida de C, que se pasa a C ++ porque se supone que C ++ compila 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. Se supone que C 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 inserte código "oculto"; la comprobación del rango de tiempo de ejecución está "prohibida".
  3. El código de máquina generado para acceder a una matriz estática o dinámica (ya sea en la pila o asignado) es realmente diferente.
  4. Dado que la función llamada no puede conocer el "tipo" de matriz que se pasa como argumento, se supone que todo es un puntero y se trata como tal.

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

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

La mayoría de los compiladores modernos C / C ++ hacen los límites de soporte de cheques, pero las normas exigen que sea desactivada por defecto (por compatibilidad con versiones anteriores). Las versiones razonablemente recientes de gcc, por ejemplo, realizan la comprobación del rango de tiempo de compilación con "-O3 -Wall -Wextra" y la comprobación de límites de tiempo de ejecución completo con "-fbounds-Check".

ZioByte
fuente
Tal vez se suponía que C ++ compilaría el código C hace 20 años, pero ciertamente no lo es , y no lo ha hecho durante mucho tiempo (C ++ 98? C99 al menos, que no ha sido "reparado" por ningún estándar más nuevo de C ++).
hyde
@hyde Eso me suena demasiado duro. Para citar a Stroustrup "Con pequeñas excepciones, C es un subconjunto de C ++". (The C ++ PL 4th ed., Sec. 1.2.1). Si bien tanto C ++ como C evolucionan aún más, y existen características de la última versión de C que no están en la última versión de C ++, en general creo que la cita de Stroustrup sigue siendo válida.
mvw
@mvw La mayoría del código C escrito en este milenio, que no se mantiene intencionalmente compatible con C ++ al evitar características incompatibles, utilizará la sintaxis de inicializadores designados C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) para inicializar estructuras, porque es una forma mucho más clara de inicializar una estructura. Como resultado, la mayoría del código C actual será rechazado por los compiladores estándar de C ++, porque la mayoría del código C inicializará estructuras.
hyde
@mvw Quizás podría decirse que se supone que C ++ es compatible con C, de modo que es posible escribir código que se compilará con los compiladores de C y C ++, si se hacen ciertos compromisos. Pero eso requiere el uso de un subconjunto de los dos C y C ++, no sólo subconjunto de C ++.
hyde
@hyde Te sorprendería la cantidad de código C que es compatible con C ++. Hace unos años, todo el kernel de Linux era compatible con C ++ (no sé si aún es cierto). Rutinariamente compilo el código C en el compilador de C ++ para obtener una comprobación de advertencia superior, solo la "producción" se compila en modo C para obtener la mayor optimización.
ZioByte
3

C no solo transformará un parámetro de tipo int[5]en *int; dada la declaracióntypedef int intArray5[5]; , se va a transformar un parámetro de tipo intArray5a *inttambién. Hay algunas situaciones en las que este comportamiento, aunque extraño, es útil (especialmente con cosas como las va_listdefinidas 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 permitir int[5]que se especifique directamente.

Encuentro que el manejo de C de los parámetros de tipo de matriz es absurdo, pero es una consecuencia de los esfuerzos por adoptar un lenguaje ad-hoc, gran parte del cual no estaba particularmente bien definido o pensado, y tratar de llegar a un comportamiento. especificaciones que son consistentes con lo que hicieron las implementaciones existentes para los programas existentes. Muchas de las peculiaridades de C tienen sentido cuando se miran desde esa perspectiva, particularmente si se considera que cuando se inventaron muchas de ellas, grandes partes del lenguaje que conocemos hoy todavía no existían. Por lo que entiendo, en el predecesor de C, llamado BCPL, los compiladores realmente no registraban muy bien los tipos de variables. Una declaración int arr[5];era equivalente a int anonymousAllocation[5],*arr = anonymousAllocation;; una vez que se asignó la asignación. el compilador no sabía ni le importaba siarrera un puntero o una matriz. Cuando se accede como cualquieraarr[x] o*arr, se consideraría como un puntero independientemente de cómo se declarara.

Super gato
fuente
1

Una cosa que aún no ha sido respondida 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 tipoint[] 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é el último de estos no es 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 myarraynombre 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 se requieren todas las implementaciones para hacer jmp_bufun tipo de matriz, sería muy problemático si no hubiera una forma estándar de declarar parámetros de función usando esos nombres: sin esa capacidad, podría haber No ser una implementación portátil de funciones como vprintf.


fuente
0

Está permitido que los compiladores puedan verificar si el tamaño de la matriz aprobada es el mismo que el esperado. Los compiladores pueden advertir un problema si no es el caso.

hamidi
fuente