Revista Do Linux
EDIÇÃO DO MÊS
 Hardware
 Comandos Avançados
 Tecnologia

 Capa
 Entrevista
 CD
 Corporativo
 Sistema
 Evento
 Segurança
 Internet
 Programação

 

Curso de Linguagem C - PARTE V

Um dos recursos mais poderosos da linguagem C é o uso de ponteiros. Neste artigo, vamos conhecer um pouco de suas características

Nesta quinta parte do nosso curso, iremos conhecer os ponteiros, também chamados apontadores, largamente utilizados nos programas em C. Sua compreensão é importante, pois a utilização plena da linguagem C só é possível após o entendimento e domínio desse recurso.

Iniciaremos com a definição de ponteiros, seguida por uma recordação do conceito de endereçamento de memória. Depois descreveremos as formas de utilização dos ponteiros, junto com suas particularidades. Iremos também rever os conceitos de vetores e cadeias de caracteres (strings) e estudar suas formas de manipulação através do uso dos ponteiros. Da mesma forma que nos artigos anteriores, serão mostrados programas exemplo para facilitar o entendimento dos conceitos apresentados. Vamos lá!

Definição inicial

Em termos bem simples, um ponteiro é uma variável que contém o endereço de uma posição de memória. Se para você essa definição parece vaga e enigmática, não se assuste. Vamos recordar um pouco alguns conceitos do hardware de microcomputadores.

Todo microcomputador é baseado em um microprocessador, um circuito eletrônico programável, que trata dados armazenados em outros circuitos eletrônicos, que chamamos de memória. O microprocessador gerencia a memória através do endereçamento de cada uma de suas posições. Por exemplo, um microcomputador com 64 megabytes de memória tem suas posições de armazenamento endereçadas de 0 até 67.108.863 (notação decimal) ou de 0 até 3FFFFFF (notação hexadecimal).

Quando utilizamos a linguagem de máquina, é necessário endereçar diretamente a posição de armazenamento de um determinado dado. Embora isso pareça simples a princípio, imagine, por exemplo, desenvolver um programa e ter de lembrar que o valor do contador de repetições está armazenado no endereço 1.730.364, ou 001A673C (em notação hexadecimal).

E mais, imagine que você esteja alterando o programa e tenha de modificar esse local de armazenamento. Isso implica a mudança de todos os comandos onde você utilizou o endereço anterior, para colocação do novo endereço.

Em vez de endereços, as linguagens de programação permitem que manipulemos dados através das variáveis. Assim, quando declaramos uma variável i, por exemplo, e a usamos em um comando como:



i = i + 1;


O nome i representa o valor que associamos a ela, e não o endereço onde armazenamos o seu valor.

Porém, existem casos em que é necessário utilizar o endereço de uma variável e não o seu conteúdo. Para atender a essa necessidade, a linguagem C disponibiliza operadores específicos, que serão vistos a seguir.

O operador &

A linguagem C disponibiliza um operador que fornece o endereço de uma variável, representado pelo caractere & ("e" comercial). Esse é um dos operadores unários do C, ou seja, ele requer apenas um operando. O operador +, por exemplo, é considerado binário, pois necessita dois operandos.

O operador & é utilizado colocando-se à sua direita o nome da variável da qual desejamos o endereço: &nome_variavel

Para entendermos melhor a diferença entre variável e endereço da variável, digite o programa exemplo a seguir:



#include <stdio.h>
main()
{
int i;
i = 5;
printf("O valor de i eh %d\n", i);
printf("O valor de &i eh %u\n", &i);
exit(0);
}

Grave o programa com o nome exemplo1.c e gere um executável utilizando o seguinte comando:



# gcc exemplo1.c -o exemplo1


Agora execute o programa:


# ./exemplo1
O valor de i eh 5
O valor de &i eh 3221223380
O valor de &i mostrado poderá ser diferente, já que se trata de um endereço de memória, que irá variar de máquina para máquina.

Pelo exemplo, podemos ver a diferença entre os valores: quando nos referenciamos a i, estamos falando do valor representado por i, que é 5, enquanto &i é o endereço da variável i, cujo valor é 3.221.223.380.

Os leitores mais atentos devem estar perguntando: mas se o endereço que o programa gerou é a posição do dado na memória, o micro onde o exemplo foi executado tem mais de 3 Gb de memória? Na verdade, o valor apresentado é um valor simbólico do endereço para o programa. O Linux, por ser um sistema operacional multitarefa, disponibiliza para cada programa executado uma área de memória exclusiva, que é endereçada com uma faixa de números que não corresponde às posições reais de memória.

