Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Tipos de Dados Personalizados em C – Estruturas

Olá, pessoal!

Já faz um certo tempo que não posto nenhuma “aula” nova, e peço desculpas, mas pretendo terminar o “básico” de C para o dia-a-dia ainda em 2011, se minhas atividades de serviço assim permitir.

Antes de tudo, espero que vocês tenham visto as “aulas” anteriores, onde falamos de Makefiles e compilação fracionada e tenham entendido direitinho tudo o que foi dito, pois esse será um conceito fundamental a partir de agora.

Outra coisa, estou publicando todos os códigos do Aulas de C em meu github. Basicamente, um github é um local onde podemos armazenar remotamente códigos fontes como backup. Na realidade, o github é um servidor que oferece acesso aberto e gratuito a um servidor de controle de versões de código fonte remoto baseado no software git, criado por Linus Torvalds, o mesmo criador do Linux. É um sistema rápido e eficiente, com a vantagem de ser distribuído, o que permite que cada desenvolvedor trabalhe no seu código sem interferir de imediato nos códigos dos demais desenvolvedores. Não vamos nos aprofundar nesse assunto de imediato e para os interessados, a sugestão é ir no site github.com e dar uma olhada nas instruções do site, que são bem claras.

Além disso, estarei divulgando arquivos .zip com os códigos fontes das aplicações para que vocês possam dar uma olhada nas mesmas.

Bem,  sem mais delongas, vamos falar do nosso “assunto” dessa “aula”: estruturas de dados em C.

1-) O que são estruturas de dados?

Bem, vocês devem ter reparado que trabalhamos com tipos de dados “discretos”, como números inteiros e flutuantes, caracteres, matrizes e ponteiros. Isso parece ter ficado claro até aqui. E em geral isso é bom quando estamos aprendendo…

Porém, no “mundo real”, não utilizamos apenas esses tipos, e sim utilizamos dados como “informações cadastrais”, “conta bancária”, “informações de login do usuário”, “ficha técnica do filme” e etc. É possível, obviamente, utilizar-se caracterers, matrizes, ponteiros e toda uma complexa combinação de tipos de dados “discretos” para criarmos nosso sistema com essas informações, mas isso cedo ou tarde ficará confuso e sujeito a bugs.

Para facilitar nossa vida, o C (como a grande maioria das linguagens de programação modernas) oferece mecanismos para que o desenvolvedor crie e trabalhe com seus próprios tipos de dados. Existem duas formas básicas de se fazer isso, que são chamadas de estruturas (struct) e uniões (unions). Uniões são um tópico avançado sobre o qual não falaremos aqui. Vamos então falar de estruturas.

As estruturas são formas que o C oferecem de organizarmos nossos dados. Por exemplo, imagine um software para o cálculo de números complexos, que possuem uma parte real e uma imaginária. Você pode, por exemplo, ter declarações como a seguinte:

int real1, imaginario1, real2, imaginario2, real3, imaginario3….

Parece algo muito tranquilo, mas isso causa uma série de problemas:

  1. O código fica complexo e pouco flexível: para operações individuais pode até parecer ok, mas conforme você vai trabalhando com mais e mais informações, seu programa ficará cada vez mais complexo, com código redundante e repetitivo;
  2. Você ficará “engessado”: é bem complicado trabalhar com o melhor do sistema, com recursos como uso de alocação dinâmica e ponteiros para melhor aproveitar os recursos do sistema e, com isso, alcançar o melhor em termos de rendimento do sistema, além dos problemas que você terá quando você precisa trabalhar com uma quantidade arbitrária (não previamente determinada) de dados;

Uma forma mais inteligente é criar uma espécie de tipo de dados novo. Em teoria isso é impossível, pois você teria que recriar compiladores e afins, pensando de uma maneira rudimentar. Mas o C já prevê, em seu padrão, a possibilidade de “extender-se” os tipos padrões do C e do sistema e com isso criar-se tipos personalizados de dados que representem os dados que estamos trabalhando, desse modo tornando o código mais lógico e aproveitando de todos os recursos do C. O mecanismo que nos permite isso são as estruturas de dados. No nosso caso, criamos uma estrutura. Toda estrutura é criada com a palavra-chave struct seguida do nome da estrutura e o que compõe essa informação dentro de chaves ({}), como se estivéssemos declarando variáveis normalmente. Formalmente a struct é representada como abaixo:

