Olá pessoal, tudo certo?!
Depois de uma discussão boa sobre a “estética” do C++ no twitter, resolvi voltar a abordar o tema aqui no blog. Talvez você ainda não saiba, mas comecei uma série “introdutória” para C++ há algum tempo.
No post de hoje, pretendo introduzir um tema “traumático” para quem utiliza C++: ponteiros.
O que é um ponteiro?
Cada posição de memória que usamos para armazenar um valor possui um endereço.
Um ponteiro é uma variável que armazena um endereço em memória que contém dados de um determinado tipo.
Um ponteiro tem um “nome de variável” e também tem um tipo que identifica que “tipo de dado” está armazenado no endereço que possui.
Um ponteiro que armazene o endereço de memória onde está armazenado um dado inteiro, por exemplo, é conhecido como “ponteiro para um inteiro”.
Declarando um ponteiro
A declaração para um ponteiro é parecida com aquela que estamos habituados a fazer para variáveis comuns, exceto que o nome do ponteiro deve ser prefixado com um asterisco. Considere:
#includeint main() { int *pnumber1; int* pnumber2; return 0; }
O pequeno programa acima declara dois ponteiros. Repare que não há diferença entre manter o asterísco “perto” do nome da variável ou do tipo. Qualquer forma que você escolha, será aceita pelo compilador.
É comum encontrarmos ponteiros nomeados começando com um p.
O operador Address-Of
Já sabemos que ponteiros são utilizados para armazenar endereços em memória dos nossos dados. Então, como fazemos para obter o endereço de memória correspondentes aos dados armazenados em uma variável? Simples, usamos o operador “address-of” (&). Observe:
#includeint main() { int number = 10; int* pnumber; pnumber = &number; return 0; }
O que esse pequeno código faz é criar uma variável do tipo inteiro, com o valor 10. Logo depois, atribuímos o “endereço” dessa variável em nosso ponteiro.
Podemos utilizar o operador “address-of” para obter o endereço de qualquer variável (de qualquer tipo). Entretanto, devemos sempre tomar o cuidado de utilizar o tipo apropriado de ponteiro.
Por exemplo, se desejamos armazenar o endereço de uma variável do tipo double, devemos utilizar um ponteiro para double (double *).
Usando o ponteiro
Já sabemos como obter o endereço de uma variável. Também já sabemos como armazenar esse endereço em um ponteiro. Agora, vamos ver como “acessar” o valor que está na posição do ponteiro, para isso, usamos o operador de indireção (*).
Observe:
#includeint main() { int number = 10; int* pnumber; pnumber = &number; std::cout << "Value-Of number variable : " << number << std::endl << "Address-Of number variable: " << pnumber << std::endl << "Value-Of pnumber variable : " << pnumber << std::endl << "Value-Of pnumber pointer : " << *pnumber << std::endl; return 0; }
Executando (na minha máquina):
Lindo!
Entenda que o ponteiro está “apontando” para o valor da variável. Repare:
#includeint main() { int number = 10; int* pnumber; pnumber = &number; *pnumber = 11; std::cout << "Value-Of number variable : " << number << std::endl; return 0; }
Esse código “altera” o valor de number através do ponteiro.
Por que usar ponteiros?
Se você é novo em C++, talvez esteja se perguntando o porquê usar ponteiros? Vamos a algumas justificativas:
- como você verá em seguida, podemos utilizar a “notação” de ponteiros para manipular arrays, o que é frequentemente “mais rápido”;
- ponteiros facilitam o acesso a grandes porções de dados;
- ponteiros permitem a alocação dinâmica de memória;
- se você vem de C#, ponteiros são os “delegates” do C++.
Inicializando ponteiros
Utilizar ponteiros que não foram inicializados é extremamente perigoso. Sem querer, podemos sobrescrever dados em áreas “aleatórias” de memória.
Já sabemos como inicializar um ponteiro para apontar para o “endereço” de uma variável.
Outra boa prática seria inicializar um ponteiro é atribuir nullptr que seria o equivalente a “sem endereço”. Observe:
#includeint main() { int *p1; int *p2(nullptr); if (p1 == nullptr) std::cout << "p1 does not points to anything" << std::endl; if (p2 == nullptr) std::cout << "p2 does not points to anything" << std::endl; return 0; }
A execução desse código deixa evidente que nullptr NÃO é o valor default
Entendido?!
Usando deslocamentos com um ponteiro
Ponteiros nos permitem “navegar” facilmente através de vetores. Considere:
#includeint main() { char* name = "Elemar Jr"; int i = 0; while (name[i] != '\0') std::cout << name[i++] << std::endl; }
O que gostaria de observar:
- quando iniciamos um ponteiro para char (char *) apontando para uma string, estamos, de fato, criando um pequeno “array de chars” que é encerrado pelo caractere nulo (‘’);
- Quando usamos um ponteiro, podemos avançar “posições de memória” utilizando um deslocamento (semelhante a array).
Outra versão para o mesmo código:
#includeint main() { char* name = "Elemar Jr"; int i = 0; while (*name != '\0') std::cout << *(name++) << std::endl; }
O que estou fazendo?! No lugar de usar um “deslocamento”, estou indicando que desejo avançar para a próxima “posição” de memória, compatível com o tipo. Para isso, usei “aritmética de ponteiros”. Entretanto, esse é tema para outro post.
Por hoje, era isso.
Breno Ferreira
22/09/2011
Excelente artigo Elemar. Só acho que voce disse uma analogia meio errada (ao meu ver):
“se você vem de C#, ponteiros são os “delegates” do C++.”
Pelo que eu saiba, delegates são function-pointers, e não ponteiros simples. Isso pode confundir a galera que ler seu post. Acho que uma analogia melhor seria falar que ponteiros são a mesma coisa quando se passa valores utilizando a keyword ref ou out.
Abraços
Breno
elemarjr
22/09/2011
Breno,
Obrigado pelo feedback.
Me referi a ponteiros de função na analogia para os delegates. Há muito mais em ponteiros que qualquer analogia com C# consiga explicar.
Espero que os leitores consigam fazer a distinção.
Alberto Monteiro
23/09/2011
Só completando o que o Elemar disse, ref e out são uma das keywords que tem relação a ponteiros.
Considere:
MinhaClasse minhaClasse = new MinhaClasse(“Alberto”);
Console.WriteLine(minhaClasse.Nome);
MinhaClasse outraClasse = minhaClasse;
outraClasse.Nome = “Breno”;
Console.WriteLine(minhaClasse.Nome);
——————————– Resultado ——————————–
Alberto
Breno
Como podemos ver, nesse código ai alteramos variáveis diferentes, mas que apontavam para o mesmo endereço de memoria(ponteiros lindo)!
A analogia que chegue mais perto creio que seja falar dos objetos dos quais ficam armazenados na HEAP
23/09/2011
Elemar,
Sugiro que em futuros tópicos mais avançados da série seja abordado também informações sobre o C++ moderno e conceitos como smart pointers, lambdas e outros.
Abraços.
elemarjr
23/09/2011
Obrigado pela dica. Chegaremos lá.