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.
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.
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;
}
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?
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...
É 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?
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?
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.