Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Matrizes e Ponteiros – Parte 1

Olá todos! Espero que teham ido bem de Festas!

Hoje começaremos talvez o tópico mais importante de programação C. Esse tópico é importantíssimo e com certeza provocará dúvidas, portanto lembro que os comentários deverão ser usados para tirar dúvidas. Não deixem nenhuma dúvida passar nesse momento, pois isso poderá depois complicar o aprendizado de outros tópicos avançados.

O tema de hoje, e de mais alguns posts é Matrizes e Ponteiros.

Antes, porém, de vermos alguma programação, precisamos de alguma teoria:

Memória e Ponteiros:

Quando vimos a criação de variáveis, ficou uma espécie de “aberto”. Lá foi dito que “em C, as variáveis representam espaços de memória que o compilador irá preparar para determinadas funções para uso do programa.” Na verdade, isso tá certo, mas não totalmente. Quando declaramos uma varíavel, indicamos ao computador que precisamos que uma derminada posição de memória seja separada para uso do programa e que, toda vez que o compilador achar o nome da variável, ele aponte o local em questão para onde a variável foi encontrada. Desse modo podemos dizer que o nome de uma variável é a “representação” do endereço onde fica o conteúdo da mesma.

Porém, existem situações onde precisamos utilizar uma posição de memória que não conhecemos previamente. Na realidade, isso é mais comum do que se imagina: não fosse assim, todo programa deveria ser artificialmente limitado em suas capacidades baseado em números arbitrários de informação a ser processada. Por exemplo: um programa teria que criar previamente 500 variáveis de notas para processar o boletim escolar de uma classe de 20 alunos, sendo inútil para uma classe de 501 alunos. Isso também aumentaria o custo de desenvolvimento e manutenção de um programa de computador, além de utilizar os recursos de um computador de maneira pouco eficiente.

O C nos oferece um mecanismo muito importante para apontar-se para um local de memória previamente desconhecido, ao qual chamamos de ponteiro.

Uma variável de ponteiro (que chamaremos de ponteiro, ou pointer em inglês) é uma variável que armazena o endereço de memória onde o conteúdo que desejamos está. Ela em si não é o conteúdo (embora possamos manipular o ponteiro de modo a mudar o local de memória indicado conforme a necessidade), mas indica onde esse conteúdo tá armazenado. Usando esse ponteiro, podemos chegar ao conteúdo e o trabalhar.

Imagine o ponteiro como a agência de correio. Ela não é as pessoas para quem são entregues as mercadorias, mas ele sabe de alguma forma onde elas moram. O ponteiro funciona de maneira similar.

Matrizes, strings e ponteiros

OK… Mas o que os ponteiros têm a ver com matrizes e strings (que, como vimos lá no Hello World, é uma matriz de caracteres). Bem. na realidade, podemos dizer que uma matriz é um ponteiro.

‘Como assim Bial?”

Quando você declara, por exemplo char nome[80], você está alocando 80 caracteres em uma matriz e apontando para o primeiro deles, de 0 a 79. (Veremos isso mais para frente quando falarmos de aritmética de ponteiros).

Um programa exemplo com ponteiros

OK. Vamos dar um tempo na teoria. Hora de colocar a mão na massa. Digite e compile o programa abaixo:

#include <stdio.h>

int main (void)
{
  char palavra[80]=”Hello World!”;
  char *palavra2;

  palavra2=palavra;

  printf(“O texto %s esta armazenado no endereco %p\n”,palavra,palavra);
  while (*palavra2)
    {
      printf(“O caracter %c da palavra %s esta no endereco %p\n”,*palavra2,palavra,palavra2);
      palavra2++;
    }

  return(0);

}

Bem, o início cansamos de ver, mas vamos ver as declarações que temos coisas interessantes nelas:

  char palavra[80]=”Hello World!”;

  char *palavra2;

Na primeira linha, declaramos uma matriz de 80 caracteres na qual armazenaremos a string “Hello World!”. O C possui uma convenção bastante prática para strings: sempre que você coloca uma string entre aspas duplas (“), o compilador já sabe que deverá colocar ao final da string em questão o caracter terminador nulo (\ 0 – já vimos ele lá atrás, lembra?). No momento em que o programa é carregado, o próprio sistema aloca o equivalente a 80 caracteres e dentro deles coloca a string “Hello World!”. Em seguida, declaramos uma varíavel ponteiro de caracter (char * – ocasionalmente lendo-se char pointer) chamada palavra2. O asterisco é o símbolo que indica que a variável em questão é um ponteiro para o tipo de dado desejado, não o próprio dado. Embora todos os ponteiros tenham o mesmo tamanho, é importante indicar o tipo de dados ao qual aquele ponteiro aponta, pois o uso de um ponteiro de um tipo de dados errado pode acarretar problemas seríssimos (a má interpretação e uso dos dados pelo programa sendo o menor deles). 

