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


119

Esta pregunta va dirigida a los gurús de C:

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

char (* p)[10];

.. que básicamente establece 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 compilación si intenta asignar un puntero de una matriz de diferente tamaño ap. También le dará un error de tiempo de compilación si intenta asignar el valor de un puntero char simple ap. Intenté esto con gcc y parece funcionar con ANSI, C89 y C99.

Me parece que declarar un puntero como este sería muy útil, especialmente cuando se pasa un puntero a una función. Por lo general, la gente escribiría el prototipo de una función como esta:

void foo(char * p, int plen);

Si esperaba un búfer de un tamaño específico, simplemente probaría el valor de plen. Sin embargo, no se puede garantizar que la persona que le pasa p realmente le dará ubicaciones de memoria válidas en ese búfer. Debe 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 como este en ningún código con el que me haya encontrado.

Mi pregunta es: ¿Hay alguna razón por la que la gente no declare punteros como este? ¿No estoy viendo alguna trampa obvia?


3
nota: Desde C99, la matriz no tiene que ser de tamaño fijo como sugiere el título, 10puede ser reemplazada por cualquier variable en el alcance
MM

Respuestas:


173

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

Cuando las características específicas de su área de aplicación requieren una matriz de tamaño fijo específico (el tamaño de la matriz es una constante en tiempo de compilación), la única forma correcta 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 permitirá la verificación de tipos a nivel de idioma, lo que garantizará que la matriz del tamaño exactamente correcto se proporcione como argumento. De hecho, en muchos casos la gente usa 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 invariante con relación a que el Vector3dtipo sea una matriz o un struct. Puede cambiar la definición de Vector3den cualquier momento de una matriz a ay structvolver, y no tendrá que cambiar la declaración 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á que este método de transferencia de matrices se use explícitamente con demasiada frecuencia, simplemente porque demasiadas personas se confunden con una sintaxis bastante complicada y simplemente no se sienten 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 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 la matriz es una técnica muy especializada, 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, tamaño en tiempo de ejecución) . Si realmente necesita poder procesar matrices de tamaño en 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 de 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 adecuada de tamaño fijo.

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

void foo(char p[])

es un error importante a nivel de técnica, que lamentablemente 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 obstaculizar la adopción de la técnica de paso de arreglos de tamaño fijo es el predominio del enfoque ingenuo para la tipificación de arreglos asignados dinámicamente. Por ejemplo, si el programa requiere arreglos fijos de tipo char[10](como en su ejemplo), un desarrollador promedio usará mallocarreglos 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 les hace abandonar la declaración de parámetros de tamaño fijo sin pensarlo más. Sin embargo, en realidad, la raíz del problema radica en el mallocenfoque ingenuo . El mallocformato que se muestra arriba debe reservarse para matrices de tamaño en tiempo de ejecución. Si el tipo de matriz tiene un tamaño en tiempo de compilación, una mejor manera de mallochacerlo sería la siguiente

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

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

foo(p);

y el compilador realizará la verificación de tipo adecuada. 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 "típico" promedio de todos los días.


2
La respuesta ofrece una descripción muy concisa e informativa de cómo sizeof () tiene éxito, con qué frecuencia falla y las formas en que siempre falla. sus observaciones de que la mayoría de los ingenieros de C / C ++ no entienden y, por lo tanto, hacen algo que creen que entienden es una de las cosas más proféticas que he visto en un tiempo, y el velo no es nada comparado con la precisión que describe. en serio, señor. gran respuesta.
WhozCraig

Acabo de refactorizar un código basado en esta respuesta, bravo y gracias tanto por la Q como por la A.
Perry

1
Tengo curiosidad por saber cómo maneja la constpropiedad con esta técnica. Un const char (*p)[N]argumento no parece compatible con un puntero a char table[N];Por el contrario, un char*ptr simple sigue siendo compatible con un const char*argumento.
Cyan

4
Puede ser útil tener en cuenta que para acceder a un elemento de su matriz, debe hacerlo (*p)[i]y no *p[i]. Este último saltará por el tamaño de la matriz, que es casi seguro que no es lo que desea. Al menos para mí, aprender esta sintaxis provocó, en lugar de prevenir, un error; Habría obtenido el código correcto más rápido simplemente pasando un flotador *.
Andrew Wagner

1
Sí @mickey, lo que ha sugerido es un constpuntero a una matriz de elementos mutables. Y sí, esto es completamente diferente de un puntero a una matriz de elementos inmutables.
Cyan

11

