sexta-feira, 22 de fevereiro de 2008

PROGRAMAÇÃO C - APONTADORES E VETORES


Considerações sobre Apontadores e Vetores em C


Talvez a maior dificulade em um curso de linguagem C seja a compreenção quanto ao uso de ponteiros e vetores (arrays). Ao contrário da maioria das linguagens, existe em C, uma forte relação entre apontadores e vetores. Tão forte que apontadores e vetores deveriam ser discutidos juntos. Em C, qualquer operação que possa ser feita com subscritos de um vetor pode ser feita com apontadores. Assim, o objetivo deste texto será mostrar essa relação.

Considere a declaração abaixo:

// Programa 1

void main()

{

int a[10];

}

Essa declaração define um vetor de escopo local de tamanho 10, isto é, um bloco com 10 variáveis do tipo int consecutivos chamados a[0],a[1],...,a[9]. A figura abaixo tenta mostrar, de forma hipotética, como seria a disposição de memória para o vetor <a>. O símbolo ? representa um valor desconhecido, pois, no momento em que uma variável de escopo local ganha vida, ou seja, é instanciada, o seu valor inicial é desconhecido.


?

?

?

?

?

?

?

?

?

?

Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito



A notação a[i] refere-se ao elemento da i-ésima posição do vetor.


Considere o programa abaixo. Note que ele declara o vetor <a> e atribui o valor 0 (zero) para cada elemento.


// Programa 2

void main()

{

int a[10];

int I;

for ( I = 0; I <>

a[I] = 0;

}


Observe o que aconteceu com os valores do vetor <a> após a sua execução:


0

0

0

0

0

0

0

0

0

0

Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito





Considere agora a possibilidade de fazer a mesma operação utilizando ponteiros.

Para tanto, vamos ver uma breve introdução a ponteiros em C.

Em C, um ponteiro é uma variável que contém um endereço de memória, ou seja, no lugar de armazenar um valor do tipo “Quantidade de Alunos Matriculados”, ou algo que o valha, ele é usado para apontar para um endereço de memória previamente alocado.


Vamos analisar o programa a seguir:


// Programa 3

void main()

{

int *pa;

int b = 10;

pa = &b;

*pa = 6;

printf(“\nO valor de b eh: %d\n”,b);

}


  1. A intenção da declaração int *pa foi de declarar uma variável que irá apontar para uma posição de memória que será representada como inteiro (int), ou seja, <pa> é um ponteiro para um tipo int.

  2. A variável <b>, declarada como inteira, poderá ser a posição de memória desejada para ser manipulada pelo ponteiro <pa>. Note que o valor inicial de b é 10;

  3. Para atribuir o endereço da variável b ao ponteiro pa, usamos o operador &, ou seja, pa = &b. Isso significa: Atribua a <pa> o endereço de memória de <b>.

  4. Posso agora manipular o endereço de memória que <pa> aponta da seguinte forma: *pa = 6. Isso pode ser interpretado como: Coloque o valor 6 na célula de memória apontada por <pa>, ou seja, se <pa> aponta para <b>, logo b será igual a 6 após a execução dessa instrução.


O uso de ponteiros em C deve ser bastante racional. O menor erro na manipulação com ponteiros poderá causar resultados inesperados. Usando o exemplo anterior, se no lugar de *pa = 6 fosse usado pa = 6, o programa não atingiria o seu propósito.


Voltando com a idéia inicial com relação a vetores. Se <pa> for uma apontador para um inteiro, declarado por


int *pa;


E <a> for um um vetor de inteiros, declarado por


int a[10];

então a atribuição

pa = &a[0];

ou

pa = a;

define <pa> de modo que aponte para o elemento zero do vetor <a> ; isto é, <pa> contém o endereço de a[0].


Continuando com o raciocínio e como já foi mostrado no início, a figura abaixo representa a disposição de memória para o vetor <a>. Note a relação entre subscritos do vetor a e o ponteiro <pa>.












Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito

pa+0

pa+1

pa+2

pa+3

pa+4

pa+5

Pa+6

pa+7

pa+8

pa+9

Aritimética de Ponteiro



O índice 0 poderá ser representado por (pa+0) ou simplesmente pa;

O índice 1 poderá ser representado por (pa+1);

O elemento I-ésimo do vetor <a> poderpa ser representado por (pa+I).


Considere o trecho de código abaixo:


// Programa 4

void main()

{

int *pa;

int a[10];

int x;


pa = a; // ou pa = &pa[0]


x = *pa;

}

x = *pa

copiará o conteúdo de a[0] em x.




Se <pa> aponta para o primeiro elemento vetor <a>, então podemos concluir que pa + 1 apontará para o próximo elemento do vetor <a>.


Considere o programa a seguir:


// Programa 5

#include

#include


void mostre( int *, int );


void main()

{

int a[10];

int *pa;

int i;


for ( i = 0; i <>

mostre(a,10);


pa = &a[0]; // pa aponta para o primeiro elemento


for ( i = 0; i <>


mostre(a,10);

}


void mostre(int *vetor, int tamanho )

{

int i;

for ( i = 0; i <>

printf("\n%d",vetor[i]);

}

No programa 5, exemplo acima, a primeira estrutura for utiliza a operação de subscrito para fazer atribuição ao vetor <a> (exe.: a[i] = 10 ). Isso, dependendo da capacidade que o compilador tem para otimizar código, poderá resultar em custo adicional de processamento. Para ilustrar, considere que o tamanho do tipo inteiro utilizado seja 4 Bytes, ou seja, o nosso compilador C representa o tipo int com 4 bytes.


4 Bytes

4 Bytes

4 Bytes

4 Bytes

4 Bytes

4 Bytes …


0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

i=0

i=1

i=2

i=3

i=4

i=5 …

pa + 0

pa + 1

pa + 2

pa + 3

pa + 4

pa + 5 …



Sabendo-se que a unidade de endereçamento da maioria dos computadores modernos é o Byte, para endereçar o quarto valor inteiro, índice 3, do vetor <a> será feita a seguinte aritmética:


(Base do vetor) + (tamanho em bytes do tipo de dados) * (o Índice)

Ou seja,

Para endereçar a[3] temos:

a + 4 * 3.


Para o exemplo acima, isso significa que terei que deslocar 12 Bytes (4 * 3) a partir do endereço base do vetor <a> para chegar ao Índice 3 ( 4° elemento do vetor <a>).

Como você pôde notar no exemplo dado, foi necessária uma operação de multiplicação para endereçar a posição do vetor desejada. Sabendo-se que uma operação de multiplicação custa muito mais caro que uma operação de adição para uma CPU, operações com subscritos poderá resultar em um tempo adicional, considerando um vetor de grande dimensão.


Ao contrário, se no lugar de usar operações com subscrito, for utilizado ponteiros, o resultado final será o mesmo, porém, o tempo poderá ser sensivelmente menor para vetores de grandes dimensões. Isso em função da aritmética utilizada para ponteiros que, no nosso caso é diferente da utilizada pelos subscritos, ou seja, se <pa> for um ponteiro para inteiros de 4 bytes e apontar para a base do vetor <a>, então pa = pa + 1 ou pa++, fará o ponteiro <pa> deslocar 4 Bytes, fazendo assim, <pa> apontar para o próximo elemento do vetor. Dessa forma, para acesso seqüencial a um vetor, poderá ser mais barato utilizar um ponteiro.


Voltando ao programa 5, a segunda estrutura for, faz uso do ponteiro <pa> para manipular o vetor <a>. Nesse caso, o compilador irá gerar um código sem operações de multiplicação, tornando assim, o código final mais eficiente para esse tipo de problema.


Você notará essa forte relação entre apontadores e vetores, quando usar as funções de manipulação de strings (strlen, strcpy, etc). A manipulação de uma string, vetor de char em C, será por meio de ponteiro ou vetor de acordo com a conveniência.


Concluindo, devido a relação muito próxima entre ponteiros e vetores, o programador C deverá ser capaz de discernir quando usar um ou outro. Muitas vezes será mais conveniente utilizar subscritos para operar com vetores, sobretudo quando o tempo de execução e o tamanho do código gerado não são críticos. O uso de ponteiros tende para um código mais otimizado, porém, poderá deixar seu código em C menos legível além de poder comprometer a robustez do seu programa caso o ponteiro seja utilizado de forma errada.


Referências Bibliográficas:


Kernighan Brian W., Dennis Ritchie M, C A Linguagem de Programação Padrão ANSI, Editora CAMPUS

Aaron M., Yedidyah Langsan e Moshe J. Augenstein, Data Structures Using C, Editora Prentice-Hall