Tracks
/
C
C
/
Temario
/
Punteros
Pu

Punteros en C

{one: "1 ejercicio", many: "%{count} de 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 poderosa y potencialmente peligrosa de C. Entender los punteros puede ser todo un desafío.

int i = 5;

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

¿Quién eres?

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 guardan su valor en una ubicación de memoria. Esa ubicación en 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 * antes de i_address? ¿No significa multiplicar?

Un tipo seguido de un * y luego 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.

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

¿Y por qué nos importa, para empezar, cuál es la dirección de una variable?

Bueno, 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 resulta ser la misma que 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 un array empiezan en 0

El ejemplo anterior muestra por qué el indexado de arrays empieza en 0 en lugar de en 1. El índice dentro 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 y lo sumamos a la dirección inicial para obtener el primer elemento. Para obtener el segundo elemento multiplicamos el tipo por 1 y lo sumamos a la dirección inicial. Si el tipo ocupa cuatro bytes, la dirección del segundo elemento estará cuatro bytes más adelante que la del primero.

Esto también explica por qué los punteros se declaran con un tipo determinado. 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 estemos sumando 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 creías que ya habíamos terminado con *, ¿verdad?

El operador de indirección (parte 2)

Ahora que entendemos cómo funcionan las direcciones con el indexado de arrays, ¿de qué sirve todo esto si no obtenemos el valor que está guardado en una dirección? ¿Cómo lo hacemos? Para obtener el valor guardado 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 sirve para definir un puntero, pero, técnicamente, el * en ambos usos se llama operador de indirección.

¡Vamos! ¿Estás bromeando? ¿Cuántos usos tiene el *? ¡Multiplicación, declarar un puntero y ahora desreferenciar! ¡Es confuso! Y, después de todo, ¿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 usar aritmética de punteros. El siguiente ejemplo puede que 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 primero se incrementa al segundo elemento, que luego se desreferencia. En la segunda línea, my_ptr, que se incrementó al segundo elemento en la primera línea, primero se desreferencia y luego 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 quedar más allá del array. Nos hemos pasado del array y estamos apuntando a los siguientes cuatro bytes en memoria que el puntero interpreta como un int, pero, en realidad, esos bytes 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 el indexado de arrays se implementa con un puntero, se aplica el mismo peligro de pasarse del array.

Y, como un string es un array de char, también se pueden usar punteros con strings. Un string suele declararse usando la sintaxis de punteros, así

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

Sin embargo, 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 sería 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 aún no has tenido suficiente de *, te damos la bienvenida a...

Punteros a punteros

Es común 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 obvio, 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 firma 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 de un solo char?

No lo sabemos.

Esta es una de las muchas cosas que pueden hacer que usar punteros sea confuso. Lo siguiente puede que 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 firma diferente no ayuda. Cambiar la firma a esta

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

puede que siga compilando y siga imprimiendo basura. Pero realmente no tiene ningún valor pasar un puntero a un puntero de un solo 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, también termina el valor copiado.

Mira qué pasa 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 de 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é servirá eso, 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 por fin funciona como se pretendía.

Una forma de leer el operador de indirección es «valor en». Entonces, *a es el valor en a. ¿Qué tan bien 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 char * a describe_me(). El argumento adjective está definido 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;
}

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

Ahora intentemos 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 poderosos, pero también potencialmente peligrosos? Pero ¿por qué no funciona? ¡Un array es un puntero, y un puntero sí funcionó con un array de char! 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 diferentes. Intentar cambiar el valor de un string literal es como intentar cambiar el valor literal de 5. No está permitido. Así que el problema en realidad no es que sea un puntero. El problema es aquello a lo que apunta.

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