Quarta-feira, 29 de Setembro de 2010

Ponteiros em C - Parte 1

Penso que esta é a maior dor de cabeça de principiantes em C e em C++ também, além de existirem as referências que são muito mais simples de se utilizar. Uma vez dominados os ponteiros podem ser muito mas mesmo muito úteis em todas as vertentes da linguagem.

Nesta primeira parte vamos:
* Compreender o conceito
* Utilizar e compreender os operadores & e *.

Temos muitos exemplos diários que se assemelham à utilização de ponteiros. Vou dar um exemplo que penso ser o melhor. Imaginemos que estamos a escrever manualmente um documento (não muito formal) e que nesse documento existe algum termo técnico ou conceito que nem toda a gente entende. O que fazemos é um referência a essa palavra no final do parágrafo ou até no final do próprio documento.

Exemplo:
"[...] os ponteiros[1] são muito úteis [...]".
[1] Em programação, um ponteiro ou apontador é um tipo de dado de uma linguagem de programação cujo valor se refere diretamente a um outro valor alocado em outra área da memória, através de seu endereço. Um ponteiro é uma simples implementação do tipo referência da ciência da computação. @pt.wikipedia.org

Podemos ver neste exemplo bastante simples que o [1] no texto em si faz referência ao [1] que, neste caso, é a definição da palavra.

Obviamente vocês perguntam o que é que isto tem a ver com a linguagem C e com programação. Este pequeno exemplo permite-nos explicar conceitos bastante importantes nesta matéria.

Agora vou introduzir um problema que é bastante comum para a explicação de ponteiros.

Como vocês sabem quando se chama uma função é criado na stack um stack frame para essa função (podem ver o meu artigo sobre buffer overflow), com todas as variáveis locais, etc. Ou seja, cada função tem o seu próprio stack frame (quando chamadas), logo nenhuma função poderá interferir com a variáveis de outra função visto que estão em regiões de memória diferentes. É aqui que os ponteiros nos vão ser úteis, eles vão permitir-nos realmente alterar o valor das variáveis de outras funções. Isto é apenas um exemplo introdutório, obviamente.

Então vamos já escrever um programa em que vamos poder ver que não podemos realmente alterar o valor de variáveis de funções diferentes.
Exemplo nº 1

Meh, vocês olham para este problema e pensam "este tipo está a fazer as coisas mal", com os conhecimentos que vocês já têm conseguem resolver o problema facilmente! Como? Simples, usando o return. Simples, retornámos o valor de x modificado para a função main. Escrevendo novamente o código.
Exemplo nº 2

E pronto tinhamos o problema resolvido.

Então vamos agora apresentar outro problema. Escrever uma função que dados dois valores x e y troca entre si os valores, ou seja:
* x = 10, y = 20
* fun (x,y)
* x = 20, y = 10

Este problema seria impossível de resolver se não tivessemos os ponteiros para ajudar! Não podemos fazer dois return's, ou seja, nunca vamos poder retornar os dois valores! Temos mesmo de arranjar uma forma de "invadir" o stack frame da função main e alterar lá os valores. Como fazer isto? Simples, ponteiros.
Este problema vai ser resolvido numa próxima parte deste artigo. Primeiro temos de conhecer bem como trabalhar com ponteiros.

Como repararam com certeza eu naquele pequeno "documento" pus uma definição para ponteiro. Basicamente um ponteiro é um tipo de dado que armazena nele um endereço de memória de outra variável ou objecto qualquer. Tudo em memória tem o seu endereço de memória, logo podemos ter o endereço de qualquer coisa e podemos ainda armazenar esse endereço num ponteiro. Com o ponteiro podemos ainda trabalhar directamente na variável para qual o ponteiro aponta, podemos alterar o seu valor, podemos realmente trabalhar com a variável em si através de um "comunicador" que é o ponteiro.

O operador em C que nos retorna o endereço de qualquer coisa é o &. Portanto, imaginemos que temos a variável x, o endereço da variável x pode ser obtido pela expressão &x. Bastante simples, uh?
Agora para termos esse endereço armazenado num ponteiro temos de, adivinhem só, declarar um ponteiro e atribuir &x a esse ponteiro.
Como se declara um ponteiro? Muito simples. Primeiro temos de saber sempre isto: Se temos uma variável int o ponteiro que aponta para essa variável tem de ser do tipo int também, se temos uma variável char o ponteiro que aponta para lá também tem de ser do tipo char e assim por diante!
Portanto, para declarar fazemos: int *pt. É exactamente a mesma coisa que declarar uma variável normal mas põe-se o * antes. Podemos declarar uma variável "normal" e um ponteiro na mesma linha, se forem do mesmo tipo obviamente. P.e. int *pt, x = 0. Neste caso apenas pt é um ponteiro!

Então já sabemos:
* Declarar um ponteiro
* Obter o endereço de uma variável

Sabemos também atribuir algo a algo.
Então vamos lá fazer um pequeno código que nos "põe" dentro de um ponteiro o endereço de uma variável. Pûs no inicio do código algumas notas que achei importantes.
Exemplo nº 3

Como podem ver o conteúdo de pt e o endereço de x são exactamente os mesmos, simplesmente porque atribuímos a pt o endereço de x através de: pt = &x.

A questão agora é o que é que podemos fazer com o endereço de x. Podemos sem dúvida alterar o seu valor e trabalhar com ele com o ponteiro pt. Fazemos isto com o operador *. Quando nos referimos a *pt estamos na verdade a referir-nos ao valor de x e quando nos referimos a pt, somente, estamos na verdade a trabalhar com o endereço de x, ou seja, com o conteúdo de pt.
Portanto, para trabalharmos com o valor de x temos de trabalhar com *pt. Vamos então alterar o valor de x através de pt.
Exemplo nº 4

Como podem ver neste exemplo trabalhamos várias vezes com *pt, pt e até com x e chegámos sempre ao mesmo resultado. Porquê? Simplesmente porque pt aponta para x e podemos trabalhar com x através de pt muito facilmente utilizando os operados que aprendemos até aqui.

Resumindo, o operador & retorna o endereço de alguma variável (e não só mas isso veremos mais para a frente). O operador * permite-nos referir ao valor da variável para qual o ponteiro aponta.

Termino então aqui a primeira parte deste artigo.
Aconselho desde já a pensarem como resolver o problema da troca de valores entre variáveis (criando um função que faz isso) e também a brincarem com todos os códigos que forneci até aqui. É de extrema importância que compreendam estes conceitos iniciais visto que vão ser usados para sempre daqui para a frente!

Espero que tenham gostado e que leiam a parte 2 quando esta for publicada.

0 comentários:

Enviar um comentário