Punteros C: apuntando a una matriz de tamaño fijo

Esta pregunta va para los gurús de C que están por ahí:

En C, es posible declarar un puntero de la siguiente manera:

char (* p)[10]; 

… que básicamente indica que este puntero apunta a una matriz de 10 caracteres. Lo bueno de declarar un puntero como este es que obtendrá un error de tiempo de comstackción si intenta asignar un puntero de una matriz de diferente tamaño a p. También le dará un error de tiempo de comstackción si intenta asignar el valor de un simple puntero a p. Intenté esto con gcc y parece funcionar con ANSI, C89 y C99.

Me parece que declarar un puntero como este sería muy útil, particularmente cuando se pasa un puntero a una función. Por lo general, las personas escriben el prototipo de una función como esta:

 void foo(char * p, int plen); 

Si esperaba un buffer de un tamaño específico, simplemente probaría el valor de plen. Sin embargo, no se puede garantizar que la persona que le transfiere p realmente le proporcione ubicaciones plenas de memoria válidas en ese búfer. Debes confiar en que la persona que llamó a esta función está haciendo lo correcto. Por otra parte:

 void foo(char (*p)[10]); 

… obligaría a la persona que llama a darle un búfer del tamaño especificado.

Esto parece muy útil, pero nunca he visto un puntero declarado así en ningún código que haya encontrado.

Mi pregunta es: ¿hay alguna razón por la cual la gente no declara indicadores como este? ¿No estoy viendo una trampa obvia?

Lo que dices en tu publicación es absolutamente correcto. Yo diría que cada desarrollador C llega exactamente al mismo descubrimiento y exactamente a la misma conclusión cuando (si) alcanzan cierto nivel de competencia con el lenguaje C.

Cuando los detalles de su área de aplicación requieren una matriz de tamaño fijo específico (el tamaño de matriz es una constante de tiempo de comstackción), la única forma adecuada de pasar dicha matriz a una función es mediante el uso de un parámetro de puntero a matriz

 void foo(char (*p)[10]); 

(en lenguaje C ++ esto también se hace con referencias

 void foo(char (&p)[10]); 

)

Esto habilitará la verificación de tipo de nivel de idioma, lo que asegurará que la matriz de tamaño exactamente correcto se suministre como argumento. De hecho, en muchos casos las personas usan esta técnica implícitamente, sin siquiera darse cuenta, ocultando el tipo de matriz detrás de un nombre typedef

 typedef int Vector3d[3]; void transform(Vector3d *vector); /* equivalent to `void transform(int (*vector)[3])` */ ... Vector3d vec; ... transform(&vec); 

Tenga en cuenta, además, que el código anterior es invariable en relación con el tipo Vector3d es una matriz o una struct . Puede cambiar la definición de Vector3d en cualquier momento desde una matriz a una struct y volver, y no tendrá que cambiar la statement de la función. En cualquier caso, las funciones recibirán un objeto agregado “por referencia” (hay excepciones a esto, pero dentro del contexto de esta discusión esto es cierto).

Sin embargo, no verá este método de paso de matriz utilizado explícitamente con demasiada frecuencia, simplemente porque demasiadas personas se confunden por una syntax complicada y simplemente no son lo suficientemente cómodas con tales características del lenguaje C para usarlas correctamente. Por esta razón, en la vida real promedio, pasar una matriz como un puntero a su primer elemento es un enfoque más popular. Simplemente parece “más simple”.

Pero en realidad, usar el puntero al primer elemento para pasar los arreglos es una técnica muy específica, un truco, que tiene un propósito muy específico: su único propósito es facilitar el paso de matrices de diferentes tamaños (es decir, el tamaño del tiempo de ejecución) . Si realmente necesita poder procesar matrices de tamaño de tiempo de ejecución, entonces la forma correcta de pasar dicha matriz es mediante un puntero a su primer elemento con el tamaño concreto proporcionado por un parámetro adicional

 void foo(char p[], unsigned plen); 

