Um ponteiro é uma variável ou constante cujo valor é o endereço de outra variável ou constante. Os ponteiros são um recurso poderoso e potencialmente perigoso do C. Pode ser desafiador entender ponteiros.
int i = 5;
Quando atribuímos um valor a uma variável, podemos fazer duas perguntas.
Quem é você?
Eu sou i.
O que você tem?
Eu tenho o inteiro 5.
Há também uma terceira pergunta que podemos fazer.
Onde você está?
Eu estou em 0x6dfed4.
Espera aí, o quê?
Variáveis e constantes guardam seu valor em um local na memória. Esse local na memória é conhecido como o endereço da variável ou constante.
Para obter o endereço de uma variável, usamos o operador de endereço, que é o &.
int i = 5;
int *i_address = &i;
Espera aí, o que significa aquele * antes de i_address? Isso não quer dizer multiplicação?
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 operador de indireção porque se refere indiretamente ao valor cujo endereço o ponteiro guarda.
Tá, mas por que criar uma variável só para guardar o endereço de outra variável?
Por que, afinal, nos importamos com o endereço de uma variável?
Bom, nos importamos com onde as variáveis estão quando fazem parte de um array.
Quando criamos um array assim
int my_ints[] = {1, 2};
O valor de my_ints é, na verdade, o endereço dele, que por acaso é o mesmo endereço do seu primeiro elemento.
O endereço do segundo elemento será o endereço do primeiro elemento mais o tamanho de um int no sistema.
Para ver isso melhor, experimente rodar 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 por que a indexação de array começa em 0 em vez de 1.
Ao indexar um array, o índice é multiplicado pelo tamanho do tipo e somado ao endereço inicial do array.
Como o primeiro elemento coincide com o início do array, multiplicamos o tipo por 0 para somar ao endereço inicial e obter o primeiro elemento.
Para obter o segundo elemento, multiplicamos o tipo por 1 para somar ao endereço inicial.
Se o tipo tem quatro bytes de tamanho, o endereço do segundo elemento será quatro bytes maior que o do primeiro elemento.
É também por isso que os ponteiros são declarados com um determinado tipo.
Se um ponteiro não soubesse para que tipo aponta, não saberia quantos bytes avançar ao somar 1 a ele.
Somar 1 a um ponteiro não significa necessariamente somar um byte ao endereço.
Significa somar 1 vez o tamanho do tipo em bytes.
Mas espera, tem mais! Você não achou que tínhamos acabado com o *, achou?
Agora que entendemos como os endereços funcionam com a indexação de array, de que adianta se não conseguimos obter o valor guardado em um endereço?
Como fazemos isso?
Para obter o valor guardado em um endereço, usamos o operador de indireção, que, como você deve se lembrar, é o *.
Nessa situação, o operador de indireção também é às vezes chamado de operador de desreferência, porque desreferencia o valor cujo endereço o ponteiro guarda.
Chamá-lo de operador de desreferência quando usado para desreferenciar distingue-o do operador de indireção para definir um ponteiro, mas o * em ambos os usos é tecnicamente chamado de operador de indireção.
Ah, qual é!
Tá de brincadeira?
Quantos usos tem o *?!
Multiplicação, declarar um ponteiro e agora desreferenciar!
É confuso!
E o que exatamente é desreferenciar, 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 a seguir 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 * quanto 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 então é desreferenciado.
Na segunda linha, my_ptr, que foi incrementado para o segundo elemento na primeira linha, é primeiro desreferenciado, e então o valor desreferenciado é incrementado.
Se adicionarmos esta linha ao exemplo acima
// 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, incrementamos o ponteiro para além do array.
Ultrapassamos o array e estamos apontando para os próximos quatro bytes na 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 não deveríamos nem estar olhando para eles com o nosso ponteiro.
Poderíamos ter usado
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
Como a indexação de array é implementada com um ponteiro, o mesmo perigo de ultrapassar o array se aplica.
E, como uma string é um array de chars, ponteiros também podem ser usados com strings.
Uma string costuma ser declarada usando a sintaxe de ponteiro, assim
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
Uma pergunta, no entanto. Por que colocamos o asterisco ao lado do identificador? Por que 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;
Então é bom colocar o * perto do identificador para minimizar a ambiguidade.
Ou declarar apenas um identificador por linha.
E, se você ainda não se cansou do *, 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 para print_strings(), a assinatura o descreve como um ponteiro para ponteiro(s) de char.
Então, como sabemos que char ** é realmente um ponteiro para um array de ponteiros para arrays de char, e não um ponteiro para ponteiro de um único char?
Não sabemos.
Essa é 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á sendo usada como pretendido.
Usar uma assinatura diferente não ajuda. Mudar a assinatura para isto
void print_strings(char *passed_in_strings[]) {
// code snipped
}
pode ainda compilar e ainda imprimir lixo.
Mas realmente não há valor em passar um ponteiro para ponteiro de um único char.
Provavelmente, quando você vê char **, ele representa um ponteiro para um array de ponteiros para arrays de char.
Já vimos um ponteiro sendo passado para uma função. Mas por que passar um ponteiro para uma função?
Em C, os argumentos são passados para funções por valor. Isso significa que o valor é copiado do argumento e essa cópia é local à função. Quando a função termina, a cópia do valor também termina.
Veja o que acontece quando tentamos mudar 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;
}
Isso acontece porque, embora as variáveis tenham os mesmos nomes, a e b em swap() não são os mesmos a e b de main().
Elas são cópias dos valores de a e b.
Podemos mudar swap para que funcione como pretendemos?
Sim, podemos!
Podemos passar os endereços de a e b como ponteiros.
Mas de que isso adianta, já que a função copia os ponteiros?
Adianta, porque um endereço copiado continua sendo o mesmo endereço.
Ao desreferenciar o endereço, conseguimos acessar o valor original como ele 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;
}
São muitos * em swap agora, mas agora funciona como pretendido.
Uma forma de ler o operador de indireção é "valor em".
Então, *a é o valor em a.
Quão bem isso 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 tanto como um array de char, char my_string[], quanto como um ponteiro para char, char *.
Qual é a diferença entre eles?
Neste código, passamos um argumento char * para describe_me().
O argumento adjective é definido como um array de char e é aceito 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 mudança persistente em adjective, o que demonstra que o array de char definido pode ser aceito 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;
}
Lembra que os ponteiros são poderosos, mas também potencialmente perigosos?
Mas por que não funciona?
Um array é um ponteiro e um ponteiro funcionou com um array de char!
A razão é que, antes, adjective foi 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 chars são escritos em memória estática somente leitura que pertence ao programa inteiro.
Ambos são ponteiros, mas apontam para coisas diferentes.
Tentar mudar o valor de uma string literal é como tentar mudar o valor literal de 5.
Isso não é permitido.
Então, o problema não é realmente o fato de ser um ponteiro.
O problema é para o que ele aponta.