VOLPITI - Tecnologia da Informação
 
Busca
Tópicos
  Cadastre-se :: Hospedagem LinuxDicas :: Acessórios LinuxDicas :: Fórum LinuxDicas   

Usuários LinuxDicas
· AvantGO
· Backend XML (RSS)
· Contato/Publicidade
· Enviar Notícias/Artigos
· Fórum LinuxDicas
· Lista de Discussão
· Mensagens Privadas

Casa do Linux


Lista de Discussão
·[linuxdicas] quetão de linux
·Ferramenta de Backup
·redes
·Search Engine

Leia mais...

Artigos LinuxDicas
· Resistência ao Linux
· Guia para o Novato
· Licenciamento do SuSE
· Foca GNU/Linux - Iniciante
· Foca GNU/Linux - Intermediário
· Foca GNU/Linux - Avançadoe
· Curso C
· Outras Seções de Artigos

FAQ LinuxDicas
· Licença
· Servidores
· Ambiente X
· Hardware
· Instalação
· Internet
· Aplicativos
· Sistema
· StarOffice

Notícias Velhas
Segunda, abril 21
· MON - Monitorando a disponibilidade de serviços
Quinta, abril 17
· Últimos dias para Inscrições de Palestras no Flisol Campinas
· Acessando seus arquivos de sua máquina de qualquer computador do mundo
Terça, abril 15
· Artigo sobre o Moregroupware
Segunda, abril 14
· Análise do Mandriva Xtreme2 Pack
Terça, março 18
· Como migrar dados do msaccess para mysql
· A verdadeira razão para usarmos Linux
· usando o rsync 3.0.0 para fazer backup entre linux e xp
· Firefox3 : Como Instalar manualmente.
Sábado, fevereiro 09
· Configurando um DNS CHROOTED Primário e Reverso para o seu site.
Quinta, dezembro 27
· Censurando a internet com o OpenDNS
Domingo, outubro 14
· Bandwidth Monitoring Tools For Linux
Quinta, outubro 11
· Variáveis de ambiente
Quinta, agosto 02
· Rodando processos em múltiplos servidores, Cluster!!!
· Instalando XEN no seu Ubuntu/Debian
Domingo, junho 17
· Vovó, faz backup pra mim?
Quarta, junho 13
· Canetas: Idealismo Antártico
Quinta, junho 07
· CA confiável com certificados digitais
Domingo, junho 03
· Equações Matemáticas via Shell
Sábado, junho 02
· Permanent Link to Garimpar é preciso [2]: Confira seu e-mail

Notícias antigas

Versões Estáveis
· Kernel Linux
· *nix e *BSD ISOs da Unicamp
· Linux ISO
· FreeBSD Releases

Curso de C - 9. Ponteiros

(3240 total de palavras neste texto)
(4629 vizualização(ões))   Imprimir




Curso de C - 9. Ponteiros

Para compreender melhor o que são ponteiros, vamos relembrar a definição de variável. O que é uma variável? É um espaço de memória reservado para guardar alguma coisa, referenciado por um nome. Tá bom, mas que tipo de coisa uma variável guarda? Depende... Uma variável inteira guarda números inteiros, uma variável caractere guarda um caractere, uma variável ponto flutuante armazena números em ponto flutuante. Mas, e os ponteiros? O que são??

Bem, um ponteiro não deixa de ser uma variável... E sendo uma variável, ele guarda alguma coisa. Mas o que um ponteiro armazena?

Um ponteiro é uma variável que armazena o endereço de outra variável. Armazenando o endereço de outra variável, ele pode referenciá-la indiretamente. Isto garante um acesso à variável diferente da forma que vinhamos fazendo até aqui, manipulando a variável diretamente. Quando um ponteiro contém o endereço de outra variável, nós dizemos que ele aponta para essa variável.

9.1. Porque Utilizar Ponteiros?

Há vários motivos para se usar ponteiros. Vamos listar alguns:

  • através de ponteiros, podemos passar os argumentos para uma função por referência, isto é, ao invés de passarmos uma cópia como argumento para a função manipular, passamos o endereço (um ponteiro) da variável argumento, de modo que as alterações que a função fizer neste ponteiro, afetarão a variável;
  • oferece uma forma elegante de passar matrizes e strings como argumentos para funções;
  • os ponteiros são a base para a criação de estruturas de dados mais avançadas, como listas ligadas, pilhas, filas e árvores;
  • a utilização sensata de ponteiros deixa o programa mais rápido;
  • entre outras coisas...

9.2. Declaração de Ponteiros

Para declarar ponteiros, você deve deixar claro para o compilador para qual tipo de dado vai apontar. Portanto, é necessário especificar um tipo. A forma geral da declaração de ponteiros é a seguinte:

  int *ptr;

Este comando declara uma variável ponteiro que aponta para (ou armazena o endereço de) uma variável do tipo int. Para qualquer tipo de dado funciona da mesma forma.

9.3. Os operadores * e &

A linguagem C possui dois operadores unários para a manipulação de ponteiros: o * e o &.

Um nós já conhecemos. O operador & retorna o endereço de memória de uma variável já conhecida. Lembra que nós usamos muito este operador na função scanf()? Ele passava o endereço da variável para a função. A função gravava o dado lido na variável através do endereço passado como argumento.

E o outro operador? O operador indireto faz mais ou menos o inverso de &. Ele serve para obter, ou manipular, o valor da variável apontada por um ponteiro. Sacou? Quando colocamos o * na frente (lado esquerdo) da variável ponteiro, nós estamos dizendo "olha, sr. ponteiro, eu quero o conteúdo da variável que o sr. está apontando". Vamos aos exemplos:


  int *x;		/* ponteiro para inteiro */
  int num;	/* uma variável inteira */
  
  num= 5;
  
  x= #		/* o ponteiro está recebendo o endereço de num (está apontando para num) */
  
  *x++;			/* estamos incrementando de 1 o CONTEÚDO da variável apontada por x (a variável num). */
  
  printf("%d", *x);	/* imprime o CONTEÚDO da variável apontada por x, e não o seu endereço. */

No exemplo acima, declaramos um ponteiro para inteiro (int *x) e uma variável inteira (int num). A variável num recebe 5, depois, o ponteiro x recebe o endereço de num. Logo após, o conteúdo da variável apontada por x é incrementado de 1, e por último, imprimimos o conteúdo da variável apontada por x (no caso, a variável num).

O que devemos gravar em nossa memória é que, quando um ponteiro aparece com um * do lado esquerdo da variável, ele está trabalhando com o conteúdo da variável apontada. Só quando ele aparece sem o * é que refere-se ao endereço.

No exemplo acima, de quebra, já aprendemos a atribuir o endereço de uma variável a um ponteiro, através da instrução:

  x= #	/* o ponteiro x recebe o endereço da variável num */

Agora que já sabemos brincar com os ponteiros, vamos fazer um outro exemplo:

  #include <stdio.h>
  
  /* este programa troca o valor de duas variáveis, utilizando ponteiros */
  
  main()
  {
  	int a, b, aux;
  	int *pa, *pb, *paux;
  	
  	printf("Digite número a: ");
  	scanf("%d", &a);
  	
  	printf("Digite número b: ");
  	scanf("%d", &b);
  	
  	pa= &a;		/* ponteiro pa recebe o endereço de a */
  	pb= &b;		/* ponteiro pb recebe o endereço de b */
  	paux= &aux;	/* ponteiro paux recebe o endereço de aux */
  	
  	 /* agora, a troca */
  	*paux= *pa;	
  	*pa= *pb;
  	*pb= *paux;
  	
  	printf("a: %d, b: %d", *pa, *pb);
  	return 0;
  }

Como vocês viram, usamos os ponteiros, e não as variáveis originais.

Mais um exemplo:

  int *p;
  int num, n;
  
  n= 10;
  num= 15;
  
  p= &n;
  
  *p= *p + num;
  
  printf("%d", *p);

O valor impresso será 25.

9.3. Atribuição entre Ponteiros

Quando ocorre um comando de atribuição entre dois ponteiros, um ponteiro recebe o endereço que o outro armazena. Em outras palavras, ele passa a apontar para o mesmo dado. Exemplo:

  int *p, *q;
  int num= 10;
  
  p= &num;	/* p passa a apontar para a variável num */
  
  q= p;	/* o ponteiro q recebe o endereço armazenado em p, ou seja, q passa a apontar para a mesma variável que p */
  
  printf("%d, %d", *p, *q);

Neste exemplo será impresso duas vezes o valor da variável num, que é apontada tanto por p quanto por q.

9.4. Aritmética de Ponteiros

Podemos usar as operações aritméticas de adição e subtração para manipular ponteiros. Como você sabe, a memória do computador é linear, ou seja, é como se fosse uma reta: os dados são armazenados lado a lado, cada um ocupando o espaço correspondente ao seu tipo. Vamos considerar um int, que possui 4 bytes no Linux (no DOS um int tem 2 bytes). Suponhamos que uma variável inteira é guardada no endereço 1000 hexadecimal; o próximo inteiro seria guardado no endereço 1004, depois deste o próximo seria armazenado no endereço 1008, e assim por diante (no DOS seria 1000, depois 1002, 1004, ...).

Vamos utilizar um exemplo:

  int num;
  int *p;
  
  p= &num;
  
  p++;

Vamos supor que a variável num está armazenada no endereço 1000. O valor que p armazena, portanto, é 1000. Mas que raios a variável p faz em p++?? Nós chamamos isto de aritmética de ponteiros. Quando um ponteiro é incrementado de 1, ele passa a apontar para o próximo dado do seu tipo na memória. Isto significa que se ele armazenava o endereço 1000, depois de p++ ele passa a ter o endereço 1004, ou o próximo inteiro na memória. Não é a variável num que foi incrementada, e sim o endereço armazenado no ponteiro p.

Entenderam? Cada vez que um ponteiro é incrementado, ele apontará para o próximo dado do seu tipo na memória, seja qual tipo for. E se, ao invés de fazer p++, eu utilizar um comando assim:

  p= p + 3;

Neste caso, se p armazenava o endereço 1000, agora ele passa a armazenar o endereço 1012, ou passa a apontar para o terceiro inteiro após num na memória. Isto com um inteiro, pois se fosse um double:

  double fp;
  double *p;
  
  p= &fp;
  
  p= p + 2;

Se p aqui recebesse o endereço 1000 da variável fp, na expressão p= p + 2 ele passaria a ter o endereço 1016, ou seja, apontaria para o segundo double depois de fp na memória.

Com a subtração é a mesma coisa. Considerando o exemplo anterior, do double:

  double fp;
  double *p;
  
  p= &fp;
  
  p= p - 2;

Se o p tivesse recebido o endereço 1000 de fp, após p= p - 2 o ponteiro p teria o endereço 984, ou seja, apontaria para os dois doubles antes de fp na memória.

  OBS: Somente as operações de adição e subtração são permitidas na aritmética de ponteiros. 
  Nem multiplicação, nem divisão, ou nenhuma outra operação aritmética é permitida.

Você pode pensar: "mas isso é perigoso!". Com certeza!!! A aritmética de ponteiros é uma operação que devemos utilizar com cautela, pois você pode facilmente "invadir" algum espaço de memória que faz parte de outra aplicação.

Lembra da aula sobre estruturas de repetição, onde nós vimos um exemplo do laço for para a eliminação de espaços em strings?

  for ( ; *str == ' '; str++);

O comando str++ é uma operação de aritmética de ponteiros. Lembra que o nome de uma matriz ou string é um ponteiro para o seu início?

A aritmética de ponteiros é bastante usada para percorrer matrizes, o que garante um desempenho superior ao método convencional. Muitos autores, porém, criticam essa prática, pois dizem que torna o código menos legível e menos seguro.

9.5. Comparação de Ponteiros

Os ponteiros podem ser comparados com a utilização de operadores relacionais. Por exemplo, para testarmos se dois ponteiros apontam para a mesma variável basta fazer assim:

  if (p == q)					/* supondo que p e q são ponteiros */
  	printf("p e q apontam para a mesma variável");
  else
  	printf("p e q não apontam para a mesma variável");

Esta é a operação de comparação mais usual entre ponteiros. Outra comparação bastante usual é a seguinte:

  if (p != NULL)

O comando acima testa se o ponteiro é diferente de NULL. NULL em C é uma constante que significa que o ponteiro não aponta para endereço nenhum (nulo). Este tipo de comparação é bastante comum em listas ligadas, para verificar se a lista possui algum dado.

OBS: Um ponteiro será maior que o outro quando armazenar um endereço mais alto de memória.

9.6. Ponteiros e Matrizes

Como já dissemos, o nome de uma matriz sozinho é um ponteiro que aponta para o início da matriz. Portanto, ponteiros e matrizes tem uma relação bastante estreita. Podemos acessar os índices de uma matriz somente usando aritmética de ponteiros. Para ficar claro, vejam estes dois exemplos, que imprime o conteúdo da matriz através da forma convencional, e depois através da aritmética de ponteiros:

  #include <stdio.h>
  
  main()
  {
  	int mat[]= { 2, 4, 6, 8, 10 };
  	int i;
  	
  	for (i=0; i<5; i++)
  		printf("%d ", mat[i]);
  	
  	return 0;
  }

Agora, com aritmética de ponteiros:

  #include <stdio.h>
  
  main()
  {
  	int mat[]= { 2, 4, 6, 8, 10 };
  	int i;
  	
  	for (i=0; i<5; i++)
  		printf("%d ", *(mat+i));
  	
  	return 0;
  }

No segundo exemplo, a expressão *(mat+i) faz a mesma coisa que mat[i], só que através da aritmética de ponteiros. O asterisco faz com que seja impresso o valor, não o endereço.

Não podemos alterar diretamente o conteúdo do ponteiro mat, pois quando declaramos uma matriz, o seu nome é um ponteiro constante. Não podemos utilizar a expressão mat++, pois isto causaria um erro.

9.7. Matrizes de Ponteiros

Em alguns casos, quando precisamos de muitos ponteiros, é mais conveniente declarar uma matriz de ponteiros. Como qualquer outro tipo de dado, os ponteiros podem também ser organizados desta forma. Declaração de uma matriz de ponteiros:

  int *pmat[10];

Declaramos uma matriz de 10 ponteiros para variáveis inteiras. Para atribuir endereços de variáveis, faça assim:

  pmat[2]= &num;	/* O terceiro ponteiro da matriz passa a apontar para a variável num */

Para saber o conteúdo da variável apontada por pmat[2], basta fazer assim:

  printf("%d", *pmat[2]);

O nome desta matriz sozinho, ao invés de ser um simples ponteiro, é um ponteiro para ponteiro, e pode ser declarado assim:

  int **p;     /* um ponteiro que aponta para um ponteiro que aponta para uma variável inteira */

Está confuso?? Calma... Isto é chamado de indireção múltipla, e estudaremos a seguir.

9.8. Indireção Múltipla

Essa é para pirar de vez... Em C podemos ter ponteiros apontando para ponteiros, estes por sua vez apontando para variáveis normais. Isto é conhecido como indireção múltipla, e pode causar bastante confusão. Para ilustrar, veja abaixo:

  
  	 ponteiro      			variável
  	(endereço)---------------------->(valor)	INDIREÇÃO SIMPLES
  	
  	
  	 ponteiro	 ponteiro	 variável
  	(endereço)----->(endereço)------->(valor)	INDIREÇÃO MÚLTIPLA

Podemos criar indireções que vão bem além disso, com mais de duas dimensões. Porém, isso raramente é necessário, além de ser grande a chance de aparecerem erros difíceis de diagnosticar.

A declaração de um ponteiro para ponteiro é assim:

  int **p;	/* p aponta para um ponteiro que aponta para uma variável inteira */

Como você viu, basta colocar um * adicional para criar um ponteiro para ponteiro. Agora vamos ver um exemplo:

  #include <stdio.h>

  
  main()
  {
  	int *p;
  	int **pp;
  	int n;
  	
  	printf("Digite um número: ");
  	scanf("%d", &n);
  	
  	p= &n;
  	
  	pp= &p;		/* pp recebe o endereço do ponteiro p */
  	
  	printf("%d", **pp);	/* imprime o valor de n */
  	return 0;
  }

Neste exemplo o ponteiro p recebe o endereço da variável n; depois, pp recebe o endereço do ponteiro p. Quando imprimimos **pp, é impresso o valor da variável apontada pelo ponteiro que pp aponta... Ufa!! Que confusão!!

9.9. Funções de Alocação Dinâmica

Através dos ponteiros podemos utilizar as funções de alocação dinâmica de memória da linguagem C. Alocação dinâmica é a forma de obter memória durante a execução do programa. Nós sabemos que as variáveis são um meio de usar a memória para armazenar nossos dados, mas temos que definir de antemão as variáveis que o programa utilizará. Através da alocação dinâmica, você só aloca a memória quando for necessário, e a libera quando não precisar mais. Isto é uma vantagem em muitas situações. Por exemplo, no caso de um programa que utilize uma matriz grande o suficiente para armazenar dados que vão sendo lidos. Muitas vezes o espaço alocado pela declaração de uma matriz passa longe de ser todo utilizado, o que resulta em um mau aproveitamento da memória. Este é um caso onde a alocação dinâmica se encaixa perfeitamente, pois você só aloca memória para um dado se precisar.

As funções de alocação dinâmica alocam espaço do heap, que é a parte da memória que está entre a área de armazenamento permanente e a pilha, e geralmente possui um espaço razoavelmente grande de memória livre.

Não estudaremos nesta aula técnicas de listas ligadas, pilhas e filas, que são estruturas que se beneficiam da alocação dinâmica. Vamos apenas apresentar as funções responsáveis pela alocação da memória.

A seguir as funções de alocação dinâmica de C:


malloc - A função malloc é a principal função de alocação de memória em C. Seu protótipo é: void *malloc(size_t num_bytes); Ela possui apenas um argumento que é o número de bytes que será alocado da memória (size_t é um inteiro sem sinal), e retorna um ponteiro do tipo void, o que significa que você pode atribuir este retorno a qualquer tipo de ponteiro. Vejamos um exemplo:

  int *p;		/* declaramos um ponteiro para inteiro */
  
  /* agora alocaremos um inteiro através de malloc, e retornaremos o endereço deste inteiro para p */
  p= (int *) malloc(sizeof(int));		

Como malloc retorna um ponteiro do tipo void, devemos converter o tipo através de (int *), pois o ponteiro p aponta para um inteiro. O número de bytes é fornecido através de sizeof(int).

Mais um exemplo. Agora vamos alocar um double:

  double *p;
  
  p= (double *) malloc(sizeof(double));	


calloc - A função calloc é bastante parecida com malloc. Sua única diferença é que ela possui um argumento a mais que especifica quantos espaços do tipo especificado ela alocará. Ou seja, a grosso modo, ela é usada para alocar uma matriz dinamicamente. Exemplo:

  int *p;
  
  p= (int *) calloc(50, sizeof(int));

Neste exemplo é alocada uma matriz de 50 inteiros, e o endereço do início desta matriz é atribuído ao ponteiro p. Você pode acessar os elementos através dos índices ou através de aritmética de ponteiros.

OBS: a função malloc também pode ser usada para alocar matrizes dinamicamente. Basta fazer o seguinte:

  p= (int *) malloc(50 * sizeof(int));

Este comando faz a mesma coisa que o calloc do exemplo anterior.