A linha seguinte:

  palavra2=palavra;

Tem que ser pensada calmamente. No caso de matrizes , existe uma coisa a ser mencionada: quando você utiliza o nome da variável sem um [] (indicador de posição a referenciar), o C entende que você deseja utilizar ou manipular a o endereço que indica o priemiro item da matriz. Ao mesmo tempo, quando utilizamos apenas o nome da varíavel ponteiro sem o indicador *, indica a mesma coisa. No caso, essa linha pode ser traduzida como:

“Pegue o endereço do primeiro item da matriz palavra e coloque-o como o endereço a ser apontado por palavra2

Ponteiros em C podem ter a posição à qual eles apontam modificadas em tempo de execução. Na realidade, os ponteiros são feitos justamente para terem esse comportamento: veremos a importância desse comportamento mais adiante, quando falarmos de alocação dinâmica de memória.

Em seguida temos um printf:

  printf(“O texto %s esta armazenado no endereco %p\n”,palavra,palavra);

A ideia aqui é mostra onde é que está, na memória, a string “Hello World!”. Em seguida temos um loop:

  while (*palavra2)

    {

      printf(“O caracter %c da palavra %s esta no endereco %p\n”,*palavra2,palavra,palavra2);

      palavra2++;


    }


Que irá deslocar o ponteiro palavra2 e mostrar em que lugar da memória cada um dos elementos de palavra2 está armazenado. Para isso, em ambos os caso, utilizamos o modificador de formato %p que faz com que o printf imprima na tela o local na memória onde o ponteiro está apontando, e não o seu valor.
Agora, uma coisa pode ficar confusa no printf dentro do loop: por que quando queremos mostrar o caracter, temos que usar o símbolo * e quando queremos mostrar o string e o endereço apontado não? Isso acontece porque, tanto o modificador %s quanto o %p esperam um endereço de memória, enquanto o modificador %c (para caracteres) espera um conteúdo “real” (no caso, um caracter). Basicamente essa é a diferença e é uma diferença importante em C: em muitos casos, o C espera conteúdos “reais”, discretos, como um número ou um caracter. Nos demais casos, normalmente se trabalhará com ponteiros. Não existem em C conceitos como “strings”, “filas” e “listas” como o de linguagens de maior nível, como PHP, Java ou Python. Na realidade, o C oferece mecanismos para criar-se e manipular-se esses tipos de dados, em especial por meio dos ponteiros, mas a linguagem em si não possui tratativa para tais estruturas de dados mais amplas. Veremos no futuro como criar algumas dessas estruturas de dados.
Vamos destrinchar um pouco mais esse while, pois ele nos oferece dicas interessantes e maiores informações sobre como lidar com ponteiros em geral e com uma string em C em particular. Primeiro, vamos ver a “condição de saída” do while:
  while (*palavra2)

Ou seja, enquanto o valor apontado por palavra2 (*palavra2) for considerado verdadeiro, o laço segue adiante. Agora, a pergunta que deve estar passando na cabeça é: “como ele vai saber se o valor apontado é verdadeiro?” Quem ficou atento ao que dissemos quando falamos sobre os operadores lógicos deve ter se lembrado de que falamos que para o C qualquer valor é verdadeiro, à exceção do número 0, do caracter terminador nulo \ 0 e do valor pré-definido null. Se lembrarmos como as strings são compostas em C, elas são seqüências de caracteres terminadas com o caracter terminador nulo \ 0. Portanto, uma vez que o deslocamento do ponteiro leve-o para o caracter terminador nulo, o valor do ponteiro será falso e o programa sairá do loop que criamos.
Mas como fazemos o ponteiro avançar nos da string?“. Para isso, usamos um pouco de aritmética de ponteiro. No C, se usarmos os operandos aritméticos mais rudimentares + e , além do incremento e decremento unitários ++ e , podemos fazer o ponteiro avançar ou recuar, ou então indicar elementos adiante e anteriores à posição indicada pelo ponteiro. No nosso caso, utilizamos o incremento unitário:

      palavra2++;

No endereço em palavra2 (perceba a ausência do asterisco). Nesse caso, estamos indicando que queremos passar para o próximo elemento de palavra2. Quando o C executar essa operação, ele irá somar o equivalente ao número de bytes do tipo apontado por palavra2 ao valor de palavra2. Essa No nosso exemplo, não muda muita coisa, pois em quase todas as plataformas, um char tem o tamanho de 1 byte, mas esse é um conceito que é importante ficar claro: quando usamos operadores aritméticos para modificar o endereço apontado por um ponteiro, ele sempre trabalha somando ou subtraindo em bytes o número de elementos do mesmo tipo vezes o tamanho do tipo. Isso ficará mais claro no próximo post, quando iremos usar ponteiros para valores inteiros.
Isso nos dá como o programa funcionará conceitualmente, mas para melhor entendermos o que aconteceu, vamos analisar a saída do mesmo:

Analisando a saída do programa

OK, e como será a saída disso tudo?
O que iremos ter de saída do programa aparentará ser algo como a seguir:

O texto Hello World! esta armazenado no endereco 0xbffc5eb0
O caracter H da palavra Hello World! esta no endereco 0xbffc5eb0
O caracter e da palavra Hello World! esta no endereco 0xbffc5eb1
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb2
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb3
O caracter o da palavra Hello World! esta no endereco 0xbffc5eb4
O caracter   da palavra Hello World! esta no endereco 0xbffc5eb5
O caracter W da palavra Hello World! esta no endereco 0xbffc5eb6
O caracter o da palavra Hello World! esta no endereco 0xbffc5eb7
O caracter r da palavra Hello World! esta no endereco 0xbffc5eb8
O caracter l da palavra Hello World! esta no endereco 0xbffc5eb9
O caracter d da palavra Hello World! esta no endereco 0xbffc5eba
O caracter ! da palavra Hello World! esta no endereco 0xbffc5ebb

Os valores ao final de cada linha são os endereços onde estão armazenados os conteúdos em questão. Com certeze eles irão aparecer diferentes para você no momento em que você executar o programa, mas o importante é entendê-lo.
A primeira coisa é que todos os endereços são indicados no formato numérico de base 16, chamado hexadecimal. Essa é uma convenção antiga adotada no mundo da informática para indicar endereços de memória. Não importa para nós as posições em questão, pois esses valores poderão (e provavelmente irão) mudar de execução em execução.
Na primeira linha, é indicado o endereço da string “Hello World!”, pego pelo endereço do início da matriz que armazena o vetor de dados. Repare bem que o endereço de “Hello World!” e do caracter “H” são os mesmos, e depois o endereço onde ficam armazenados cara caracter aumenta de um em um byte (que é o tamanho do tipo char).
Como “brincadeira”, uma sugestão é tentar fazer com que a string seja “corrida” ao contrário. Isso pode ser feito utilizando o decremento unitário na aritmética de ponteiros e usando os endereços para comparar o momento em que o ponteiro palavra2 chegar no início da string palavra (lembre-se que deverá fazer comparações com o endereço armazenado em ambos os casos, e não com seus conteúdos). É algo mais difícil, e portanto ressalto que qualquer dúvida os comentários estão abertos para que elas sejam tiradas.
Bem, esse é apenas o início do caminho nos ponteiros em C. Na próxima “aula”, veremos mais sobre a aritmética de ponteiros e alocação dinâmica de memória, um tópico muito importante em C.
Até lá, bom estudo!

Uma resposta para “Matrizes e Ponteiros – Parte 1

  1. Joao Carlos Agostini 30/09/2014 às 19:05

    Olá
    Entendo por que o estudo de ponteiros é complicado, não é o conceito em si, mas a nomenclatura usada.
    Temos: tipo *nome; é a indicação de que nome é um ponteiro, nome, dentro do código, é o endereço armazenado no ponteiro e *nome, dentro do código, é o conteúdo que está no endereço indicado por nome!!!
    É muito confuso. Seria mais fácil usar algo assim: tipo *nome; declara que *nome é um ponteiro de um determinado tipo, *nome, no meio do código, refere-se ao endereço contido em *nome, e **nome é o conteúdo existente naquele endereço indicado por *nome.
    Dessa forma a sentença dentro de while ficaria:
    printf(“O caractere %c da palavra %s está no endereço %p\n”, **palavra2, palavra, *palavra2); em vez de (…,*palavra2, palavra, palavra2), na qual palavra2 aparece despida da sua característica de ponteiro, parecendo-se mais a uma variável comum!
    Bom, seja como for, tem-se que aprender como está.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: