Punteros triples en C: ¿es una cuestión de estilo?

13 minutos de lectura

avatar de usuario
Darda

Siento que los punteros triples en C se consideran “malos”. Para mí, tiene sentido usarlos de vez en cuando.

Partiendo de lo básico, el puntero único tiene dos propósitos: crear una matriz y permitir que una función cambie su contenido (pasar por referencia):

char *a;
a = malloc...

o

void foo (char *c); //means I'm going to modify the parameter in foo.
{ *c="f"; }

char a;
foo(&a);

los puntero doble puede ser una matriz 2D (o una matriz de matrices, ya que no es necesario que cada “columna” o “fila” tenga la misma longitud). Personalmente, me gusta usarlo cuando necesito pasar una matriz 1D:

void foo (char **c); //means I'm going to modify the elements of an array in foo.
{ (*c)[0] = 'f'; }

char *a;
a = malloc...
foo(&a);

Para mí, eso ayuda a describir lo que está haciendo foo. Sin embargo, no es necesario:

void foo (char *c); //am I modifying a char or just passing a char array?
{ c[0] = 'f'; }

char *a;
a = malloc...
foo(a);

también funcionará.

Según la primera respuesta a esta pregunta, si foo fuera a modificar el tamaño de la matriz, se requeriría un puntero doble.

Uno puede ver claramente cómo un puntero triple (y más allá, en realidad) sería necesario. En mi caso, si estuviera pasando una matriz de punteros (o una matriz de matrices), lo usaría. Evidentemente, sería necesario si está pasando a una función que está cambiando el tamaño de la matriz multidimensional. Ciertamente, una matriz de matrices de matrices no es demasiado común, pero los otros casos sí lo son.

Entonces, ¿cuáles son algunas de las convenciones que existen? ¿Es esto realmente solo una cuestión de estilo/legibilidad combinada con el hecho de que muchas personas tienen dificultades para entender los punteros?

  • ¿Es tan poco común crear un puntero doble a un tipo opaco (que podría terminar siendo un puntero en sí mismo)? es decir typedef struct foo *foo_t; luego más tarde foo_t **bar

    – asveikau

    31 de enero de 2014 a las 19:38

  • Mire cómo un puntero a un puntero puede ser un punto de “venta” único de C (en comparación con Java y otros): stackoverflow.com/questions/18373657/…

    – Joop Eggen

    31 de enero de 2014 a las 19:42

  • Además: si tuviera que “comenzar desde lo básico” con respecto a los punteros, su relación con las matrices no es algo que considero fundamental. El propósito fundamental de un puntero es habilitar la manipulación de ubicaciones de almacenamiento como datos. Que algunas de esas ubicaciones de almacenamiento sean arreglos de particular aridad es un detalle interesante pero no fundamental.

    –Eric Lippert

    31 de enero de 2014 a las 20:33

  • Mediante el uso typedefel código puede reducir el número de * en una declaración de variable o parámetro. foo(bar *a) muy bien presenta a como un puntero para escribir barincluso si typedef int **** bar. Sugerir 3+ * siempre se debe evitar y 2 * solo se usa en situaciones seleccionadas.

    – chux – Reincorporar a Monica

    31 de enero de 2014 a las 21:19

  • @EricLippert: StackOverflow es un gran lugar para poner información, punto. I soy haciendo una pregunta, aunque menos práctica. Aún así, muchas de estas preguntas (incluso las menos populares) aparecen en los resultados de búsqueda y ayudan a las personas.

    – darda

    31 de enero de 2014 a las 22:55

avatar de usuario
insecto del milenio

El uso de punteros triple+ está dañando tanto la legibilidad como la mantenibilidad.

Supongamos que tiene una pequeña declaración de función aquí:

void fun(int***);

Mmm. ¿Es el argumento una matriz irregular tridimensional, o un puntero a una matriz irregular bidimensional, o un puntero a un puntero a una matriz (como en, la función asigna una matriz y asigna un puntero a int dentro de una función)

Comparemos esto con:

void fun(IntMatrix*);

Seguramente puede usar punteros triples a int para operar en matrices. Pero eso no es lo que son. El hecho de que se implementen aquí como punteros triples es irrelevante al usuario

Las estructuras de datos complicadas deben ser encapsulado. Esta es una de las ideas manifiestas de la Programación Orientada a Objetos. Incluso en C, puede aplicar este principio hasta cierto punto. Envuelva la estructura de datos en una estructura (o, muy común en C, usando “controladores”, es decir, punteros a tipo incompleto; esta expresión se explicará más adelante en la respuesta).

Supongamos que implementó las matrices como matrices irregulares de double. En comparación con las matrices 2D contiguas, son peores cuando se itera sobre ellas (ya que no pertenecen a un solo bloque de memoria contigua), pero permiten acceder con notación de matriz y cada fila puede tener un tamaño diferente.

Entonces, ahora el problema es que no puede cambiar las representaciones ahora, ya que el uso de punteros está cableado sobre el código de usuario, y ahora está atascado con una implementación inferior.

Esto ni siquiera sería un problema si lo encapsulara en una estructura.