realloc - Esta função é usada para modificar o tamanho da memória previamente alocada. Suponhamos que você alocou uma matriz de 30 elementos com malloc, e depois precisa aumentar o tamanho desta matriz para 50 elementos; neste caso você utiliza realloc. realloc possui dois argumentos: o ponteiro para a região de memória que será redimensionada e o tamanho em bytes da nova alocação. Exemplo:

  int *p;
  
  p= (int *) malloc(30 * sizeof(int));	/* alocamos uma matriz de 30 elementos */
  
  p= (int *) realloc(p, 50 * sizeof(int));  /* redimensionamos para 50 elementos */

Todas as funções de alocação (malloc, calloc e realloc) retornam NULL caso não consigam alocar memória. Nesse caso, podemos testar se a memória foi alocada ou se a alocação falhou através de um if:

  int *p;	
  
  p= (int *) malloc(sizeof(int));	
  
  if (!p)		/* se o p for NULL */
  {
  	printf("Falha ao alocar memória!");
  	exit(1);
  }
  else
  	printf("Memória alocada com sucesso!");
  		

Agora que já sabemos alocar, vamos aprender desalocar.


free - A função free é a responsável por desalocar a memória previamente alocada por malloc, calloc ou realloc. Ela possui apenas um argumento, o ponteiro para a região de memória que será liberada. Exemplo:

  float *p;
  
  p= (float *) malloc(sizeof(float));	/* alocamos um float */
  
  free(p);	/* desalocamos o float alocado por malloc */

Simples assim...

OBS: as funções malloc, calloc, realloc e free estão definidas no cabeçalho stdlib.h, que deve ser incluído no início do programa com a diretiva #include.

9.10. Problemas com Ponteiros

Ponteiros são maravilhas no que trazem de recursos para o programador, mas podem dar muita dor de cabeça, com problemas difíceis de detectar. A principal causa de problemas é tentar utilizar um ponteiro que ainda não foi inicializado. Por exemplo:

  int *p;
  int n;
  
  n= 20;
  *p= x;

Nós declaramos o ponteiro p, mas ele não aponta para ninguém ainda. O erro está na atribuição *p= x, pois estamos tentando atribuir um valor a uma área de memória desconhecida. O correto seria primeiro atribuir o endereço de uma variável ao ponteiro p ou alocar memória dinamicamente, para depois realizar esta atribuição.

Muitas vezes atribuímos um valor a um ponteiro como se ele fosse uma variável comum. Por exemplo:

  int *p;
  int n= 30;
  
  p= n;

Neste exemplo p recebe o valor, não o endereço, de n. O certo seria: p= &n;

Estes são apenas dois tipos de erros bastante comuns com ponteiros. Devemos sempre tomar o cuidado de não utilizar ponteiros que não foram inicializados ainda. Outro bom costume é inicializar com NULL os ponteiros que ainda não foram usados, pois isso ajuda a construir estruturas para evitar erros banais.


Exercícios

1) Refaça o programa da calculadora que fizemos em outra aula utilizando ponteiros.

2) Faça um programa que leia 10 números inteiros e armazene em uma matriz. Depois, utilize a aritmética de ponteiros para imprimir todos os elementos da matriz.

3) Faça um programa que aloque uma string de 30 caracteres, armazene uma palavra digitada pelo usuário nesta string, imprima na tela e, depois de imprimido, desaloque esta string.

4) Faça um programa que leia 5 strings e armazene em uma matriz de strings. Cada string deve ter seu espaço alocado dinamicamente (considere strings de no máximo 20 caracteres).


Powered by txt2tags

  

[ Voltar Curso C | Índice de Seções ]


Copyright © 2002 - 2007 LinuxDicas - Todos Os Direitos Reservados.
LinuxDicas: lucas.martinez @linuxdicas.com.br
Web site engine's code is Copyright © 2003 by PHP-Nuke. All Rights Reserved. PHP-Nuke is Free Software released under the GNU/GPL license.
Tempo para gerar esta página: 0.050 segundos.