PONTEIROS

 

Ponteiros são variáveis que armazenam endereços de memória, por este motivo, em grande maioria dos casos seu uso é fundamental. Um fato ao qual se deve observar é que quando um ponteiro utiliza endereços de memória, ele sempre utilizará de endereços contínuos desta memória, além disso, o tamanho em bytes de cada tipo de dado escalar existente em C é de valiosa importância, pois a aritmética de ponteiros (assunto visto mais adiante) depende essencialmente desse prévio conhecimento.

 

Declarando um Ponteiro

 

Como abordado anteriormente, um ponteiro consiste em uma variável que guarda endereços de memória, porém além dos tipos básicos de dados encontrados em C, ponteiros podem ser usados para estruturas heterogêneas (struct), arquivos, matrizes, vetores e principalmente para alocação dinâmica. A seguir um exemplo de ponteiro:

 

int *ponteiro;

 

A variável criada neste caso é um ponteiro e serve para guardar o endereço inicial de uma área de memória do tipo inteiro. A declaração para qualquer outro ponteiro com outro tipo de dado, por exemplo um float, deve ser feita da mesma forma: primeiro o tipo de dado, depois o asterisco (operador indireto) e após isso o nome da variável apontadora que será usada durante a execução do programa. Por exemplo:

 

float *exemplo;

 

Operadores Importantes

 

Endereço (&): Operador da Linguagem C que retorna o endereço de memória de uma variável.

 

Indireto (*): Pode ser usado tanto na operação aritmética de multiplicação, como na criação e uso de ponteiros, ou seja, na manipulação de endereços de memória de um computador. Um pequeno exemplo é apresentado a seguir, onde uma variável comum e uma variável ponteiro são utilizadas. Esse pequeno exemplo servirá para que se possa entender melhor a diferença entre um ponteiro e uma variável comum.

 

Exemplo

 

/*

 Sintese

   Objetivo: Mostrar a diferença entre ponteiro e uma variável comum

   Entrada: Uma variável comum e uma variável ponteiro

   Saida: Endereços de memória e valores numéricos inteiros

*/

#include <stdio.h>

#include <conio.h>

int main(void)
{

// Declarações

   int *ponteiro = NULL;  // também pode ser iniciado com zero

   int numero;

// Instruções

   printf("\nDigite um valor: ");

   scanf("%d",&numero);

   printf("\nResultado da variavel comum: %d",numero);

   printf("\nEndereco da variavel: %x",&numero); // por não se tratar de ponteiro 
                            // para apresentar o endereço se necessita do operador 
                            //endereço(&) para mostrar o endereço

   ponteiro=&numero; // passagem de endereço para um ponteiro

   printf("\n\nValor do ponteiro: %d",*ponteiro); //apresentação do conteúdo do
                                                  // ponteiro

   printf("\nEndereco do ponteiro: %x",ponteiro); //apresentação do endereço 
                                                  //armazenado

   *ponteiro=50; // Novo valor armazenado ao conteúdo do ponteiro

   printf("\n\nNovo valor do ponteiro: %d",*ponteiro); //Apresentação do novo 
                                                       //conteúdo do ponteiro

   printf("\nMesmo endereco: %x",ponteiro);

   getch();

   return 0;

}

 

Neste pequeno exemplo se tenta explicar, de forma simples, as diferenças entre variável ponteiro e variável comum.

 

Após a declaração destas variáveis se pede para digitar um valor inteiro. Feito isso o valor digitado passa a estar armazenado na variável denominada numero e é apresentado o valor informado pelo usuário e seu endereço de memória em hexadecimal, porém, para mostrar esse endereço a instrução printf deve utilizar o operador de endereço de memória (&) antes da variável que se deseja conhecer o endereço (neste exemplo a variável numero). Atribui-se então este endereço a variável ponteiro denominada ponteiro.

 

Observe que esta variável ponteiro também passa pelo mesmo tipo de apresentação, ou seja, é mostrado em sequência o seu conteúdo em hexadecimal, pois um ponteiro guarda somente endereços de memória, e o conteúdo para onde ela aponta (conteúdo do endereço de memória guardado pelo próprio ponteiro).

 

O endereço da variável numero e o conteúdo da variável ponteiro são idênticos, resultando na igualdade do conteúdo da variável comum numero e do conteúdo apontado pela variável ponteiro. Isso ocorre porque os dois estão apontando para o mesmo endereço de memória, ou seja, a variável numero e a variável ponteiro apontam para o mesmo local, que é mostrado ao final o seu valor sem o operador de endereço (&), mas com o uso do operador de acesso indireto (*) por se tratar de um ponteiro.

 

