Olá, caro leitor! Este artigo tem o intuito de detalhar um recurso muito importante da linguagem C, o ponteiro! Veremos que a definição geral é simples de compreender, porém a utilização incorreta deste recurso pode ocasionar problemas muito graves para uma aplicação.
Afinal, o que é um ponteiro?
Em poucas palavras a definição de um ponteiro é bem simples: um ponteiro é uma variável que contém um endereço de memória [1]. Esta definição é simples, mas o entendimento correto acerca da utilização deste recurso começa pela compreensão de como a memória de um programa é organizada, ou ainda, o que é um endereço de memória.
Memória
Grosso modo, uma memória pode ser definida como um conjunto de elementos que armazenam informação. Esses elementos são chamados de palavras, sendo cada palavra identificada de maneira unívoca a partir de um endereço, isto é, um endereço de memória! Uma característica da palavra é a sua capacidade de armazenar informação, isto é, a quantidade de bytes que a palavra representa. A memória é ilustrada na Figura 1.
Agora sabemos que uma memória é composta por palavras e que toda palavra possui um endereço único, assim sendo, é importante saber que um programa é um conjunto de informações armazenadas na memória, logo o próximo passo é entender como um programa é organizado na memória.
Organização de um programa na memória
Este tópico é importante para sabermos o que acontece quando um programa em C é compilado. Um programa é um conjunto de informações armazenadas na memória, desta maneira podemos dividir um programa em duas categorias: instruções e dados. As instruções são as operações (o programa propriamente dito) realizadas pela máquina, já os dados são as variáveis, ou valores, processados nessas operações.
Embora o programa seja composto por instruções e dados, tem-se, conceitualmente, quatro regiões de memória [1]. Abaixo segue uma breve descrição de cada segmento:
- Stack – A Stack (pilha) é uma região dinâmica, isto é, varia conforme a execução do programa. É utilizada para armazenar o endereço nas chamadas de funções e interrupções, passagem de parâmetros para funções, armazenar variáveis locais ou pode ser manipulada para armazenar dados em determinada operação;
- Heap – A região Heap também é dinâmica, porém difere da pilha. Essa região é considerada livre e é utilizada por mecanismos de alocação dinâmica de memória;
- Dados – A região de dados corresponde à área onde as variáveis globais e estáticas são armazenadas;
- Programa – Essa região armazena as instruções do programa.
Variáveis e endereço de variáveis
Vimos que tudo que é posto em memória possui um endereço e que a definição de um ponteiro é: uma variável que contém um endereço de memória. Deste modo, se um ponteiro armazena o endereço de outra variável, então temos a relação de que uma variável aponta para outra.
Na Figura 2, é ilustrado uma memória sendo que um dos seus elementos é um ponteiro e outro é uma variável comum. O conteúdo do ponteiro é um endereço, portanto a seta indica a relação descrita anteriormente. Adiante veremos que utilizando ponteiros podemos realizar o acesso indireto a outras variáveis, isto é, podemos ler ou alterar o conteúdo de uma variável sem utilizar o nome desta variável.
Declaração de ponteiro
Para declarar uma variável como um ponteiro deve-se utilizar o símbolo ‘*’ entre o tipo e o nome da variável. A forma geral da declaração é:
Tipo_da_variável * Nome_da_Variável;
Uma vez que o ponteiro realiza o acesso indireto a uma variável, devemos ter o mesmo tipo de ponteiro e variável apontada. Esta relação não é uma regra, pois um ponteiro pode armazenar um endereço independente do tipo de variável armazenada no endereço especificado, contudo veremos no próximo artigo que o tipo do ponteiro é importante para o que é chamado de aritmética de ponteiros.
Operadores
Ao trabalhar com ponteiros fazemos o uso de dois operadores, são eles:
- o operador de indireção ‘*’;
- o operador unário ‘&’ para obter o endereço de uma variável (também chamado de ponteiro constante, pois representa um endereço de memória fixo).
Para exemplificar, considere o trecho de código abaixo. Temos a definição de uma variável inteira e de um ponteiro para o tipo de dados inteiro.
int varX = 10; int * pt_varX; pt_varX = &varX; /*atribui o endereço de varX para o ponteiro pt_varX */
A Figura 3 ilustra a indireção simples mostrada no código acima.
O conteúdo do ponteiro foi determinado na quarta linha, logo o mesmo armazena o endereço da variável varX. Para acessar o conteúdo do endereço armazenado no ponteiro basta utilizar o operador ‘*’ antes do nome do ponteiro.
*pt_varX
Considere o caso abaixo em que o valor da variável varX é alterado a partir do ponteiro. Essa situação é ilustrada na Figura 4.
*pt_varX = 20;
Observe que o conteúdo de varX foi alterado de forma indireta, isto é, sem fazer referência ao nome da variável varX. Cabe ressaltar que esse ponteiro é chamado de ponteiro variável, pois possibilita armazenar qualquer endereço.
O conceito de NULL
Ao declarar uma variável sem atribuir um valor direto, o valor armazenado nela vai depender do contexto onde a variável foi declarada, por exemplo, uma variável local de uma função. A não inicialização de um ponteiro e posteriormente a sua utilização podem trazer sérias consequências para execução do programa, pois ao alterar o seu conteúdo podemos modificar regiões de memória importantes para o controle de execução, por exemplo, o endereço de retorno de uma função armazenado na Stack. Para evitar problemas é possível utilizar um valor que determina se um ponteiro não foi inicializado com um endereço válido, esse valor é o NULL.
Geralmente, o valor NULL é definido como:
#define NULL ((void *)0)
Assim, é possível inicializar um ponteiro como NULL e antes de usa-lo pode ser realizado um teste condicional. Por exemplo, a função de alocação dinâmica de memória malloc retorna um ponteiro para o bloco de memória alocado, caso contrário o valor retornado é NULL.
int * pt;
pt = (int *)malloc(10 * sizeof(int));
if(pt != NULL)
{
//operação bem-sucedida
}
else
{
//falha de alocação
}
O conceito de atribuir NULL ao ponteiro é simples, contudo não deve ser compreendido como um ponteiro que aponta para o endereço zero. O NULL tem como função determinar que o ponteiro não aponta para um endereço válido. De fato, isso não impede erros de execução, porém fornece uma alternativa para testar se o ponteiro foi inicializado.
Modificadores de Acesso
Assim como qualquer variável os ponteiros também podem ser declarados utilizando modificadores de acesso.
Declaração sem modificadores: O conteúdo de ‘var’ pode ser alterado utilizando o ponteiro e o ponteiro também pode ser alterado.
int var = 10; int * ptr = &var;
Ponteiro para um valor constante: O conteúdo de ‘var’ não pode ser alterado utilizando o ponteiro, no entanto o ponteiro pode ser alterado.
int var = 10; int const * ptr = &var;
Ponteiro constante: O conteúdo de ‘var’ pode ser alterado utilizando o ponteiro, porém o ponteiro não pode ser alterado.
int var = 10; int * const ptr = &var;
Ponteiro constante para um valor constante: O conteúdo de ‘var’ não pode ser alterado pelo ponteiro e o ponteiro também não pode ser alterado.
int var = 10; int const * const ptr = &var;
Indireção Múltipla
O endereço do ponteiro pode ser obtido da seguinte forma:
&pt_varX
Desse modo, um ponteiro pode armazenar o endereço de outro ponteiro, ocasionando uma indireção múltipla. Para declarar um ponteiro de indireção múltipla utiliza-se N vezes o operador *, sendo N o nível de indireção. No exemplo abaixo é declarado um ponteiro para ponteiro.
int ** ptr2; /*ponteiro para ponteiro do tipo inteiro*/ int * ptr1; /*ponteiro para o tipo inteiro*/ int var = 10; ptr2 = &ptr1; ptr1 = &var; *ptr1 = 30; **ptr2 = 50;
Do código acima:
- &ptr2: Endereço do ponteiro ‘ptr2’;
- &ptr1: Endereço do ponteiro ‘ptr1’;
- ptr2: Conteúdo de ‘ptr2’, ou seja, o endereço de ‘ptr1’.
- ptr1: Conteúdo de ‘ptr1’, ou seja, o endereço de ‘var’.
- *ptr2: Conteúdo do endereço apontado, ou seja, o conteúdo de ‘prt1’.
- *ptr1: Conteúdo do endereço apontado, ou seja, o conteúdo de ‘var’.
- **ptr2: Acessa o conteúdo do endereço armazenado no ponteiro que é referenciado por ‘ptr2’, ou seja, acessa ‘var’ indiretamente.
A Figura 5 ilustra as operações listadas acima.
Conclusão
Nesse artigo foi apresentada a definição geral de ponteiro e algumas características desse recurso poderoso. A capacidade de armazenar endereços e poder manipular, de forma indireta, o conteúdo de variáveis, permite ao desenvolvedor modelar aplicações C com bastante flexibilidade. Cabe ressaltar, novamente, que o uso incorreto pode gerar graves problemas para aplicação.
Com essa introdução outros pontos podem ser explorados: alocação dinâmica de memória, acesso de membros de estruturas, ponteiros para funções, passagem de parâmetros por referência e técnicas de orientação ao objeto. Esses temas serão explorados em outros artigos.
Referências
[1] – Livro: C, completo e total – 3ª edição revista e atualizada. Herbert Schildt. [2] – Livro: Sistemas Digitais – Princípios e Aplicações – 8ª edição. Ronald J. Tocci e Neal S. Widmer.Fonte da imagem destacada: https://listamaze.com/top-10-programming-languages-for-job-security/











Parabéns pelo artigo Fernando, muito bom e educativo. Gostaria apenas de apontar um erro na seção ‘Modificadores de Acesso’ onde no segundo exemplo
int var = 10;
int const * ptr = &var;
você escreve: ‘O conteúdo de ‘var’ não pode ser alterado utilizando o ponteiro e o ponteiro não pode ser alterado.’ Enquanto na verdade o ponteiro pode sim ser alterado. Os exemplos onde ele não pode ser alterado estão corretamente descritos nos dois exemplos seguintes.
Olá, Frederico.
Olhos de águia! Obrigado pelo retorno.