Me gustaría agregar a la respuesta de AndreyT (en caso de que alguien se tope 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 gran desventaja asociada con ellas en C (aparentemente no en C ++). Es bastante común tener una situación en la que le gustaría darle a la persona que llama un puntero constante a un búfer 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 sean mucho más útiles. Pero en el caso de C, es necesario recurrir a una declaración de puntero regular siempre que desee un puntero constante al búfer de tamaño fijo (a menos que el búfer en sí se haya declarado constante para empezar). Puede encontrar más información en este hilo de correo: texto del enlace

En mi opinión, esta es una restricción severa y podría ser una de las principales razones por las que las personas no suelen declarar punteros como este en C.La otra es el hecho de que la mayoría de las personas ni siquiera saben que se puede declarar un puntero como este como AndreyT ha señalado.


2
Eso parece ser un problema específico del compilador. Pude duplicar usando gcc 4.9.1, pero clang 3.4.2 fue capaz de pasar de una versión no constante a una constante sin problema. Leí la especificación C11 (p. 9 en mi versión ... la parte que habla de que dos tipos calificados son compatibles) y estoy de acuerdo en que parece decir que estas conversiones son ilegales. Sin embargo, sabemos en la práctica que siempre puede convertir automáticamente de char * a char const * sin previo aviso. En mi opinión, clang es más consistente en permitir esto que gcc, aunque estoy de acuerdo con usted en que la especificación parece prohibir cualquiera de estas conversiones automáticas.
Doug Richardson

4

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

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

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

Consulte también esta pregunta , el uso foo(&p)debería funcionar.


3
Por supuesto, foo (p) no funcionará, foo está solicitando un puntero a una matriz de 10 elementos, por lo que debe pasar la dirección de su matriz ...
Brian R. Bondy

9
¿Cómo es esa la "razón obvia"? Obviamente, se entiende que la forma correcta de llamar a la función es foo(&p).
el AnT

3
Supongo que "obvio" es la palabra incorrecta. Quise decir "más sencillo". La distinción entre py & p en este caso es bastante oscura para el programador de C promedio. Alguien que intente hacer lo que sugirió el cartel escribirá lo que escribí, obtendrá un error en tiempo de compilación y se rendirá.
Keith Randall

2

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

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

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

  • Acceder a la matriz requiere usar (*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 frustraría parcialmente el propósito de usar el tipo correcto.

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

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

    El operador address-of obtiene la dirección de toda la matriz &a, con el tipo correcto para asignarla p. Sin el operador, ase convierte automáticamente a 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 me sorprende que &sea ​​necesario. Es consistente con el uso de &on variables de otros tipos, pero debo recordar que una matriz es especial y que necesito &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 &todavía no permitía usar el operador en arreglos completos (aunque algunos compiladores lo ignoraron o toleraron la sintaxis). Lo cual, por cierto, puede ser otra razón por la que los apuntadores a matrices tienen dificultades 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 typedefse no se utiliza para crear un tipo de la matriz de punteros-a-(en un archivo de cabecera común), entonces una matriz de punteros-a-global necesita una más complicada externdeclaración compartirlo a través de archivos:

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

1

Bueno, en pocas palabras, C no hace las cosas de esa manera. Una matriz de tipo Tse pasa como un puntero al primero Tde la matriz, y eso es todo lo que obtiene.

Esto permite algunos algoritmos geniales y elegantes, como recorrer la matriz con expresiones como

*dst++ = *src++

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

Lo que se acerca a lo que pregunta en C es pasar un 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 entrega la referencia como el código que lo usa están de acuerdo con el tamaño de los datos que se manejan.

Su estructura puede contener los datos que desee; podría contener su matriz de un tamaño bien definido.

Aún así, nada impide que usted o un codificador incompetente o malévolo utilicen conversiones para engañar al compilador y que trate su estructura como una de diferente tamaño. La capacidad casi ilimitada de hacer este tipo de cosas es parte del diseño de C.


1

Puede declarar una matriz de caracteres de varias formas:

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 sintaxis de matriz:

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

2
No olvide que una matriz de tamaño fijo también se puede asignar dinámicamente como char (*p)[10] = malloc(sizeof *p).
el AnT

Vea aquí para una discusión más detallada entre las diferencias de char array [] y char * ptr aquí. stackoverflow.com/questions/1807530/…
t0mm13b

1

No recomendaría esta solución

typedef int Vector3d[3];

ya que oculta el hecho de que Vector3D tiene un tipo que debe conocer. Los programadores 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


No estaba sugiriendo esto como una solución. Simplemente estaba usando esto como ejemplo.
figurassa

Hm. ¿Por qué sizeof(a)no es lo mismo que sizeof(b)?
sherrellbc

0

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

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


4
Las matrices NO son punteros constantes. Lea algunas preguntas frecuentes sobre matrices, por favor.
el AnT

2
Para lo que importa aquí (matrices unidimensionales como parámetros), el hecho es que decaen a punteros constantes. Lea las preguntas frecuentes sobre cómo ser menos pedante, por favor.
fortran

-2

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


1
-1 Incorrecto. Funciona bien en vs2008, vs2010, gcc. En particular, este ejemplo funciona bien: stackoverflow.com/a/19208364/2333290
kotlomoy
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.