O caractere de conversão u, logo após o caractere de escape % no comando printf, indica que o valor de &i será impresso como um inteiro sem sinal (unsigned int). Em C, cabe ao programador definir se um número inteiro tem ou não o sinal, já que ambos são armazenados na memória em forma de números binários (0 ou 1). Como, nesse caso, o número representa uma posição, o formato mais adequado é o inteiro sem sinal.

O operador *

Com o operador & conseguimos obter o endereço de uma variável. E se tivermos o endereço de uma variável e quisermos obter o seu conteúdo? Para isso, a linguagem disponibiliza um outro operador unário, representado pelo caractere * (asterisco). Ele fornece o valor da variável cujo endereço é o operando imediatamente à sua direita. Complicado? Vejamos um exemplo:


i = 5;
pont = &i;
j = *pont;  /* o valor de j eh 5 */

Nessa seqüência de comandos, a variável pont está armazenando o endereço da variável i, obtido através do operador &. O que fizemos foi atribuir à variável j o valor armazenado no endereço armazenado em pont. Preste atenção no detalhe: não estamos armazenando em j o endereço da variável i, e sim o conteúdo da variável i.

Ainda nos comandos acima, a variável pont armazena o endereço de um valor inteiro, que é representado pela variável i. Dizemos assim que a variável pont "aponta" para i e, da mesma forma, que a variável pont "aponta" para um inteiro. Repare que o tipo da variável pont não é mais inteiro, e sim um endereço, um apontador para uma variável inteira. Por esse motivo, declaramos a variável pont de uma forma diferente:


int *pont;

Essa declaração informa ao compilador que a variável pont irá armazenar um endereço de memória onde está armazenado um valor do tipo inteiro. Lembre que a linha acima é uma declaração e não um comando. Por esse motivo, o caractere * não está indicando uma operação!

Vamos expandir o primeiro exemplo. Digite o programa a seguir:



#include <stdio.h>
main()
{
int i;
int j;
int *pont;
i = 5;
pont = &i;
j = *pont;
printf("O valor de i eh %d\n", i);
printf("O valor de pont eh %u\n", pont);
printf("O valor de j eh %d\n", j);
exit(0);
}

Grave com o nome exemplo2 e compile:


# gcc exemplo2.c -o exemplo2

Execute:


# ./exemplo2
O valor de i eh 5
O valor de pont eh 3221223380
O valor de j eh 5

O que fizemos no programa? Primeiro, declaramos as variáveis i e j do tipo inteiro e a variável pont do tipo apontador para um inteiro. Em seguida, atribuímos o valor 5 à variável i. No comando seguinte, atribuímos a pont o endereço da variável i. Por fim, atribuímos a j o valor que está armazenado no endereço armazenado em (ou apontado por) pont. Repare que esses três comandos utilizaram os operadores de ponteiro (& e *) para atribuir o valor da variável i à variável j (equivalente ao comando i = j;).

Vetores

Vamos recordar o conceito de vetores apresentado na segunda parte de nosso curso. Quando digitamos:


int vet[10];

Estamos declarando um vetor, de nome vet, com dez posições. Como você também deve lembrar, o acesso a cada um dos elementos do vetor é feito pelos índices: vet[0], vet[1], e assim por diante, e o último elemento do vetor tem o índice 9, e não 10.

Uma característica importante implementada na linguagem C é que o nome de um vetor é considerado também um ponteiro para a primeira posição desse vetor. Por exemplo, se declararmos:


int vet[10];
int *pont;
e utilizarmos o comando:
pont = vet;

O endereço de pont é o endereço do primeiro elemento de vet, ou seja, o comando acima é idêntico a:


pont = &vet[0];

A linguagem C também permite que sejam feitas operações com o valor de um ponteiro, e os resultados levarão em conta a forma como o dado é armazenado na memória. No caso de um vetor, a linguagem determina que todos os valores devem estar armazenados em posições contíguas de memória, em ordem crescente de posição. Assim, se utilizarmos os seguintes comandos:


pont = vet;
pont++;


A variável pont estará armazenando o endereço do segundo elemento do vetor vet (&vet[1]). É importante ressaltar que o endereço armazenado em pont não foi incrementado em uma unidade. Isso porque uma variável inteira ocupa um determinado número de bytes de memória, que depende do tipo de microprocessador utilizado.

Na verdade, o valor do ponteiro foi modificado para o endereço do próximo dado do tipo que ele aponta, que é a "unidade" desse tipo de ponteiro.

Digite o programa exemplo a seguir para entender melhor esses conceitos:



#include <stdio.h>
main()
{
int i;
int vet[5];
int *pont;
vet[0] = 12;
vet[1] = 20;
vet[2] = 41;
vet[3] = 86;
vet[4] = 65;
for(i=0; i < 5; i++)   {
printf("Valor de vet[%d] = %d\n", i, vet[i]);
printf("Endereco - &vet[%d] = %u\n", i, &vet[i]);
}
for(i=0, pont=vet; i < 5; i++, pont++)
printf("Valor no endereco %u = %d\n", pont, *pont);
}

Grave com o nome exemplo3.c e compile:


# gcc exemplo3.c -o exemplo3

Execute:



# ./exemplo3
Valor de vet[0] = 12
Endereco - &vet[0] = 3221223360
Valor de vet[1] = 20
Endereco - &vet[1] = 3221223364
Valor de vet[2] = 41
Endereco - &vet[2] = 3221223368
Valor de vet[3] = 86
Endereco - &vet[3] = 3221223372
Valor de vet[4] = 65
Endereco - &vet[4] = 3221223376
Valor no endereco 3221223360 = 12
Valor no endereco 3221223364 = 20
Valor no endereco 3221223368 = 41
Valor no endereco 3221223372 = 86
Valor no endereco 3221223376 = 65

O primeiro laço imprimiu os valores armazenados em cada posição do vetor, bem como os endereços de cada posição. O segundo laço utilizou a variável pont como ponteiro, inicialmente para a primeira posição do vetor, e o incrementou a cada repetição, imprimindo o endereço do ponteiro e o valor armazenado nesse endereço. Repare como os endereços não aumentam em uma unidade, e sim de quatro em quatro, que é o número de bytes utilizado por uma variável inteira em micros de arquitetura Intel Pentium, e como os valores apontados são exatamente aqueles dos elementos dos vetores.

Cadeias de caracteres

Da mesma forma que temos um vetor de números inteiros, podemos ter um vetor de caracteres. Esse vetor também é chamado de cadeia de caracteres (string). Você deve lembrar-se desse nome, apresentado na primeira parte do curso. As cadeias de caracteres são declaradas da forma usual como um vetor é declarado: char vetcar[50];

A declaração acima reserva um espaço em memória para 50 dados do tipo caractere, que na verdade são os códigos ASCII. Por definição, uma cadeia de caracteres deve conter, na sua última posição, o valor ‘\0’, para indicar seu fim e permitir sua correta impressão. Dessa forma, no vetor vetcar, temos 49 possíveis posições para caracteres imprimíveis. Como vetcar é um vetor, continua válida a característica citada anteriormente: vetcar também é um ponteiro para a primeira posição da cadeia. Vamos mostrar um exemplo de manipulação de uma cadeia de caracteres:



#include <stdio.h>
main()
{
char vetcar[6];
char *pontcar;
vetcar[0] = ‘T’;
vetcar[1] = ‘E’;
vetcar[2] = ‘S’;
vetcar[3] = ‘T’;
vetcar[4] = ‘E’;
vetcar[5] = ‘\0’;
printf("Conteudo da cadeia vetcar: %s\n", vetcar);
pontcar = vetcar;
printf("Conteudo apontado por pontcar: %s\n", pontcar);
}

Compile:


# gcc exemplo4.c -o exemplo4

Execute:


# ./exemplo4
Conteúdo da cadeia vetcar: TESTE
Conteúdo apontado por pontcar: TESTE

Nesse exemplo, montamos inicialmente a cadeia de caracteres por meio da atribuição das letras a cada uma das posições da cadeia. Repare que a última posição armazena o valor 0, para indicar o fim da cadeia.

O comando printf está usando o tipo de formatação s, que indica que o argumento correspondente é um ponteiro para o início de uma cadeia de caracteres, e que todos os caracteres até o zero deverão ser impressos. Aqui vemos claramente como o fato de o nome de um vetor também ser um ponteiro facilita a programação, pois não é necessário digitar o argumento do printf como "&vetcar[0]".

Por fim, mostramos novamente o conteúdo da cadeia, agora usando pontcar como ponteiro para a cadeia.

Conclusão

Pudemos ver neste artigo várias propriedades importantes dos ponteiros. Estude-as com cuidado e procure entender o conceito de endereços e seus conteúdos. No próximo artigo, continuaremos a apresentar os ponteiros, com novas funções e recursos, além de aprofundar o estudo dos argumentos de funções. Até lá!


A Revista do Linux é editada pela Conectiva S/A
Todos os Direitos Reservados.

Política de Privacidade
Anuncie na Revista do Linux