struct [nome_da_estrutura]

{

    tipoValor1 valor1;

    tipoValor2 valor2;

    …

    tipoValorN valorN;

}

Imaginando os números complexos que falamos anteriormente, um tipo de dados que representa números complexos poderia ser representada assim:

struct tComplexo

{

    int real;

    int imaginario;

};

Perceba que colocamos o nome da estrutura como tComplexo. É uma boa prática um nome como esse, pois o C oferece um comando que permite criar um “apelido” para um tipo de dados que tornaria a coisa mais fácil. Porém, se você nomeasse ele como, por exemplo, complexo, esse nome não ficaria mais disponível como “apelido”.

OK… Criamos nosso tipo… Mas como o usamos?

Bem, primeiro de tudo, temos que declarar nossas variáveis com nosso tipo de dados desejado. A declaração é feita exatamente da mesma forma que a declaração de qualquer variável, com uma pequena diferença, que é a presença do palavra-chave struct junto com o nome do tipo, como abaixo:

    struct tComplexo comp1, comp2;

Esse comando faz o mesmo que faria com um int, float, etc…

E o acesso aos dados? Ele se dá por meio do operador . (ponto). Ele permite que você indique ao compilador qual informação específica do seu tipo de dados você deseja acessar. Por exemplo, vamos criar uma rápida função de cálculo de números complexos, baseado nesse código que mostramos. O código completo do exemplo pode ser encontrado no meu github:

struct tComplexo somaComplexo (struct tComplexo a, struct tComplexo b)
{
  struct tComplexo soma;

  soma.real=a.real+b.real;
  soma.imaginario=a.imaginario+b.imaginario;

  return soma;
}

Uma coisa importante é que, assim como no caso das strings (lembrando que strings são matrizes/ponteiros de caracteres), as estruturas não podem ser manipuladas diretamente. O que a estrutura ajuda é que podemos com elas organizarmos melhor nosso código e, assim, tornarmos ele mais legível e,. ao mesmo tempo, termos acesso à todas as demais benesses do C para qualquer tipo de dados, inclusive alocação dinâmica de memória e coisas do gênero.

Você deve estar se perguntando agora: “se uma estrutura personalizadas passa a ser considerada um tipo de dados dentro do meu programa, então posso colocar uma estrutura dentro de outra?” A resposta é SIM, você pode. Depende do compilador, mas em geral os que seguem o padrão C permitem que você “aninhe” estruturas em até 8 níveis de interação (estrutura dentro de estrutura dentro de estrutura dentro de estrutura dentro de estrutura dentro de estrutura dentro de estrutura dentro de estrutura – pior que Duna). Alguns compiladores permitem até mais níveis de “abstração”, mas vá por mim, isso vai bastar para 99,999999999999999999999999999999999999% das necessidades.

E como eu faço para “aninhar” uma estrutura dentro de outra. É simples. Primeiro crie a estrutura a ser “aninhada” (pegamos o exemplo abaixo do Curso de C da UFMG):

struct tipo_endereco
{
        char rua [50];
        int numero;
        char bairro [20];
        char cidade [30];
        char sigla_estado [3];
        long int CEP;
};

E em seguida a adicionamos como um campo dentro da estrutura que irá a receber:

struct ficha_pessoal
{
        char nome [50];
        long int telefone;
        struct tipo_endereco endereco;
};

Perceba que você continua precisando adicionar o struct [tipo] lá dentro. O acesso às informações continua a mesma. Imagina que você tem uma variável struct ficha_pessoal  de nome eu e você quer definir o estado onde mora. Para isso, você usa algo como:

strcpy (eu.endereco.estado,”SP”);

No caso de um tipo numérico, usaria-se normalmente o operador de atribuição (=), sem maiores mistérios.

Uma coisa que você deve estar pensando agora: “e se eu usar ponteiros nos tipos de dados, e como aponto memória para nosso tipo de dados?

OK… A gente vai ver um programinha de agenda que vai nos ajudar… O código dele está disponível no meu github. Ele está dividido em 5 arquivos: um Makefile genérico similar ao que vimos anteriormente, na nossa última aula sobre makefiles. Você pode copiar o que disponibilizei no github, ou então utilizar um similar ao que colocamos no final do post sobre Makefiles. Nesse caso, troque as referências a roletrando por agenda.

O segundo arquivo é o arquivo de cabeçalho do nosso projeto, agenda.h:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _agendainfo
{
  char nome[80];
  char rua[80];
  char cidade[40];
  char estado[40];
  char cep[11];
  char datanasc[11];
  char email[100];
  int sexo; // utilizaremos 0 para feminino e 1 para masculino
} agenda_info;
typedef struct _agenda
{
  agenda_info *entrada;
  struct _agenda *next;
} agenda;
void creditos (void);
void insereItem(agenda **head);
void listaAgenda(agenda *head);

Perceba que a nossa primeira estrutura (struct _agendainfo) é precedida de uma palavra-chave typedef. Essa palavra chave é usada para dar um apelido ao nosso tipo de dados. No nosso caso, utilizamos typedef para criar um apelido ao nosso tipo typedef struct _agendainfo chamado agenda_info. Isso facilita muito a coisa para a legibilidade do código.

Veja que temos uma outra estrutura struct _agenda, que tem um apelido definido via typedef chamado agenda. Essa estrutura tem duas coisas interessantes: primeiro, veja que temos um ponteiro usando o apelido agenda_info da nossa struct _agendainfo.

  agenda_info *entrada;

Essa declaração é equivalente a:

  struct _agendainfo *entrada;

Mas percebe-se que é mais legível que a entrada acima. Essa é a grande vantagem do uso de typedef e por isso usar typedef pode ser considerado (na maioria dos casos) uma boa prática.

Abaixo da declaração de nossa entrada de agenda, perceba que temos uma segunda declaração dentro dessa estrutura:

  struct _agenda *next;

“Quer dizer que posso ter uma declaração dentro de uma estrutura para uma estrutura igual?” SIM, você pode! A única regra é que normalmente você não pode usar um apelido (alias) para essa estrutura, pois nesse momento ele não sabe que o apelido definido representa a estrutura em questão… Portanto é importante tomar-se cuidado ao fazer isso…

OK… O que cada estrutura dessas faz…

Nossa estrutura agenda_info (vamos usar os alias para facilitar a leitura) representa os dados a serem usados na agenda para cada entrada individual. Já a nossa estrutura agenda é quem vai realmente montar a agenda. Para isso, vamos usar uma estrutura de dados.

“Como assim?”. No caso, vamos criar uma estrutura de fila, onde cada item será lido um após o outro e terá um ponteiro para o item seguinte (struct _agenda *next;) Por convenção, vamos usar null nesse ponteiro quando quisermos indicar o final da lista.

E qual é a idéia? Cada item agenda irá apontar para dois itens: a entrada de dados (agenda_info *entrada;) e o já citado ponteiro para o próximo item. Veremos no main que o sistema terá como saber onde fica o começo da lista.

Além dessas declarações de dados, declaramos três funções:

void creditos (void);
void insereItem(agenda **head);
void listaAgenda(agenda *head);

A primeira é meio clara: apenas irá exibir créditos. A segunda tem uma estrutura estranha: foi declarada uma função insereItem,, com um parâmetro agenda **head. O que queremos dizer aí?

Ponteiros de Ponteiro:

Você deve ter percebido que usamos dois *, ou seja, dois símbolos de indicação de endereço. Aqui temos uma situação bastante comum em programação C. Se você lembra anteriormente, que falamos que tem como modificar o endereço de uma variável quando a passamos por referência. Nesse caso, utilizamos exatamente isso, um ponteiro para um ponteiro. Ou seja, ao invés de alterarmos o endereço que leva ao conteúdo, alteramos o endereço que leva ao endereço de memória onde está o conteúdo em questão… É um pouco complexo isso. Por enquanto basta saber que iremos alterar o local de memória de uma determinada varíavel… Isso irá garantir que consigamos acessar corretamente o sistema.

Vamos ver o main.c desse nosso projeto:

 

#include “agenda.h”

 

void limpaDados (agenda *minhaAgenda);

 

int main(void)

{

  agenda *minhaAgenda=NULL;

  int op;

  creditos();

  do

    {

      printf(“Escolha uma das opções abaixo para a sua agenda\n\n”);

      printf(“1 – Inserir um novo registro\n”);

      printf(“2 – Listar os registros inseridos\n”);

      printf(“3 – Sair\n”);

      printf(“4 – Créditos\n\n\nEscolha sua opção:”);

      scanf(“%d”,&op);

      while(getchar()!=’\n’);

      switch(op)

        {

        case 1:

          insereItem(&minhaAgenda);

          break;

        case 2:

          listaAgenda(minhaAgenda);

 

          break;

        case 3:

          limpaDados(minhaAgenda);

          return(0);

        case 4:

          creditos();

          break;

        default:

          printf(“Opção inválida!\n”);

        }

    } while (op!=3);

}

 

void creditos(void)

{

    printf(“Programa de agenda simples do curso de C livre do Aulas de C\n\n\n\n”);

    printf(“Autor…: Fábio Emilio Costa <fabiocosta0305@gmail.com>\n”);

    printf(“Licença.: GPL 2\n\n”);

}

 

void limpaDados (agenda *minhaAgenda)

{

  agenda *next,*now;

 

  next=minhaAgenda;

 

  while(next!=NULL)

    {

      now=next;

      next=next->next;

 

      free(now->entrada);

      free(now);

    }

}

Nenhum mistério nesse main.c: usamos nosso cabeçalho personalizado agenda.h para importar todos os cabeçalhos que precisamos e também trazer os tipos agenda e agenda_info que usaremos no nosso código. Perceba que marcamos em vermelho a declaração de nossa agenda (na variável minhaAgenda) e em seguida, passamos o endereço aonde está essa informação com o comando insereItem(&minhaAgenda). Perceba que utilizamos o operador de de-referenciamento (&) para obtermos o endereço onde o C irá guardar o endereço para o ponteiro da nossa estrutura da agenda. Isso permitirá que possamos alterar o conteúdo desse ponteiro dentro da nossa função. O resto do código não tem muito mistério, e em geral não deve ser de maior complexidade se você estudou corretamente nosso “curso” até aqui e também fez com calma todos os exemplos que já mostramos.
OK… E como é feita a inserção dos dados? Para isso, utilizamos os códigos de nosso arquivo insert.c:
#include “agenda.h”
void limpandoDados(agenda_info *dados)
{
  memset(dados->nome,”,sizeof(dados->nome));
  memset(dados->rua,”,sizeof(dados->rua));
  memset(dados->cidade,”,sizeof(dados->cidade));
  memset(dados->estado,”,sizeof(dados->estado));
  memset(dados->cep,”,sizeof(dados->cep));
  memset(dados->datanasc,”,sizeof(dados->datanasc));
  memset(dados->email,”,sizeof(dados->email));
  dados->sexo=0;
}
void insereItem (agenda **head)
{
  agenda_info *dados=(agenda_info*)malloc(sizeof(agenda_info));
  agenda *entrada=(agenda*)malloc(sizeof(agenda)), *hook;
  char sexo=’M’;
  if ((!entrada) || (!dados))
    exit(1);
  limpandoDados(dados);
  printf(“Digite o nome da pessoa, ou então FIM se entrou por engano: \n”);
  fgets(dados->nome,sizeof(dados->nome),stdin);
  if(strncmp(dados->nome,”FIM”,strlen(“FIM”))==0)
    {
      free(dados);
      return;
    }
  printf(“Digite a rua onde essa pessoa mora: “);
  fgets(dados->rua,sizeof(dados->rua),stdin);
  printf(“Digite a cidade onde essa pessoa mora: “);
  fgets(dados->cidade,sizeof(dados->cidade),stdin);
  printf(“Digite o estado onde essa pessoa mora: “);
  fgets(dados->estado,sizeof(dados->estado),stdin);
  printf(“Digite o cep onde essa pessoa mora: “);
  fgets(dados->cep,sizeof(dados->cep),stdin);
  printf(“Digite o email dessa pessoa: “);
  fgets(dados->email,sizeof(dados->email),stdin);
  printf(“Digite a data de nascimento dessa pessoa: “);
  fgets(dados->datanasc,sizeof(dados->datanasc),stdin);
  do
    {
      printf(“Digite o sexo [M/F]: “);
      scanf(“%c”,&sexo);
      getchar();
      sexo=((sexo>=’a’)&&(sexo<=’z’))?sexo+’A’-‘a’:sexo;
      printf(“%c\n”,sexo);
      if (sexo!=’M’&&sexo!=’F’) printf (“sexo inválido\n”);
    } while (sexo!=’M’&&sexo!=’F’);
  dados->sexo=(sexo==’F’)?0:1;
  /**
   *  Checa se já existem itens dentro da agenda
   */
  if (!*head)
    {

      *head=entrada;
      (*head)->next=NULL;
    }
  else
    {
      hook=*head;
      while (hook->next!=NULL) hook=hook->next;
      hook->next=entrada;
    }
  entrada->entrada=dados;
  return;
}
Perceba que aqui temos alguns segredos que irão nos ajudar a montar nossa lista de dados:
Primeiro, perceba que alocamos memória para uma entrada de dados de agenda, na variável dados, e uma entrada para a agenda, chamada entrada, além de declararmos uma terceira variável de agenda, chamada hook (gancho, em inglês). Essa variável irá ser usada para fazer uma “corrida” para achar o final da agenda e colocar nela os dados necessários. O preenchimento de dados não possui grandes mistériosd, exceto que utilizamos, ao invés do operador . (ponto), utilizamos o operador -> (que chamaremos de operador seta, e é composto por um hifen e um sinal de maior). Também não precisamos usar o * para indicar que você quer alterar a informação apontada, como em, dados->rua. Lemos essa entrada como “o elemento rua da estrutura apontada por dados“. O resto é como já vimos anteriormente. Usamos uma função utilitária para inicializar corretamente a estrutura como se deve. Agora, vamos falar sobre o código abaixo… 
  if (!*head)
    {

      *head=entrada;
    }
  else
    {
      hook=*head;
      while (hook->next!=NULL) hook=hook->next;
      hook->next=entrada;
    }
  entrada->entrada=dados;
  entrada->next=NULL;

É esse código que faz toda a parte da inserção de dados. Perceba que testamos para ver se o valor apontado pelo endereço obtido no início da função e passado pelo main() é NULL (if (!*head)): isso irá ocorrer apenas uma vez, logo no início do programa. Quando isso acontecer, ele irá pegar e substituir o endereço apontado por *head, pelo endereço entrada. Caso contrário, ele irá utilizar o seguinte procedimento para “correr” a lista até o fim:

  1. Usando a variável hook, irá armazenar a variável o endereço apontado por *head;
  2. Em seguida, irá associar o endereço do próximo item apontado na variável hook (o item em questão) à própria variável hook, até alcançar último item (aquele cujo next for NULL). Lembre-se desse procedimento: veremos ele novamente mais adiante;

Agora que sabemos onde a fila termina, adicionamos no novo item à fila, modificando o next do item apontado em hook pelo endereço de entrada, com isso “amarrando” essa entrada à lista.

Após ambos os caso, associamos os dados entrados ao valor de entrada o endereço de nossa estrutura, e definimos next como NULL, o que indicará que esse é o último item (não faremos organização das entradas para facilitar).

E como funcionará a visualização desses itens?

Bem, vamos ver o código de exibição dos dados:

#include “agenda.h”

 

void listaAgenda(agenda *head)

{

  agenda *item;

  agenda_info *registro;

  int counter=0;

  item=head;

 

  while (item!=NULL)

    {

      registro=item->entrada;

      counter++;

 

      printf(“Registro no. %d\n\n”,counter);

      printf(“Nome…..: %s\n”,registro->nome);

      printf(“Endereco.: %s\n”,registro->rua);

      printf(“Cidade…: %s\n”,registro->cidade);

      printf(“Estado…: %s\n”,registro->estado);

      printf(“CEP……: %s\n”,registro->cep);

      printf(“DataNasc.: %s\n”,registro->datanasc);

      printf(“Email….: %s\n”,registro->email);

      printf(“Sexo…..: %c\n”,(registro->sexo==0)?’F’:’M’);

 

      printf(“Pressione qualquer tecla para continuar!\n”); while(!getchar());

 

      item=item->next;

    } 

 

  printf(“Exibidos %d registros\n”, counter);

  return;

}

É bem simples… Lembra da “corrida” que fazemos na entrada de dados? Basicamente fazemos exatamente a mesma coisa: seguimos o seguinte procedimento até acharmos o último item da nossa lista:
  1. Armazenamos em registro o ponteiro para os dados que iremos exibir;
  2. Exibimos os dados utilizando printf (como de costume);
  3. E associamos o ponteiro next e atribuímos a ela o valor de item;
Esse procedimento irá se repetir até que o valor de item seja NULL…
Antes de encerrarmos, vamos falar de uma rotina importante que é uma boa prática no desenvolvimento de qualquer sistema com alocação dinâmica de memória, mas especialmente em programas como nosso, onde usamos intensivamente esse procedirmento. Esse procedimento ocorre no nosso programa apenas no encerramento do mesmo. Observe o seguinte código em main.c:
void limpaDados (agenda *minhaAgenda)
{
  agenda *next,*now;
  next=minhaAgenda;
  while(next!=NULL)
    {
      now=next;
      next=next->next;
      free(now->entrada);
      free(now);
    }
}
Essa função é chamada apenas no final do programa Note que ele segue a mesma idéia de sempre quando falamos na fila, portanto guarde esse procedimento na mente:
  1. Ele atribui o valor de next para um ponteiro temporário now (importante fazer isso para que não haja erros de lógica onde o sistema “pire”).
  2. Em seguida, como vamos usar apenas o ponteiro now na liberação de memória, podemos passar  para next o valor do próximo item (next);
  3. Após isso, liberamos primeiro os dados (com free(now->entrada)) e em seguida, liberamos a entrada (free(now));
  4. E então repetimos o processo, enquanto o next não for NULL;
É uma boa prática desalocar toda a memória usada durante o uso do programa. A maioria dos sistemas operacionais modernos conseguem detectar a memória usada pelo programa (incluse do heap, onde é armazenada toda a informação alocada dinamicamente) e eliminá-la, principalmente porque em geral cada programa recebe uma determinada quantidade de memória no momento em que ele executar. Ainda assim, existe a possibilidade de memória alocada não ser liberada no momento do encerramento do programa, formando o que se chama de memory leakage (vazamento de memória), pois para o SO essa memória ainda está alocada, só não sabendo-se por quem. Ao desalocar explicitamente a memória, você diminiu e muito a chance de um memory leakage. Lermbre-se sempre dessa máxima em C:
“Aloque ao entrar e desaloque ao sair”
Com essa máxima, terminamos nossa “aula”. Guarde esse código, pois o usaremos em nossa próxima aula, onde falaremos de arquivos em disco.
Como brincadeiras, sugiro:
  1. Se você reparar, não removemos o ENTER na entrada dos dados. Tente criar uma função que remova os terminadores ‘\ n‘ das entradas de dados;
  2. Quando falamos que não iríamos organizar os dados, há um jeito: lembre-se que você pode apontar informações de ponteiros e estrururas dentro de outras estruturas. Tente criar um código que permita que você “ordene” os dados armazenados. Procure informações sobre a função strcomp para algumas idéias;

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: