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=№ // 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:
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: Conveter um valor de horas em minutos e segundos. Entrada: O valor de horas. Saída: Valor de horas, minutos e segundos. */ #include <stdio.h> int main(void) { // Declaracoes void converte_horas(int hora, int *p_min, int *p_sec); int hora,minuto,segundo; // Instrucoes printf("Digite o numero de horas: "); scanf("%d",&hora); converte_horas(hora,&minuto,&segundo); printf("\n%d horas equivalem a %d minutos ou %d segundos",hora,minuto,segundo); getch(); return 0; } void converte_horas(int hora, int *p_min, int *p_seg) { // ponteiro p_min e p_seg recebem os endereços de minuto e segundo respctivamente // Declarações // sem declarações locais // Instruções *p_min = hora * 60; *p_seg = hora * 3600; }
Depois de digitado um valor para a variável hora, chama-se o procedimento converte_horas() e se envia o endereço das variáveis minuto e segundo para o procedimento que o recebe como ponteiros (p_min e p_seg). Estes recebem somente o endereço das variáveis minuto e segundo e não o valor informado por seu usuário (conteúdo guardado por estas variáveis. Após essa etapa o programa entrará no escopo ou corpo do procedimento e lá realizara a conversão do valor de horas inicialmente informado pelo usuário do programa. Repare que a conversão foi feita ao conteúdo do valor armazenado no endereço para onde os ponteiros p_mim e p_seg apontam, 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 os valores de minuto e segundo são alterados é 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 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:
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().
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:
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!!