typedef struct Matrix_
{
    double** data;
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i][j];
}

simplemente se cambia a

typedef struct Matrix_
{
    int width;
    double data[]; //C99 flexible array member
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i*m->width+j];
}

La técnica de control funciona así: en el archivo de encabezado, declara una estructura incompleta y todas las funciones que funcionan en el puntero a la estructura:

// struct declaration with no body. 
struct Matrix_;
// optional: allow people to declare the matrix with Matrix* instead of struct Matrix*
typedef struct Matrix_ Matrix;

Matrix* create_matrix(int w, int h);
void destroy_matrix(Matrix* m);
double get_element(Matrix* m, int i, int j);
double set_element(Matrix* m, double value, int i, int j);

en el archivo fuente, declara la estructura real y define todas las funciones:

typedef struct Matrix_
{
    int width;
    double data[]; //C99 flexible array member
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i*m->width+j];
}

/* definition of the rest of the functions */

El resto del mundo no sabe lo que hace el struct Matrix_ contiene y no sabe el tamaño de la misma. Esto significa que los usuarios no pueden declarar los valores directamente, sino solo usando el puntero para Matrix y el create_matrix función. Sin embargo, el hecho de que el usuario no conozca el tamaño significa que el usuario no depende de él, lo que significa que podemos eliminar o agregar miembros a struct Matrix_ a voluntad.

  • +1. Este es un gran ejemplo y muestra claramente el valor de definir tipos de punteros cuando define un struct. Si hubiera hecho eso en mi código, podría haber evitado la notación de puntero triple, ya que estaba pasando un puntero a una matriz de punteros de algunos struct.

    – darda

    01/02/2014 a las 21:41


  • “Las estructuras de datos complicadas deben ser encapsulado” +1

    – Dave Cousineau

    26 de febrero de 2018 a las 21:27

avatar de usuario
Lundin

La mayoría de las veces, el uso de 3 niveles de indirección es un síntoma de malas decisiones de diseño tomadas en otras partes del programa. Por lo tanto, se considera una mala práctica y hay bromas sobre “programadores de tres estrellas” donde, a diferencia de la calificación de los restaurantes, más estrellas significa peor calidad.

La necesidad de 3 niveles de direccionamiento indirecto a menudo se origina en la confusión acerca de cómo asignar correctamente matrices multidimensionales de forma dinámica. Esto a menudo se enseña incorrectamente incluso en los libros de programación, en parte porque hacerlo correctamente era una carga antes del estándar C99. Mi publicación de preguntas y respuestas La asignación correcta de matrices multidimensionales aborda ese mismo problema y también ilustra cómo los múltiples niveles de direccionamiento indirecto harán que el código sea cada vez más difícil de leer y mantener.

Aunque como explica esa publicación, hay algunas situaciones en las que un type** podría tener sentido. Una tabla variable de cadenas con longitud variable es un ejemplo. Y cuando esa necesidad de type** surge, es posible que pronto tenga la tentación de utilizar type***porque necesita devolver su type** a través de un parámetro de función.

La mayoría de las veces, esta necesidad surge en una situación en la que está diseñando algún tipo de ADT complejo. Por ejemplo, digamos que estamos codificando una tabla hash, donde cada índice es una lista enlazada ‘encadenada’ y cada nodo en la lista enlazada es una matriz. Entonces, la solución adecuada es rediseñar el programa para usar estructuras en lugar de múltiples niveles de direccionamiento indirecto. La tabla hash, la lista enlazada y la matriz deben ser tipos distintos, tipos autónomos sin conocimiento mutuo.

Entonces, al usar un diseño adecuado, evitaremos las múltiples estrellas automáticamente.


Pero como ocurre con todas las reglas de las buenas prácticas de programación, siempre hay excepciones. Es perfectamente posible tener una situación como:

  • Debe implementar una matriz de cadenas.
  • El número de cadenas es variable y puede cambiar en tiempo de ejecución.
  • La longitud de las cuerdas es variable.

lata implementar lo anterior como un ADT, pero también puede haber razones válidas para mantener las cosas simples y simplemente usar un char* [n]. Luego tiene dos opciones para asignar esto dinámicamente:

char* (*arr_ptr)[n] = malloc( sizeof(char*[n]) );

o

char** ptr_ptr = malloc( sizeof(char*[n]) );

El primero es formalmente más correcto, pero también engorroso. Porque tiene que ser usado como (*arr_ptr)[i] = "string";mientras que la alternativa se puede utilizar como ptr_ptr[i] = "string";.

Ahora supongamos que tenemos que colocar la llamada malloc dentro de una función y el tipo de devolución está reservado para un código de error, como es costumbre con las API de C. Las dos alternativas se verán así:

err_t alloc_arr_ptr (size_t n, char* (**arr)[n])
{
  *arr = malloc( sizeof(char*[n]) );

  return *arr == NULL ? ERR_ALLOC : OK;
}

o

err_t alloc_ptr_ptr (size_t n, char*** arr)
{
  *arr = malloc( sizeof(char*[n]) );

  return *arr == NULL ? ERR_ALLOC : OK;
}

