Percursos
/
C
C
/
Programa
/
Ponteiros
Po

Ponteiros em C

{one: "1 exercício", many: "%{count} exercícios", other: "%{count} exercícios"}

Sobre Ponteiros

Um ponteiro é uma variável ou constante cujo valor é o endereço de outra variável ou constante. Os ponteiros são uma funcionalidade poderosa e potencialmente perigosa do C. Pode ser difícil compreender os ponteiros.

int i = 5;

Quando atribuímos um valor a uma variável, podemos depois fazer duas perguntas.

Quem és tu?

Eu sou i.

O que tens?

Tenho o inteiro 5.

Há também uma terceira pergunta que podemos fazer.

Onde estás?

Estou em 0x6dfed4.

Espera, o quê?

As variáveis e as constantes guardam o seu valor numa posição de memória. Essa posição na memória é conhecida como o endereço da variável ou da constante.

Operador de endereço e operador de indireção (parte 1)

Para obter o endereço de uma variável, usamos o operador de endereço, que é &.

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

Espera, o que significa aquele * antes de i_address? Não significa multiplicar?

Um tipo seguido de um * e depois de um identificador é a forma de declarar um ponteiro para uma variável desse tipo. O * é conhecido como o operador de indireção porque se refere indiretamente ao valor cujo endereço o ponteiro guarda.

Está bem, mas porque é que havemos de criar uma variável só para guardar o endereço de outra variável?

Porque é que nos interessa, afinal, qual é o endereço de uma variável?

Bem, interessa-nos saber onde estão as variáveis quando fazem parte de um array.

Arrays e ponteiros

Quando criamos um array assim

int my_ints[] = {1, 2};

O valor de my_ints é, na verdade, o seu endereço, que também por acaso é igual ao endereço do seu primeiro elemento. O endereço do seu segundo elemento será o endereço do primeiro elemento mais o tamanho de um int no sistema.

Para veres isto melhor, experimenta correr o seguinte

#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 ponteiros, ou porque é que os índices dos arrays começam em 0

O exemplo anterior mostra porque é que a indexação de arrays começa em 0 e não em 1. O índice de um array multiplica o índice pelo tamanho do tipo e soma-o ao endereço inicial do array. Como o primeiro elemento coincide com o início do array, multiplicamos o tipo por 0 para o somar ao endereço inicial e obter o primeiro elemento. Para obter o segundo elemento, multiplicamos o tipo por 1 para o somar ao endereço inicial. Se o tipo tiver quatro bytes de comprimento, o endereço do segundo elemento será quatro bytes superior ao do primeiro elemento.

É também por isso que os ponteiros são declarados como sendo de um determinado tipo. Se um ponteiro não soubesse para que tipo aponta, não saberia quantos bytes avançar ao somar-lhe 1. Somar 1 a um ponteiro não significa necessariamente que estamos a somar um byte ao endereço. Significa que estamos a somar 1 vezes o comprimento do seu tipo em bytes.

Mas espera, há mais! Não pensavas que já tínhamos acabado com o *, pois não?

Operador de indireção (parte 2)

Agora que percebemos como os endereços funcionam com a indexação de arrays, de que serve isto se não obtivermos o valor guardado num endereço? Como é que fazemos isso? Para obter o valor guardado num endereço, usamos o operador de indireção, que, como deves recordar-te, é o *. Nesta situação, o operador de indireção é também por vezes chamado operador de desreferenciação, porque desreferencia o valor cujo endereço o ponteiro tem. Chamar-lhe operador de desreferenciação quando é usado para desreferenciar distingue-o do operador de indireção usado para definir um ponteiro, mas o * em ambos os usos chama-se, tecnicamente, operador de indireção.

Vá lá! Estás a brincar comigo? Quantos usos tem o *?! Multiplicação, declaração de um ponteiro e agora desreferenciação! É confuso! E o que é exatamente a desreferenciação, afinal?

Um exemplo mostra como usar o operador de indireção 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ênteses pode ajudar a eliminar a ambiguidade de intenções ao usar aritmética de ponteiros. O exemplo seguinte pode não imprimir o que o programador pretendia

#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 o * como o ++ têm a mesma precedência, mas são avaliados da direita para a esquerda. Na primeira linha, my_ptr é primeiro incrementado para o segundo elemento, que é depois desreferenciado. Na segunda linha, my_ptr, que foi incrementado para o segundo elemento na primeira linha, é primeiro desreferenciado e, em seguida, o valor desreferenciado é incrementado. Se adicionarmos esta linha ao exemplo 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);

Nessa linha, incrementámos o ponteiro para além do array. Fomos além do array e estamos a apontar para os quatro bytes seguintes da memória, que o ponteiro interpreta como um int, mas, na verdade, esses bytes podem pertencer a outra variável ou a outras variáveis. Esses quatro bytes podem fazer parte de um double ou podem ser quatro chars. O problema é que nem sequer devíamos estar a olhá-los com o nosso ponteiro.

Podíamos ter utilizado

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

Como a indexação de arrays é implementada com um ponteiro, aplica-se o mesmo perigo de ultrapassar o array.

E, como uma string é um array de chars, os ponteiros também podem ser usados com strings. Uma string é frequentemente declarada com a sintaxe de ponteiro, assim

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

Mas há uma questão. Porque é que colocamos o asterisco ao lado do identificador? Porque não colocá-lo ao lado do tipo, já que é um ponteiro para esse tipo? Porque isto

int* my_ptr1, my_ptr2_that_is_really_an_int;

não declara dois ponteiros para um int, mas declara um ponteiro para um int e um int. Para declarar dois ponteiros para um int, seria assim

int *my_ptr1, *my_ptr2;

Por isso, é boa ideia colocar o * perto do identificador para minimizar a ambiguidade. Ou declarar apenas um identificador por linha. E, se ainda não tiveste o suficiente de *, bem-vindo a...

Ponteiro para ponteiros

É comum ver arrays de strings representados assim

#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;
}

Pode não ser óbvio, mas o array de strings é declarado e inicializado como um ponteiro para um array de arrays de char. Quando o array de arrays de char é passado a print_strings(), a assinatura descreve-o como um ponteiro para ponteiro(s) de char.

Então, como é que sabemos que char ** é realmente um ponteiro para um array de ponteiros para arrays de char, e não um ponteiro para um ponteiro de um único char?

Não sabemos.

Esta é uma das muitas coisas que podem tornar o uso de ponteiros confuso. O seguinte pode compilar

#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;
}

Mas print_strings() não está a ser usada como pretendido. Usar uma assinatura diferente não ajuda. Alterar a assinatura para isto

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

pode continuar a compilar e continuar a imprimir disparates. Mas não há realmente utilidade em passar um ponteiro para um ponteiro de um único char. Muito provavelmente, quando vês char **, representa um ponteiro para um array de ponteiros para arrays de char.

Já vimos um ponteiro passado a uma função. Mas porque é que se passa um ponteiro a uma função?

Passar um ponteiro a uma função

Em C, os argumentos são passados às funções por valor. Isso significa que o valor é copiado do argumento e que essa cópia é local à função. Quando a função termina, a cópia do valor também termina.

Vê o que acontece quando tentamos alterar valores dentro de uma função

#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;
}

Isto acontece porque, apesar de as variáveis terem os mesmos nomes, o a e o b em swap() não são o mesmo a e b em main(). São cópias dos valores de a e b. Podemos alterar a swap para que funcione como pretendemos?

Podemos, sim! Podemos passar os endereços de a e b como ponteiros. Mas de que serve isso, se a função copia os ponteiros? Serve, porque um endereço copiado continua a ser o mesmo endereço. Ao desreferenciar o endereço, conseguimos aceder ao valor original tal como foi definido em 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;
}

Agora há muitos *s na swap, mas finalmente funciona como pretendido.

Uma forma de ler o operador de indireção é «valor em». Assim, *a é o valor em a. Até que ponto é que isto funciona com strings?

Ponteiros para char

Sabemos que o valor de uma variável de array é o endereço onde o array começa. E sabemos que podemos definir uma string como um array de char, char my_string[], ou como um ponteiro para char, char *. Em que é que são diferentes?

Neste código, passamos um argumento char * a describe_me(). O argumento adjective é definido como um array de char e é aceite pela função como um ponteiro para 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;
}

Conseguimos fazer uma alteração persistente a adjective, o que demonstra que o array de char definido pode ser aceite como um ponteiro para char.

Agora vamos tentar definir adjective diretamente como um ponteiro para 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;
}

Lembras-te de que os ponteiros são poderosos, mas também potencialmente perigosos? Mas porque é que não funciona? Um array é um ponteiro e um ponteiro funcionou com um array de char! A razão é que, antes, adjective era definido como um array de char, que é uma variável pertencente a main(). Agora, adjective é definido como um literal de string, o que significa que os charss são escritos numa memória estática só de leitura que pertence a todo o programa. Ambos são ponteiros, mas apontam para coisas diferentes. Tentar alterar um valor de string literal é como tentar alterar o valor literal de 5. Não é permitido. Portanto, o problema não é propriamente o facto de ser um ponteiro. O problema é aquilo para que aponta.

Editar via GitHub A ligação abre numa nova janela ou separador