Ao final deste exemplo é armazenado um novo valor como conteúdo do endereço apontado por este ponteiro, resultando na alteração do conteúdo da variável numero com acesso indireto realizado pelo ponteiro. Acontece ainda a nova apresentação do novo valor armazenado com uso do ponteiro, sendo o programa encerrado em seguida. Verifique atentamente que esse valor está no mesmo endereço onde as duas variáveis manipularam valores, uma diretamente pela variável numero e outra indiretamente pela variável ponteiro.

 

Importante: Sempre que necessário é importante inicializar um ponteiro com a macro NULL ou com o valor inteiro zero, pois assim o ponteiro não estará apontando para nenhum endereço de memória, enquanto este não estiver sendo utilizado.

 

Essa inicialização garante que um possível lixo de memória não seja confundido com um endereço válido na memória do computador.

 

 

 

 

 

 

 

 

Imagine o programa exemplificado acima, porém modificando apenas o endereço de memória, onde alguns dados são importantes:

 
  • - existem duas variáveis chamadas numero e ponteiro, todas as duas são do tipo inteiro e portanto manipulam dois bytes na memória do computador;
  • - endereço inicial modificado (para melhor entendimento) é 100 , sendo 10 na variável numero;
  • - na segunda parte do programa, após a apresentação do endereço e valor da variável numero, respectivamente 100 e 10, é atribuído o endereço desta variável (numero) para outra variável denominada ponteiro e que armazena somente o endereço de memória em seu endereço próprio de armazenamento
  • - acionando essa variável (ponteiro) para descobrir seu conteúdo, esse ponteiro pega o endereço de memória armazenado na sua variável ( 100 )
  • - após isso então chega-se a terceira etapa, a modificação de valor das variáveis (tanto numero como ponteiro, pois estão apontando para o mesmo endereço), porém a modificação é feita a partir do conteúdo do ponteiro, nesse caso ocorre o mesmo procedimento já descrito, onde o ponteiro se encaminha até o endereço contido na sua variável e após isso apenas ocorre um novo passo para a atualização de seu valor que passa de 10 para 50 , terminando assim o programa.
 

Exemplo:

 

 

 

 

Suponha que o programa exemplificado esteja procurando por seus colegas entre milhões de pessoas que se encontram em um evento e que por descuido você também acabou esquecendo seu telefone celular em casa. Assim, você teria que começar a procurar seus amigos em todos os locais do evento até o momento em que encontrasse cada um deles. No entanto, como você pode perceber seria muito trabalhosa e lenta essa maneira de procurar por seus amigos, porém se você tivesse alguma referência de onde encontrá-los, este trabalho de procura se tornaria mais ágil e eficiente .

 

Os ponteiros agem de forma similar, pois ainda analisando o exemplo anterior, quando se criou um ponteiro e se passou um endereço de memória, está sendo passada uma referência para um conteúdo desejado (variável numero) para que o programa não tenha que ficar procurando por todos os endereços possíveis de memória até encontrar o valor desejado. Este processamento demoraria muito e tornaria este programa muito lento e provavelmente ineficiente para as mais variadas aplicações que poderiam utilizá-lo. Por esse motivo este assunto é um dos conteúdos mais importantes estudados na programação de computadores com a Linguagem C.

 

Exemplo:

 

/*

 Síntese

   Objetivo: Acrescentar 10 reais a um salário digitado. 

   Entrada: Um salário de uma pessoa.

   Saída: Salário acrescido dos 10 reais.

*/

#include <stdio.h>

#include <conio.h>

int main(void)
{

 // Declaracoes

    float salario;

    void retornaValor(float *p_sal);

 // Instrucoes

    printf("\n\nDigite um salario: ");

    scanf("%f",&salario);

    retornaValor(&salario);

    printf("\n\nNovo salario: %.2f",salario);

    getch();

    return 0;

}

void retornaValor(float *p_sal)
{  // ponteiro  p_sal recebe endereco de salario

 // Declarações

     // sem declarações locais

 // Instruções

    *p_sal = *p_sal + 10;   // conteúdo apontado por p_sal recebe mais 10

}

 

Depois de digitado um valor para o salário, chama-se o procedimento retornaValor() e se envia o endereço da variável salario para o procedimento que o recebe como um ponteiro (p_sal). Este recebe somente o endereço da variável salario e não o valor informado por seu usuário (conteúdo guardado por esta variável). Após essa etapa o programa entrará no escopo ou corpo do procedimento e lá acrescentará dez reais ao salário inicialmente informado pelo usuário do programa. Repare que a adição foi feita ao conteúdo do valor armazenado no endereço para onde o ponteiro p_sal aponta, mantendo-se o valor armazenado no mesmo endereço de memória.

 

Observe que o procedimento encerra e não existe retorno para a função main() que o acionou. No entanto o valor acrescido de 10 é apresentado também na função main() antes de seu encerramento. Isso acontece porque o valor do conteúdo foi alterado no mesmo endereço de memória da variável salario criada na função principal.

 

 

 

 

Aritmética de ponteiros

 

O emprego correto de ponteiros propicia a utilização eficiente de sua aritmética que consiste somente na operação de subtração e adição dos endereços de memória em um computador. Estas operações sobre variáveis comuns adicionam ou subtraem valores nos conteúdos armazenados nos endereços de memória, enquanto que em ponteiros essa adição ou subtração é feita a partir dos endereços de memória. Essa aritmética é totalmente dependente dos tipos de dados envolvidos, pois cada tipo de dado possui um tamanho específico em bytes que são alocados (reservados) pelo computador para execução correta do programa desejado no início de sua execução.

 

Um fato importante a ressaltar é que a subtração ou adição que será realizada não afetará o valor guardado no endereço apontado pelo ponteiro, mas sim na alteração do endereço que o ponteiro está armazenando, ou seja, para o endereço de memória que o ponteiro está apontando. O diagrama a seguir exemplifica o funcionamento da aritmética de ponteiros para o programa anterior usado como exemplo de uso de ponteiro.

 

 

 

A imagem acima demonstra um exemplo do programa apresentado logo abaixo. Neste programa existe um vetor com três posições do tipo float ( salario ) e com uma variável comum denominada aux , que servirá para mudar a posição do vetor. Após o usuário informar três valores, como sugerido no diagrama ( 5 , 25 e 30 ), aciona-se a função retornaMedia(). Depois deste acionamento é criado um ponteiro como parâmetro ( p_sal ) da função para receber o endereço inicial do vetor salario. Em seguida é declarado um contador, chamado conta, do tipo inteiro e uma variável float denominada media também é declarada localmente nesta função.

 

Após essa declaração o bloco de instruções desta função realiza a aritmética de ponteiros, que ocorre da seguinte forma:

 
  • 1. o ponteiro p_sal é acrescido de um valor sequencial de cada vez, sendo estes: 0, 1 e 2;
  • 2. cada um destes acréscimos alterarão o endereço de memória armazenado pelo ponteiro de acordo com o tamanho em bytes do tipo de dado que está sendo apontado pelo ponteiro que deve ser idêntico ao tipo de dado da variável ou estrutura apontada pelo mesmo. Neste exemplo a estrutura é float e consequentemente cada valor acrescido ao ponteiro de um em um alterará o endereço armazenado pelo ponteiro de 4 em 4 bytes.
 

Acompanhe atentamente a especificação detalhada de cada valor acrescido no ponteiro iniciado pelo envio do parâmetro da função principal para a função retornaMedia().

 
  • • Primeiro caso : *( p_sal + 0 ) - será feita a soma de zero ao endereço armazenado por p_sal, resultando no próprio valor de p_sal, pois a soma de zero não modifica o valor do ponteiro. Esta adição no ponteiro manterá o valor armazenando o endereço de memória 100, sendo posteriormente acessado seu conteúdo ( 5) para ser acrescido a variável local e comum denominada media no corpo da função retornaMedia().
  • • Segundo caso : *( p_sal + 1 ) - depois de feita essa soma o ponteiro se deslocará para o endereço 104, pois seu endereço inicial era 100 e foi somado mais um, ou seja, o ponteiro foi deslocado para o próximo conteúdo do tipo float consecutivo ao endereço inicial. Como cada valor float ocupa 4 bytes de memória o próximo valor float consecutivo ao endereço inicial será 104 (100 + 4 bytes = endereço 104). Assim, o próximo conteúdo apontado pelo ponteiro p_sal será 25, que está armazenado a partir do endereço de memória 104, sendo posteriormente somado com o valor armazenado na variável comum e local denominada media.
  • • Terceiro caso : *( p_sal + 2 ) – esta soma ao ponteiro deslocará o mesmo para o endereço de memória 108 , deslocando duas posições a partir do endereço inicial de p_sal ( 100 ), pois salta na memória 2 valores float, ou seja, 8 bytes depois deste endereço inicial (100 + 8 bytes = endereço 108). Após este deslocamento é acrescido o valor contido ( 30 ) neste novo endereço (108) a variável media e a repetição existente nesta função é encerrada com sua posterior divisão por MAX (quantidade de notas lidas do usuário) e retornado seu valor final para função principal.
 

Observe com atenção que o valor inicial do ponteiro não foi alterado em momento algum, apesar de seu deslocamento acontecer enquanto o valor da média final prosseguia sendo apurada por esta repetição for contida no corpo da função retornaMedia().

 

Por meio da aritmética de ponteiros, empregada na função retornaMedia(), foi possível acessar todos os valores contidos no vetor salario, declarado na função principal, chegando-se a média envolvendo todos os salários informadas pelo usuário dentro da função retornaMedia(). No exemplo, sugerido pela figura anterior, a média final seria 20, que retornaria para função principal para ser apresentada ao usuário antes do encerramento do programa.

 

Exemplo:

 

/*

 Síntese

   Objetivo: Fazer a média salarial de 3 pessoas. 

   Entrada: Trê
s salários.

   Saída: Média salarial.

*/ 

#include <stdio.h> 

#include <conio.h> 

#define MAX 3  // definição de quantos salários no cálculo da  média 

int main(void) 
{ 

 // Declaracoes

   float salario[MAX];

   int aux;

   float retornaMedia(float *p_sal);

 // Instruções

   for(aux=0;aux < MAX;aux++)  {

      printf("Digite o salario %d: ",aux+1);

      scanf("%f",&salario[aux]);   // leitura de 3 valores um por um

   }

   printf("\n\nMedia dos salarios: %3.2f", retornaMedia(salario));

   getch();

   return 0;

}

float retornaMedia(float *p_sal)

{

 // Declarações

   int conta;

   float media = 0;

 // Instruções

   for(conta=0; conta < MAX; conta++)

        media = *(p_sal+conta) + media;  // aritmética de ponteiros (p_sal+conta)

   media=media / MAX;

   return media;

}

 

Neste caso o programa serve para calcular a média salarial de três pessoas, onde serão informados três salários para depois ser acionada a função retornaMedia(). Esta função executará todas as ações em seu escopo e retornará um valor float final (média calculada). Repare que a passagem de parâmetro com o endereço inicial de um vetor para um subprograma (função ou procedimento) não requer o operador de endereço antecedendo o nome da variável vetor, pois somente o nome do vetor também corresponde ao endereço de sua posição inicial, ou seja, o endereço da posição zero do vetor salario é igual &salario[0]. Portanto, o acionamento desta função pode ser realizado de duas formas diferentes:

 
  • • retornaMedia(salario);
  • • retornaMedia(&salario[0]).
 

Nos dois casos a forma de acionamento está correta por se tratar de um vetor que foi lido na função principal e está passando como parâmetro todos os dados armazenados através do endereço inicial para no corpo da função ser efetuado os respectivos cálculos desejados usando a aritmética de ponteiros. Isso pode ser acompanhado na instrução media=*( p_sal+conta )+ media, onde a variável media recebe o conteúdo apontado pelo ponteiro de salário mais o valor já armazenado em media. Esta somatória acontece porque o ponteiro do salário é acrescido de um valor sequencial que o desloca para o próximo acesso contínuo condizente com o tipo de dado que se esteja manipulando pelo ponteiro envolvido. Neste exemplo p_sal é float e será deslocado 4 bytes para o somatório de cada valor sequencial atribuído a variável inteira conta , como foi explicado anteriormente.

 

Somente após todas essas operações serem executadas é que a função retornaMedia() é encerrada e a função principal termina com a apresentação da média calculada pela função retornaMedia().

 

 

 

 

 

Atividade de Fixação

 

No intuito de fixar a aprendizagem iniciada por meio deste módulo e verificar como está sua compreensão sobre o mesmo, são sugeridos alguns exercícios de fixação para serem resolvidos. Clique no link ao lado de exercícios, pois será por meio dele iniciada a lista de exercícios sobre os conteúdos estudados até este momento. Boa revisão sobre os mesmos!!