3

Lo que más me costó entender de C(y C++ como heredero de C) fueron los punteros, incluso es la hora y aún logran enredarme.

Hay cierta característica no he llegado a comprenderla del todo, solo he mecanizado su correcto funcionamiento y es el paso de punteros por referencia a funciones.

Entiendo que en C, acceder a la referencia de una variable automática con el ampersand(&) se obtiene la dirección de memoria de esa variable y a su vez los punteros son variables que apuntan a direcciones de memoria; los punteros almacena direcciones de memoria.

Una función C, se le pueden pasar dos tipos de parámetros(excluyendo a las variadic): parámetros por valor y parámetros por referencia. El siguiente ejemplo es la correcta utilización de un parámetro pasado por referencia de una variable automática:

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int numero = 0;
funcion_init(&numero);
printf("%d", numero);

la variable numero después de ejecutar ese ejemplo será 10, hasta acá no hay ningún problema, pero de aquí en adelante es el problema. Realizando las siguientes modificación al ejemplo anterior:

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(numero);
printf("%d", *numero);

Ejecuntando este ejemplo el resultado será 0 y no 10 como se espera, aquí toca realizar lo que no posee mucha lógica para mí: pasar un puntero por referencia ¿Pero no se supone que los punteros ya son direcciones de memoria en si?

void funcion_init(int **varRef)
{
    **varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(&numero);
printf("%d", *numero);

Cuando se utiliza el ampersand(&) a un puntero que almacena una dirección de memoria para pasarlo como referencia a una función ¿Qué se está pasando a la función, la dirección de memoria de una dirección de memoria?

¿Cuál es la lógica detrás de esto?.

Mario
  • 653
  • 3
  • 19

3 Answers3

3

Tienes un serio lío de conceptos, lo voy a poner grande y en negrita para que quede claro:

En el lenguaje no existen las referencias.


Cuando dices:

Hay cierta característica no he llegado a comprenderla del todo, solo he mecanizado su correcto funcionamiento y es el paso de punteros por referencia a funciones.

Te estás equivocando, pasas punteros por copia (no por referencia) a funciones.

Cuando dices:

Entiendo que en C, acceder a la referencia de una variable automática con el ampersand(&) se obtiene la dirección de memoria de esa variable

Te estás equivocando, en C no puedes acceder a la referencia de nada porque en C no existen las referencias.

Cuando dices:

Una función C, se le pueden pasar dos tipos de parámetros(excluyendo a las variadic): parámetros por valor y parámetros por referencia.

Te estás equivocando, la única manera de pasar parámetros a funciones C es por copia.

Cuando dices:

El siguiente ejemplo es la correcta utilización de un parámetro pasado por referencia de una variable automática

Te estás equivocando, estás copiando un puntero dentro de una función, no es un parámetro por referencia porque en el lenguaje C las referencias no existen.

Para saber la diferencia entre puntero y referencia lee este hilo que habla de las diferencias entre puntero y referencia en un lenguaje que tiene referencias.


Ahora voy a ilustrar con ASCII art lo que está sucediendo en tus ejemplos:

Primer ejemplo:

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int numero = 0;
funcion_init(&numero);
printf("%d", numero);

Tenemos un entero (numero), la función funcion_init recibe un puntero a entero llamado varRef dentro del cual se COPIA la dirección de memoria de numero:

| (int) numero |       |     (int *)varRef     |
+--------------+ <=====+-----------------------+
| 0            |       | dirección de 'numero' |

El operador et (&) obtiene la dirección de memoria de una variable, la cuál se copia dentro de un puntero de la misma manera que copias números dentro de variables numéricas:

int valor = 42;  // Copiamos 42 en valor.
int *x = &valor; // Copiamos la dirección de 'valor' en 'x'.

En general usamos el verbo asignar a la acción de copiar un valor dentro de una variable, así que estamos asignando valor a valor y x.

Segundo ejemplo:

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(numero);
printf("%d", *numero);

La función malloc reserva un espacio de memoria y devuelve un puntero a dicho espacio, ese puntero lo copias en numero y posteriormente lo copias en varRef:

((( Espacio con tamaño sizeof(int) ))) <==== |               (int *) numero            |
                                    \        +-----------------------------------------+
                                     \       | dirección del espacio pedido con malloc |
                                      \ 
                                       \==== |               (int *) varRef            |
                                             | dirección del espacio pedido con malloc |

Se imprimirá 10, porque ambos punteros apuntan a la misma memoria.

Tercer ejemplo:

void funcion_init(int **varRef)
{
    **varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(&numero);
printf("%d", *numero);

La función malloc reserva un espacio de memoria y devuelve un puntero a dicho espacio que copias en el puntero numero, la función funcion_init recibe un puntero a puntero a entero llamado varRef en el que copias la dirección de numero que es un puntero y pedir su dirección da un puntero a puntero:

((( Espacio con tamaño sizeof(int) ))) <==== |               (int *) numero            |
                                             +-----------------------------------------+
|    (int **) varRef    | =================> | dirección del espacio pedido con malloc |
+-----------------------+
| dirección de 'numero' |

Para entender mejor los punteros, te recomiendo leer este hilo.

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
3

Primero debes entender que en lenguaje C, el concepto de pase por referencia no existe, porque no es posible declarar una referencia tal como se lo hace en otros lenguajes como por ejemplo, C++.

Así que vamos recordar tres conceptos:

  • pasar por referencia: Significa que pasarás la variable original a una referencia, por lo tanto, durante la ejecución de una función, podremos alterar el contenido de la variable original.

  • pasar por valor: Significa que pasarás una copia de la variable original a un parámetro.

  • pasar por puntero: Significa que pasarás la variable original a un puntero, por lo tanto, durante la ejecución de una función, podremos alterar el contenido de la variable original.

Quiero que quede claro que en C no usamos el concepto de pase por referencia, sino, pase por puntero, que básicamente hacen lo mismo, pero la forma de usarlo es diferente.

Ejemplo (1):

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int numero = 0;
funcion_init(&numero);
printf("%d", numero);

En este ejemplo se está pasando por puntero y esto se debe porque la dirección de memoria de la variable numero la recibe un puntero (es decir, int* varRef). Por lo tanto, podemos alterar el contenido de la variable numero.

Ejemplo (2):

void funcion_init(int *varRef)
{
    *varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(numero);
printf("%d", *numero);

El resultado por pantalla es 10 y esto se debe porque en este ejemplo si se está pasando por valor, es decir, se está enviando una copia del contenido de numero, que justamente coincide con la dirección de memoria del espacio que se haya reservado con malloc.

Ojo: Aquí no se está pasando por puntero porque no estamos pasando la variable original (en este caso es int* numero), por lo tanto, nunca podremos alterar el contenido de numero.

Entonces duramente la ejecución de dicha función, podemos modificar el contenido de dicho espacio (el que reservaste con malloc).

Respondiendo a tu pregunta:

¿Pero no se supone que los punteros ya son direcciones de memoria en si?

Sí, los punteros internamente consumen un espacio de memoria, donde guardan una dirección de memoria de X variable.

Una representación en memoria sería así:

 int* numero    
  |0x04|        |0x06|
  (0x06)          (0)

En esta representación, la dirección 0x04 es la del puntero, donde guarda la dirección 0x06 (que guarda el valor 0). De este modo, cuando ejecutas esta función:

funcion_init(numero);

Lo que realmente pasas es la dirección 0x06 y para lograrlo, se debe primero acceder al contenido del puntero (que en nuestro ejemplo está alojado en la dirección 0x04).

Ejemplo (3):

void funcion_init(int **varRef)
{
    **varRef = 10;
}

int *numero = malloc(sizeof(int));
*numero = 0;
funcion_init(&numero);
printf("%d", *numero);

En este ejemplo estamos pasando por puntero y esto se debe porque en realidad lo que estamos pasando es la dirección de memoria (que para nosotros es la variable original) del puntero numero. Entonces como el parámetro varRef es un puntero doble, puede guardar la dirección de memoria de un puntero, por ende, podemos alterar dos cosas durante la ejecución de funcion_init:

  • El contenido del puntero numero.

  • El contenido del espacio de memoria que se reservó con la función malloc.

Respondiendo a tu pregunta:

¿Qué se está pasando a la función, la dirección de memoria de una dirección de memoria?

No exactamente, lo que estás pasando es simplemente una dirección de memoria.

Lo comprobamos con este diagrama de memoria:

 int* numero                 int** varRef
  |0x04|        |0x06|          |0x08|
  (0x06)          (0)            (0x04)

Cuando esta función se ejecute:

funcion_init(&numero);

Lo que estás pasando es la dirección de memoria donde está alojado el puntero numero, en este caso, en la dirección 0x04. Por lo tanto, el parámetro varRef lo que realmente recibe, es la dirección de memoria del puntero, es decir, la dirección 0x04.

Pregunta frecuente:

¿Los arrays en C se pasan por referencia?

Respuesta: No. Porque el concepto de pase por referencia en C no existe.

Lo que si podríamos decir, es que los arrays si se pasan por puntero, porque lo que realmente estás pasando es la variable original, que obviamente coincide con la dirección de memoria del primer elemento del array y por ende, podemos modificar el array original.

Conclusión:

No uses el término pasar por referencia en C, trae confusiones. Lo ideal es reemplazarlo por el término pasar por puntero.

MrDave1999
  • 7,491
  • 1
  • 7
  • 22
0

chic@, en cualquiera de los 3 casos el resultado es 10

La utilidad de un puntero a puntero, es muy poca si lo intentas ver desde la perspectiva de una sola variable. Una vez te vas a más dimensiones la utilidad sale a relucir.

Ten en cuenta este código

char string[33] = "Esto es una cadena de caracteres";
char* string = (char*)malloc(33);//malloc(sizeof(char)*33)

cuando haces un arreglo. siempre estarás usando punteros. Entonces puedes hacer cosas como estas:

char string[33] = "Esto es una cadena de caracteres";
char* puntero1 = string[0];
printf("%s",puntero1);//Esto es una cadena de caracteres
char* puntero2 = string[7];
printf("%s",puntero2);// una cadena de caracteres

como sabes que cada que trabajas con arreglos, también trabajas con punteros, entoces puedes apuntar a un arreglo bidimensional con dobles punteros. Te abrás dado cuenta que todos los programas empiezan con

 int main(int argc, char* argv[]);

entonces como argv es un arreglo, también puede ser referenciado con punteros. Por lo que puedes escribirlo como:

 int main(int argc, char** argv);

Entonces se puede decir que un doble puntero es un arreglo de punteros. y un triple puntero es un arreglo de arreglos de punteros.

Lo segundo

Por como trabaja c, todos los argumentos que se le envían a las funciones son copias de la variable que le envías. Es por eso que pasar el argumento por referencia es importante. Cuando pasas un puntero, estás pasando una copia de la dirección de memoria que almacena. Y no existe diferencia entre usar "&" o puntero.

Por Ejemplo

 typedef struct structura{
      int i;
 }tipo;

 int main(int argc, char** argv)
 {
   tipo tipo1 = (tipo)malloc(sizeof(struct structura));
   tipo *puntero = &tipo1;
 }
  • *cuando haces un arreglo. siempre estarás usando punteros.*, Eso depende. Si defines un array estático, no necesitas de un puntero, porque el nombre del array solo representaría la dirección base del array. Por ejemplo, si tienes el siguiente parámetro: `char *argv[]`, el identificador `argv` no sería un arreglo, sino un puntero. – MrDave1999 May 08 '20 at 14:33