De hecho, en muchos casos es muy útil poder procesar matrices de tamaño de tiempo de ejecución, lo que también contribuye a la popularidad del método. Muchos desarrolladores C simplemente nunca encuentran (o nunca reconocen) la necesidad de procesar una matriz de tamaño fijo, por lo que permanecen ajenos a la técnica de tamaño fijo adecuada.

Sin embargo, si el tamaño de la matriz es fijo, pasándolo como un puntero a un elemento

 void foo(char p[]) 

es un error de nivel técnico importante, que desafortunadamente está bastante extendido en estos días. Una técnica de puntero a matriz es un enfoque mucho mejor en tales casos.

Otra razón que podría dificultar la adopción de la técnica de paso de matriz de tamaño fijo es el dominio del enfoque ingenuo para el tipado de matrices asignadas dinámicamente. Por ejemplo, si el progtwig llama a matrices fijas de tipo char[10] (como en su ejemplo), un desarrollador promedio malloc tales matrices como

 char *p = malloc(10 * sizeof *p); 

Esta matriz no se puede pasar a una función declarada como

 void foo(char (*p)[10]); 

lo que confunde al desarrollador promedio y los hace abandonar la statement de parámetros de tamaño fijo sin darle más vueltas. Sin embargo, en realidad, la raíz del problema radica en el enfoque ingenuo malloc . El formato malloc que se muestra arriba se debe reservar para matrices de tamaño de tiempo de ejecución. Si el tipo de matriz tiene un tamaño de tiempo de comstackción, una mejor forma de malloc sería el siguiente

 char (*p)[10] = malloc(sizeof *p); 

Esto, por supuesto, se puede pasar fácilmente al foo declarado anteriormente

 foo(p); 

y el comstackdor realizará la verificación de tipos correcta. Pero, de nuevo, esto es demasiado confuso para un desarrollador de C no preparado, por lo que no lo verá con demasiada frecuencia en el código diario promedio “típico”.

Me gustaría agregar a la respuesta de AndreyT (en caso de que alguien se tropiece con esta página buscando más información sobre este tema):

A medida que empiezo a jugar más con estas declaraciones, me doy cuenta de que hay una desventaja importante asociada con ellas en C (aparentemente no en C ++). Es bastante común tener una situación en la que le gustaría dar a un llamante un puntero de const a un buffer en el que ha escrito. Desafortunadamente, esto no es posible cuando se declara un puntero como este en C. En otras palabras, el estándar C (6.7.3 – Párrafo 8) está en desacuerdo con algo como esto:

 int array[9]; const int (* p2)[9] = &array; /* Not legal unless array is const as well */ 

Esta restricción no parece estar presente en C ++, lo que hace que este tipo de declaraciones sea mucho más útil. Pero en el caso de C, es necesario recurrir a una statement de puntero regular siempre que desee un puntero de const para el buffer de tamaño fijo (a menos que el buffer en sí mismo haya sido declarado const para comenzar). Puede encontrar más información en este hilo de correo: texto del enlace

Esta es una restricción severa en mi opinión y podría ser una de las razones principales por las que las personas generalmente no declaran punteros como este en C. El otro es el hecho de que la mayoría de la gente ni siquiera sabe que puede declarar un puntero como este como AndreyT ha señalado.

La razón obvia es que este código no se comstack:

 extern void foo(char (*p)[10]); void bar() { char p[10]; foo(p); } 

La promoción predeterminada de una matriz es un puntero no calificado.

También vea esta pregunta , el uso de foo(&p) debería funcionar.

Bueno, en pocas palabras, C no hace las cosas de esa manera. Una matriz de tipo T se pasa como un puntero a la primera T en la matriz, y eso es todo lo que obtienes.

Esto permite algunos algoritmos interesantes y elegantes, como el bucle de la matriz con expresiones como

 *dst++ = *src++ 

La desventaja es que la gestión del tamaño depende de usted. Desafortunadamente, el hecho de no hacer esto concienzudamente también ha llevado a millones de errores en la encoding C, y / o oportunidades de explotación malévola.

Lo que se acerca a lo que pides en C es pasar una struct (por valor) o un puntero a uno (por referencia). Siempre que se use el mismo tipo de estructura en ambos lados de esta operación, tanto el código que distribuye la referencia como el código que lo utiliza están de acuerdo con el tamaño de los datos que se manejan.

Su estructura puede contener cualquier información que desee; podría contener su matriz de un tamaño bien definido.

Sin embargo, nada impide que usted o un codificador incompetente o malévolo use moldes para engañar al comstackdor para que trate su estructura como de otro tamaño. La capacidad casi no encadenada para hacer este tipo de cosas es parte del diseño de C.

Puede declarar una matriz de caracteres de varias maneras:

 char p[10]; char* p = (char*)malloc(10 * sizeof(char)); 

El prototipo de una función que toma una matriz por valor es:

 void foo(char* p); //cannot modify p 

o por referencia:

 void foo(char** p); //can modify p, derefernce by *p[0] = 'f'; 

o por syntax de matriz:

 void foo(char p[]); //same as char* 

No recomendaría esta solución

 typedef int Vector3d[3]; 

ya que oscurece el hecho de que Vector3D tiene un tipo que debes conocer. Los progtwigdores generalmente no esperan que las variables del mismo tipo tengan diferentes tamaños. Considerar :

 void foo(Vector3d a) { Vector3D b; } 

donde sizeof a! = sizeof b

También quiero usar esta syntax para habilitar más verificación de tipos.

Pero también estoy de acuerdo en que la syntax y el modelo mental del uso de punteros es más simple y fácil de recordar.

Aquí hay algunos obstáculos más que he encontrado.

  • El acceso a la matriz requiere el uso de (*p)[] :

     void foo(char (*p)[10]) { char c = (*p)[3]; (*p)[0] = 1; } 

    Es tentador usar un puntero-a-char local en su lugar:

     void foo(char (*p)[10]) { char *cp = (char *)p; char c = cp[3]; cp[0] = 1; } 

    Pero esto vencería parcialmente el propósito de usar el tipo correcto.

  • Uno debe recordar usar el operador de dirección al asignar una dirección de matriz a un puntero a matriz:

     char a[10]; char (*p)[10] = &a; 

    El operador de dirección obtiene la dirección de toda la matriz en &a , con el tipo correcto para asignarla a p . Sin el operador, a se convierte automáticamente en la dirección del primer elemento de la matriz, igual que en &a[0] , que tiene un tipo diferente.

    Dado que esta conversión automática ya se está llevando a cabo, siempre estoy desconcertado de que el & es necesario. Es consistente con el uso de & en variables de otros tipos, pero debo recordar que una matriz es especial y que necesito & para obtener el tipo correcto de dirección, aunque el valor de la dirección sea el mismo.

    Una razón de mi problema puede ser que aprendí K & R C en los años 80, lo que no permitía usar todavía el operador & en arreglos completos (aunque algunos comstackdores ignoraron eso o toleraron la syntax). Que, por cierto, puede ser otra razón por la que los punteros a las matrices tienen un tiempo difícil para ser adoptados: solo funcionan correctamente desde ANSI C, y la limitación del operador puede haber sido otra razón para considerarlos demasiado incómodos.

  • Cuando typedef no se utiliza para crear un tipo de puntero a matriz (en un archivo de encabezado común), un puntero a matriz global necesita una statement extern más complicada para compartirla en los archivos:

     fileA: char (*p)[10]; fileB: extern char (*p)[10]; 

Tal vez me esté perdiendo algo, pero … dado que las matrices son punteros constantes, básicamente eso significa que no tiene sentido pasarles los punteros.

¿No podría simplemente usar void foo(char p[10], int plen); ?

En mi comstackdor (vs2008) trata a char (*p)[10] como una matriz de punteros de caracteres, como si no hubiera paréntesis, incluso si compilo como un archivo C. ¿El comstackdor admite esta “variable”? Si es así, esa es una razón importante para no usarlo.