Tracks
/
C
C
/
Temario
/
Punteros
Pu

Punteros en C

{one: "1 ejercicio", many: "%{count} ejercicios", other: "%{count} ejercicios"}

Acerca de Punteros

Un puntero es una variable o constante cuyo valor es la dirección de otra variable o constante. Los punteros son una característica potente y potencialmente peligrosa de C. Puede resultar difícil entender los punteros.

int i = 5;

Cuando asignamos un valor a una variable, podemos hacernos dos preguntas.

¿Quién eres?

Yo soy i.

¿Qué tienes?

Tengo el entero 5.

También hay una tercera pregunta que podemos hacer.

¿Dónde estás?

Estoy en 0x6dfed4.

Espera, ¿qué?

Las variables y las constantes almacenan su valor en una posición de memoria. Esa posición de memoria se conoce como la dirección de la variable o la constante.

El operador de dirección y el operador de indirección (parte 1)

Para obtener la dirección de una variable usamos el operador de dirección, que es &.

int i = 5;
int *i_address = &i;

Espera, ¿qué significa ese * delante de i_address? ¿No significa multiplicar?

Un tipo seguido de un * y de un identificador es la forma de declarar un puntero a una variable de ese tipo. El * se conoce como el operador de indirección porque se refiere indirectamente al valor cuya dirección contiene el puntero.

Vale, pero ¿por qué crear una variable solo para contener la dirección de otra variable?

¿Y por qué nos importa la dirección de una variable?

Pues nos importa dónde están las variables cuando forman parte de un array.

Arrays y punteros

Cuando creamos un array así

int my_ints[] = {1, 2};

El valor de my_ints es en realidad su dirección, que además coincide con la dirección de su primer elemento. La dirección de su segundo elemento será la dirección del primer elemento más el tamaño de un int en el sistema.

Para verlo mejor, prueba a ejecutar lo siguiente

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

Aritmética de punteros, o por qué los índices de los arrays empiezan en 0

El ejemplo anterior muestra por qué la indexación de arrays empieza en 0 en lugar de en 1. El índice de un array multiplica el índice por el tamaño del tipo y lo suma a la dirección inicial del array. Como el primer elemento coincide con el inicio del array, multiplicamos el tipo por 0 para sumarlo a la dirección inicial y obtener el primer elemento. Para obtener el segundo elemento multiplicamos el tipo por 1 para sumarlo a la dirección inicial. Si el tipo ocupa cuatro bytes, la dirección del segundo elemento estará cuatro bytes por encima de la del primero.

Esto también explica por qué los punteros se declaran de un tipo concreto. Si un puntero no supiera a qué tipo apunta, no sabría cuántos bytes avanzar al sumarle 1. Sumar 1 a un puntero no significa necesariamente que sumemos un byte a la dirección. Significa que sumamos 1 multiplicado por la longitud de su tipo en bytes.

Pero espera, ¡hay más! No creerías que ya habíamos terminado con el *, ¿verdad?

El operador de indirección (parte 2)

Ahora que entendemos cómo funcionan las direcciones con la indexación de arrays, ¿de qué sirve todo esto si no obtenemos el valor almacenado en una dirección? ¿Cómo lo hacemos? Para obtener el valor almacenado en una dirección usamos el operador de indirección, que, como recordarás, es *. En esta situación, al operador de indirección también se le llama a veces operador de desreferencia, porque desreferencia el valor cuya dirección tiene el puntero. Llamarlo operador de desreferencia cuando se usa para desreferenciar lo distingue del operador de indirección que define un puntero, pero, técnicamente, el * de ambos usos se llama operador de indirección.

¡Venga ya! ¿Es una broma? ¿Cuántos usos tiene el *? ¡Multiplicación, declarar un puntero y ahora desreferenciar! ¡Es confuso! ¿Y qué es exactamente desreferenciar?

Un ejemplo muestra cómo usar el operador de indirección para desreferenciar

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

Usar paréntesis puede ayudar a eliminar la ambigüedad de intenciones al hacer aritmética de punteros. Puede que el siguiente ejemplo no imprima lo que el programador pretendía

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

Tanto el * como el ++ tienen la misma precedencia, pero se evalúan de derecha a izquierda. En la primera línea, my_ptr se incrementa primero hasta el segundo elemento, que luego se desreferencia. En la segunda línea, my_ptr, que ya se había incrementado hasta el segundo elemento en la primera línea, se desreferencia primero y, después, se incrementa el valor desreferenciado. Si añadimos esta línea al ejemplo anterior

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

En esa línea hemos incrementado el puntero hasta situarlo más allá del array. Hemos desbordado el array y estamos apuntando a los siguientes cuatro bytes de memoria, que el puntero interpreta como un int, pero que, en realidad, podrían pertenecer a otra variable o a otras variables. Esos cuatro bytes podrían formar parte de un double o podrían ser cuatro char. El problema es que ni siquiera deberíamos estar mirándolos con nuestro puntero.

Podríamos haber usado

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

Como la indexación de arrays se implementa con un puntero, existe el mismo peligro de desbordar el array.

Y, como un string es un array de char, los punteros también se pueden usar con strings. A menudo, un string se declara usando la sintaxis de punteros, así

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

Aunque hay una pregunta. ¿Por qué ponemos el asterisco junto al identificador? ¿Por qué no ponerlo junto al tipo, si es un puntero a ese tipo? Porque esto

int* my_ptr1, my_ptr2_that_is_really_an_int;

no declara dos punteros a un int, sino un puntero a un int y un int. Para declarar dos punteros a un int habría que escribir algo así

int *my_ptr1, *my_ptr2;

Así que es buena idea poner el * cerca del identificador para minimizar la ambigüedad. O declarar solo un identificador por línea. Y, si no has tenido suficiente con el *, bienvenido a...

Puntero a punteros

Es habitual ver arrays de strings representados así

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

Puede que no sea evidente, pero el array de strings se declara e inicializa como un puntero a un array de arrays de char. Cuando el array de arrays de char se pasa a print_strings(), la signatura lo describe como un puntero a puntero(s) a char.

Entonces, ¿cómo sabemos que char ** es realmente un puntero a un array de punteros a arrays de char, y no un puntero a un puntero a un único char?

No lo sabemos.

Esta es una de las muchas cosas que pueden hacer que usar punteros resulte confuso. Puede que lo siguiente compile

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

Pero print_strings() no se está usando como se pretendía. Usar una signatura diferente no ayuda. Cambiar la signatura por esta

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

puede que siga compilando y siga imprimiendo basura. Pero realmente no tiene ningún sentido pasar un puntero a un puntero a un único char. Lo más probable es que, cuando veas char **, represente un puntero a un array de punteros a arrays de char.

Hemos visto un puntero pasado a una función. ¿Pero por qué pasar un puntero a una función?

Pasar un puntero a una función

En C, los argumentos se pasan a las funciones por valor. Eso significa que el valor se copia del argumento y que esa copia es local a la función. Cuando la función termina, la copia del valor también desaparece.

Fíjate en lo que ocurre cuando intentamos cambiar valores dentro de una función

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Esto se debe a que, aunque las variables tengan los mismos nombres, a y b en swap() no son las mismas a y b que en main(). Son copias de los valores de a y b. ¿Podemos cambiar swap para que funcione como pretendemos?

¡Sí, podemos! Podemos pasar las direcciones de a y b como punteros. Pero ¿de qué sirve, si la función copia los punteros? Sirve, porque una dirección copiada sigue siendo la misma dirección. Al desreferenciar la dirección, podemos acceder al valor original tal como se definió en main().

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Ahora hay muchos * en swap, pero ya funciona como se pretendía.

Una forma de leer el operador de indirección es «el valor en». Así, *a es el valor en a. ¿Cómo funciona esto con los strings?

Punteros a char

Sabemos que el valor de una variable de tipo array es la dirección donde empieza el array. Y sabemos que podemos definir un string como un array de char, char my_string[], o como un puntero a char, char *. ¿En qué se diferencian?

En este código pasamos un argumento de tipo char * a describe_me(). El argumento adjective se define como un array de char y la función lo acepta como un puntero a char.

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Podemos hacer un cambio persistente en adjective, lo que demuestra que el array de char definido se puede aceptar como un puntero a char.

Ahora vamos a intentar definir adjective directamente como un puntero a char.

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

¿Recuerdas que los punteros son potentes, pero también potencialmente peligrosos? ¿Pero por qué no funciona? ¡Un array es un puntero, y con un array de char sí funcionaba! La razón es que, antes, adjective estaba definido como un array de char, que es una variable que pertenece a main(). Ahora, adjective está definido como un string literal, lo que significa que los chars se escriben en memoria estática de solo lectura que pertenece a todo el programa. Ambos son punteros, pero apuntan a cosas distintas. Intentar cambiar el valor de un string literal es como intentar cambiar el valor literal de 5. No está permitido. Así que el problema no es realmente que sea un puntero. El problema es a qué apunta.

Editar en GitHub El enlace se abre en una nueva ventana o pestaña