Es bastante difícil argumentar y decir que el primero es más legible, y también viene con el engorroso acceso que necesita la persona que llama. La alternativa de tres estrellas es en realidad más elegante, en este caso tan específico.

Por lo tanto, no nos sirve de nada descartar dogmáticamente 3 niveles de indirección. Pero la elección de usarlos debe estar bien informada, con la conciencia de que pueden crear un código feo y que existen otras alternativas.

  • Oh, entonces quieres decir que debemos diseñar el código cuidadosamente con intención y conciencia, y no confiar en pegadizo “X considerados tópicos dañinos?

    – ad absurdum

    17 de octubre de 2018 a las 9:42

  • @DavidBowling En general, sí. Pero para tomar la decisión de cuándo usar algo a pesar de que se considera una mala práctica, se necesita bastante conocimiento. Así que definitivamente no es una decisión para principiantes; simplemente deben seguir las mejores prácticas, punto.

    – Lundin

    17 oct 2018 a las 9:49

Entonces, ¿cuáles son algunas de las convenciones que existen? ¿Es esto realmente solo una cuestión de estilo/legibilidad combinada con el hecho de que muchas personas tienen dificultades para entender los punteros?

El direccionamiento indirecto múltiple no es mal estilo, ni magia negra, y si está tratando con datos de gran dimensión, entonces tendrá que lidiar con altos niveles de direccionamiento indirecto; si realmente se trata de un puntero a un puntero a un puntero a Tentonces no tengas miedo de escribir T ***p;. No oculte punteros detrás de typedefs a no ser que quien esté usando el tipo no tiene que preocuparse por su “puntería”. Por ejemplo, si está proporcionando el tipo como un “identificador” que se transmite en una API, como:

typedef ... *Handle;

Handle h = NewHandle();
DoSomethingWith( h, some_data );
DoSomethingElseWith( h, more_data );
ReleaseHandle( h );

entonces seguro, typedef fuera. Pero si h está destinado a ser desreferenciado, como

printf( "Handle value is %d\n", *h );

luego no typedef eso. Si tu usuario tiene que saber que h es un puntero a int1 para usarlo correctamente, entonces esa información debe no estar oculto detrás de un typedef.

Diré que en mi experiencia no he tenido que lidiar con niveles más altos de direccionamiento indirecto; el triple indirecto ha sido el más alto, y no he tenido que usarlo más que un par de veces. Si se encuentra regularmente lidiando con datos > tridimensionales, verá altos niveles de direccionamiento indirecto, pero si comprende cómo las expresiones de puntero y el direccionamiento indirecto trabajo no debería ser un problema.


1. O un puntero a puntero a into puntero a puntero a puntero a puntero a struct grdlphmpo lo que sea.

Después de dos niveles de indirección, la comprensión se vuelve difícil. Además, si la razón por la que está pasando estos punteros triples (o más) a sus métodos es para que puedan reasignar y restablecer alguna memoria apuntada, eso se aleja del concepto de métodos como “funciones” que simplemente devuelve valores y no afecta el estado. Esto también afecta negativamente la comprensión y la mantenibilidad más allá de algún punto.

Pero más fundamentalmente, te has topado con una de las principales objeciones estilísticas al puntero triple aquí:

Uno puede ver claramente cómo se requeriría un puntero triple (y más allá, en realidad).

Es el “y más allá” el problema aquí: una vez que llegas a tres niveles, ¿dónde te detienes? Seguramente es posible tener un número arbitrario de niveles de indirección. Pero es mejor tener un límite habitual en algún lugar donde la comprensibilidad sea buena pero la flexibilidad sea adecuada. Dos es un buen número. La “programación de tres estrellas”, como a veces se la llama, es controvertida en el mejor de los casos; es brillante o un dolor de cabeza para aquellos que necesitan mantener el código más tarde.

  • ¿Qué quiere decir con ‘Cuando declara un puntero, debe inicializarlo antes de usarlo en el programa’?

    – musicmatze

    31 de enero de 2014 a las 21:33

  • @musicmatze; Quiero decir que tienes que inicializarlo pasando una dirección de memoria válida. De lo contrario, será un puntero colgante.

    – trucos

    31 de enero de 2014 a las 22:08

  • @haacks: no veo la justificación para un -2 en esta respuesta. Le agradezco por señalar el tecnicismo de que las matrices no son punteros. Sin embargo, si no conoce la longitud en el momento de la compilación, debe asignar memoria y luego está eliminando la referencia de un puntero con notación de matriz. Creo que esto fue hecho a propósito. Si la diferencia fuera tan importante en un caso como este, habría pensado que alguien habría cambiado la sintaxis.

    – darda

    31 de enero de 2014 a las 22:58


  • @pelesl; A veces también me confundo al votar negativamente 🙂

    – trucos

    1 de febrero de 2014 a las 13:47

  • @musicmatze; Lee mi respuesta de nuevo. Dije: necesitas inicializarlo antes de usarlo en el programaesta dirección es de lectura y escritura.

    – trucos

    2 de febrero de 2014 a las 18:27


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad