linguagem de programação c

70
Linguagem de programação C C é uma linguagem flexível e poderosa que possui características de linguagens de alto nível (como o basic ou cobol) e outras de baixo nível (como assembly), sendo muitas vezes considerada como uma linguagem de nível médio. É uma linguagem tipicamente compilada (ou seja, o programa é totalmente convertido para um código legível pela máquina antes de ser executada) e permite liberdade total ao programador, que é responsável por tudo que acontece, possibilitando um bom controle e objetividade em suas tarefas. Este curso dará uma base introdutória para a programação em plataformas UNIX/Linux usando a linguagem C no padrão ANSI. Como o padrão ANSI não aborda recursos como elementos gráficos, multithreading, comunicação entre processos e comunicação em redes, esses temas não serão abordados nesse curso. O curso terá duração de 4 semanas e o conteúdo será disponibilizado em etapas, no início de cada semana. Antes de começar o curso, o aluno deverá ler o Plano de Ensino e o Guia do Aluno a seguir. Aos iniciantes na plataforma Moodle, recomendamos que leia a Ambientação do Moodle a seguir. Introdução à linguagem C Esta seção aborda aspectos teóricos da linguagem C e a sintaxe geral desta linguagem. Lição 1 - Introdução teórica O que é a linguagem de programação C? #include <stdio.h> #define MAX 100 int main (int argc , char *argv[]) { int i; for ( i = 0 ; i < MAX ; i++ ) { printf("Este curso sera sobre a linguagem C!! \n"); } } #undef MAX Programa que imprime o texto "Este curso será sobre a linguagem C!!" 100 vezes no console. C é uma linguagem que alia características de linguagens de alto nível (como pascal e basic) e outras de baixo nível como assembly (linguagem de montagem para comandos específicos da máquina), logo, é freqüentemente conhecida como uma linguagem de nível médio (ou intermediário) por permitir também facilidade de acesso ao hardware e facilitar a integração com comandos assembly. Esta linguagem foi originada da linguagem de programação B (criada por Ken Thompson), que por sua vez foi originada da linguagem de programação BCPL (criada por Martin Richards). Pode ser interessante analisar essas linguagens para avaliar algumas características herdadas, mas isso não será feito neste curso. O que isso quer dizer? Que C junta flexibilidade, praticidade e simplicidade. Adicionalmente, C permite

Upload: api-3838125

Post on 07-Jun-2015

3.689 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Linguagem de Programação C

Linguagem de programação C

C é uma linguagem flexível e poderosa que possui características de linguagens de alto nível (como o basic ou cobol) e outras de baixo nível (como assembly), sendo muitas vezes considerada como uma linguagem de nível médio. É uma linguagem tipicamente compilada (ou seja, o programa é totalmente convertido para um código legível pela máquina antes de ser executada) e permite liberdade total ao programador, que é responsável por tudo que acontece, possibilitando um bom controle e objetividade em suas tarefas.

Este curso dará uma base introdutória para a programação em plataformas UNIX/Linux usando a linguagem C no padrão ANSI. Como o padrão ANSI não aborda recursos como elementos gráficos, multithreading, comunicação entre processos e comunicação em redes, esses temas não serão abordados nesse curso.

O curso terá duração de 4 semanas e o conteúdo será disponibilizado em etapas, no início de cada semana. Antes de começar o curso, o aluno deverá ler o Plano de Ensino e o Guia do Aluno a seguir. Aos iniciantes na plataforma Moodle, recomendamos que leia a Ambientação do Moodle a seguir.

Introdução à linguagem C

Esta seção aborda aspectos teóricos da linguagem C e a sintaxe geral desta linguagem. Lição 1 - Introdução teórica

O que é a linguagem de programação C?

#include <stdio.h>#define MAX 100int main (int argc , char *argv[]) { int i; for ( i = 0 ; i < MAX ; i++ ) { printf("Este curso sera sobre a linguagem C!! \n"); }}#undef MAX

Programa que imprime o texto "Este curso será sobre a linguagem C!!" 100 vezes no console.

C é uma linguagem que alia características de linguagens de alto nível (como pascal e basic) e outras de baixo nível como assembly (linguagem de montagem para comandos específicos da máquina), logo, é freqüentemente conhecida como uma linguagem de nível médio (ou intermediário) por permitir também facilidade de acesso ao hardware e facilitar a integração com comandos assembly.

Esta linguagem foi originada da linguagem de programação B (criada por Ken Thompson), que por sua vez foi originada da linguagem de programação BCPL (criada por Martin Richards). Pode ser interessante analisar essas linguagens para avaliar algumas características herdadas, mas isso não será feito neste curso.

O que isso quer dizer? Que C junta flexibilidade, praticidade e simplicidade. Adicionalmente, C permite liberdade total ao programador que é responsável por tudo que acontece, nada é imposto ou acontece simplesmente ao acaso, tudo é pensado pelo programador e isso significa um bom controle e objetividade em suas tarefas, o que muitas vezes não é conseguido em diversas outras linguagens.

C é uma "linguagem estruturada", ou seja, são linguagens que estruturam o programa em blocos para resolver os problemas. Você divide um grande e complexo problema em um conjunto de problemas mais simples de serem resolvidos.

C é uma linguagem compilada e utiliza de um compilador C para ser executado, ao contrário de outras linguagens que utilizam de um interpretador para tal (como o prolog ou o Java Bytecode). Na concepção da linguagem é que se decide se ela vai ser

Page 2: Linguagem de Programação C

compilada ou interpretada, pois todas as linguagens têm seu objetivo a cumprir (como prioridade a velocidade ou a flexibilidade) e o método de tradução (compilação ou interpretação) tem impacto no cumprimento desses objetivos. A priori, qualquer uma poderia ser interpretada ou compilada, mas linguagens que priorizam flexibilidade e portabilidade são interpretadas e as linguagens que priorizam a velocidade são compiladas.

Na verdade, quem faz um programa ser executado é também um programa, só que um programa avançado que lê todo código fonte (o que foi escrito pelo programador) e o traduz de alguma forma para ser executado. Isso acontece em todas linguagens. A diferença básica é que um interpretador lê linha a linha do fonte, o examina sintaticamente e o executa.

Cada vez que o programa for executado esse processo tem de ser repetido e o interpretador é chamado. Já um compilador lê todo programa e o converte para código-objeto (código de máquina, binário, 0's e 1's) e pronto. Sempre quando tiver que ser executado é só chamar o código convertido, pois todas instruções já estão prontas para a execução, não tendo mais vínculo com seu código-fonte.

A linguagem C também foi projetada visando a portabilidade, ou seja, para que o mesmo código escrito em C possa ser utilizado para gerar diversos programas executáveis especializados para diferentes arquiteturas de máquina. Note que o código escrito em linguagem C é portável, mas o programa executável gerado por ele não o é.

Todas as páginas desta lição virão com um exemplo não tão complexo de código em linguagem C. Eles são para fins ilustrativos. Os interessados podem tentar compilar o código ou analisar o que eles fazem.

Quem realmente controla os programas?

Quem controla as ações mais básicas de um computador é o Sistema Operacional (SO) . O SO é o que podemos chamar de camada de software que faz a interface (comunicação) entre os usuários e o Hardware (parte física da máquina, placas, circuitos, memórias). O objetivo básico é controlar as atividades do Hardware e prover um ambiente agradável para o usuário do sistema e que ele possa trabalhar com maior grau de abstração (se preocupar menos com problemas relativos aos detalhes do funcionamento da máquina e poder pensar mais na essência da resolução de um problema). Qualquer comando de um programa convencional precisa sempre passar pelo "filtro" do Sistema Operacional antes de ser executada.

O SO tem alguns componentes primordiais para a interação do usuário com a máquina: o escalonador de processos, o gerenciador de memória, o gerenciador de entrada e saída, o sistema de arquivos e o interpretador de comandos .

O escalonador de processos (task scheduler) é uma parte do SO que controla o uso de recursos computacionais por processos (um processo é um programa em execução). Os escalonadores atuais tentam permitir que vários processos rodem quase que paralelamente em um computador e de forma eficiente. Por exemplo, caso um processo fique muito tempo ocioso esperando por um recurso que demora para ser liberado, o escalonador deve fazer com que um outro processo entre em execução enquanto que o primeiro esteja esperando. Podemos dizer basicamente que o escalonador de processos é o responsável pela eficiência de um SO como todo.

O gerenciador de memória é, como o nome já diz, o responsável pelo uso da memória. É basicamente ele quem controla como os processos (os programas em execução) devem utilizar a memória.

É através do gerenciador de entrada e saída que o SO coordena todo tráfego de saída e entrada de informações. Por exemplo, ele determina o que vai para a tela, o que vem do teclado, movimentos do mouse, etc.

O sistema de arquivos provê ao usuário uma abstração muito conhecida como "arquivos" (e adicionalmente as "pastas"). É ele quem verifica o conjunto de 1 e 0 (os famosos "bits") presentes nos dispositivos de armazenamento (atualmente o mais comum é o HD) e o "traduz" para que o usuário tenha a impressão de que nesses dispositivos realmente existam os "arquivos", não meramente os "bits".

O interpretador de comandos é uma interface primária entre o usuário e o SO. Permite que o usuário e o SO se "comuniquem" usando uma linguagem em comum. O interpretador de comandos é conhecido no mundo Linux como SHELL (pode variar para outros sistemas).

Histórico da linguagem C

#include <stdlib.h>int * buscaBin (int *p1, int tam , int elem) {

int *p2 = p1 + tam - 1;int *p = p1 + ((p2 - p1)/2);while ( p1 <= p2 ) {

if (*p == elem) {return p;

} else if (*p < elem) {p1 = p + 1;

} else {p2 = p - 1;

}p = p1 + ((p2 - p1)/2);

}return NULL;

}

Page 3: Linguagem de Programação C

Função de busca binária em um vetor de elementos inteiros ordenados.

Em 1973 Dennis Ritchie, pesquisador da Bell Labs, reescreveu todo sistema UNIX para uma linguagem de alto nível (na época considerada) chamada C, desenvolvida por ele mesmo. Esse sistema foi utilizado para um PDP-11 (o microcomputador mais popular na época).

Tal situação de se ter um sistema escrito em linguagem de alto nível foi inovador na época e pode ter sido um dos motivos da aceitação do sistema por parte dos usuários externos a Bell. Sua popularização tem relação direta com o exponencial uso do C.

Por vários anos o padrão utilizado para o C foi o que era fornecido com o UNIX versão 5 (descrito em The C programming Language, de Brian Kernighan e Dennis Ritchie - 1978). Começaram a surgir diversas implementações da tal linguagem e os códigos gerados por tais implementações eram altamente incompatíveis. Não existia nada que formalizasse essas compatibilizações e com o aumento do uso desses diversos "dialetos" da linguagem, surgiu-se a necessidade de uma padronização, regras que todos poderiam seguir para poderem rodar seus programas no maior número de plataformas possíveis.

O ANSI (American National Standards Intitute, Instituto Americano que até hoje dita diversos padrões) estabeleceu em 1983 um comité responsável pela padronização da linguagem. Atualmente, a grande maioria dos compiladores já suportam essa padronização (ou melhor, é quase uma obrigação suportar o padrão ANSI).

Trocando em miúdos, o C pode ser escrito em qualquer máquina que se utilize de tal padrão e rodar em qualquer outra que também o faça. Parece inútil? Não. Na verdade, isso (a portabilidade) é a semente de grande parte dos avanços tecnológicos que toda programação tem proporcionado no mundo de hoje. Com o tempo isso ficará mais claro.

Características

#include <stdio.h>

unsigned long int minhaSequencia (unsigned int indice) { unsigned long int sequencia[] = { 1 , 1 }; register char selecionador = 0; for ( ; indice > 0 ; indice-- , selecionador = ++selecionador % 2 ) {

sequencia[selecionador % 2] = sequencia[0] + sequencia[1]; } return sequencia[selecionador % 2];

}

int main (int argc , char *argv[]) {

int i;

for (i = 0 ; i < 10 ; i++) {

printf("\n %d \n" , minhaSequencia(i));

}

return 0;

}

Programa que imprime 10 elementos da seqüência de Fibonacci

Listamos abaixo algumas características da linguagem C:

1. Projetado inicialmente para o desenvolvimento de softwares básicos (softwares de base, que provém serviços para outros softwares específicos) de forma independente do hardware;

2. Foi projetado para ser usado por programadores especializados interessados em geração de códigos compactos e eficientes;

3. O gerenciamento de memória é por cargo do programador (não existe a coleta de lixo, como ocorre na linguagem Java), ou seja, o programador é quem especifica a alocação e a liberação de memória utilizada por um dado;

4. Economia de expressões (as expressões em C utilizam poucas letras);5. Moderno controle de fluxo e estruturas de dados. Construções para o controle de fluxo do programa é bem estruturada e

é possível criar novas estruturas de dados de forma flexível;6. Possui um conjunto rico e poderoso de operadores;7. Elementos dependentes de hardware estão integradas em bibliotecas de funções, logo, os programas convencionais não

precisam se preocupar com esses elementos;8. Performance próxima das obtidas com a linguagem Assembly;9. São "case sensitives", ou seja, diferem letras maiúsculas das minúsculas;10. O entrypoint (o ponto inicial de execução do programa) é declarada através da função "main()";11. Os dados são tipados, ou seja, devem ter o "tipo" explicitamente declarado;

Page 4: Linguagem de Programação C

12. Os tipos de dados declarados pelo programador (os que não foram especificados pela linguagem C, como o int, float, char, ...) são diferenciados pelo tamanho (número de bytes que um dado ocupa), não pelo nome atribuído ao tipo.

O compilador GCC

#include <stdio.h>#include <stdlib.h>void andVetor (int vetor1[], int vetor2[] , int tamVetor) { int *p1 = vetor1; int *p2 = vetor2; for ( ; tamVetor > 0 ; tamVetor-- , p1++, p2++ ) {

*p1 = *p1 && *p2; }}int main (int argc , char *argv[]) { int i; int v1[] = {1,1,0,1,1}; int v2[] = {1,0,1,1,0}; andVetor(v1,v2,5); for (i = 0 ; i < 5 ; i++) {printf("\n %d \n" , v1[i]);} return 0;}

Programa que imprime o resultado da operação lógica "E" aplicado em um vetor.

O GCC (GNU Compiler Collection) é uma distribuição integrada de compiladores de diversas linguagens de programação, que inclui C, C++, Objective-C, Objective-C++, Java, Fortran, e Ada. Historicamente, o GCC é mais conhecido como "GNU C Compiler" por seu uso comum ser a compilação da linguagem C.

Neste curso, utilizaremos o GCC como o compilador principal. Este curso dará somente uma visão rápida do GCC, pois este recurso é bastande diversificado e um curso completo pode ser feito para ensinar o uso avançado do GCC.

Façamos uma revisão rápida:

Um programa em C é elaborado em dois passos básicos:

O programa é escrito em texto puro num editor de texto simples. Tal programa se chama "código fonte" (source code em inglês);

Passamos o código fonte para o compilador que é o programa que gera um arquivo num formato que a máquina entenda.

O compilador trabalha, em geral, em 4 fases básicas:

1. O compilador realiza a "pré-compilação", ou seja, processa comandos especiais direcionados para o compilador (as diretivas de compilação) e ignora elementos redundantes (como espaços em branco ou comentários);

2. O compilador avalia o código fonte para detectar erros léxicos, sintáticos e os erros semânticos simples;3. O compilador gera, através do código fonte, um código intermediário em uma "gramática" mais simples para a máquina

(geralmente uma linguagem assembly). Posteriormente, o montador (assembler) gerará um arquivo "objeto" para cada código fonte. Alguns compiladores não passam pela linguagem assembly e geram diretamente o código objeto;

4. Depois, o ligador (linker) junta o arquivo objeto com a biblioteca padrão. A tarefa de juntar todas as funções do programa é bastante complexa. Nesse estágio, o compilador pode falhar se ele não encontrar referências para a função.

Freqüentemente referimos o termo "compilador" como a junção do "pré-compilador" (que faz a pré-compilação), o "analisador" (realiza a analização léxica, sintática e parte da semântica), um "gerador de código de baixo nível" (converte um programa em linguagem de alto nível em outra de baixo nível), o montador/assembler (converte um programa em linguagem assembly para um arquivo objeto) e o ligador/linker (junta diversos arquivos objeto para gerar o programa completo e executável pela máquina).

Para usar o GCC para compilar seu programa, use:

$ gcc fonte.c

Isso já efetua a pré-compilação, a compilação, a montagem (assembly) e a ligação (link), gerando um programa executável.

Na maioria dos computadores, isso gerará um arquivo chamado "a.out". Para executar esse programa, digite "./a.out". Para mudar o nome do programa executável gerado, você pode utilizar a opção -o.

Page 5: Linguagem de Programação C

$ gcc -o destino fonte.c

O seu programa se chamará destino e será o derivado da fonte chamada fonte.c.

Se você deseja compilar um programa "fonte1.c" que utiliza "fonte2.c", "fonte3.c" e "fonte4.c" como biblioteca, execute:

$ gcc -o destino fonte1.c fonte2.c fonte3.c fonte4.c

Isso criará um arquivo "destino" que foi gerado de "fonte1.c", que por sua vez utilizou "fonte2.c", "fonte3.c" e "fonte4.c" como biblioteca.

Tipos de Erros

#include <stdio.h>

#define FIM 255

unsigned char seq[] = {

3 ,

1 , 4 , 1 , 5 , 9 ,

2 , 6 , 5 , 3 , 5 ,

8 , 9 , 7 , 9 , 3 ,

2 , 3 , 8 , 4 , 6 ,

2 , 6 , 4 , 3 , 3 ,

8 , 3 , 2 , 7 , 9 ,

FIM

};

int main( int argc , char * argv[] ) {

int i = 0;

printf("\n Seq = (");

for ( ; (seq[i]!=FIM)?(printf(" %u ",seq[i]),1) : (0) ; i++ ) {}

printf(");\n");

return 0;

}

#undef FIM

Programa que imprime 30 casas decimais do PI.

Dito de forma radical, erros são provocados sempre pelo programador . Existem basicamente dois tipos de erros:

1. Léxico: relacionado ao formato da palavra (como o uso de letras erradas). Esse tipo de erro ocorre normalmente ao usar letras/símbolos inesperados nas palavras (por exemplo, ao usar símbolos como o "@" para nomes de variáveis - o que não é permitido no C);

2. Sintático: relacionado com a organização entre palavras. Erros desse tipo ocorrem quando duas ou mais palavras (ou letras que representam algum elemento da linguagem) estão colocadas em posições erradas ou quando palavras esperadas não se encontram no local devido (por exemplo, para cada "{" deve haver um "}" correspondente; se não houver, será erro sintático);

3. Lógico/Semântico: os demais erros se enquadram nesta categoria. Erros de lógica de programação (como loop infinito, abertura de arquivos que não existem, etc), erros matemáticos (divisão por zero), erros de ordem de operação, erros de tipos (quando se utiliza um dado de um tipo diferente do esperado - inteiros quando se espera ponteiros), etc. Erros desta categoria são de difícil detecção.

Page 6: Linguagem de Programação C

Os erros de sintaxe são os melhores que podem acontecer (claro, o ideal é que não ocorram erros, mas nós, como seres humanos, sempre cometemos erros nos piores momentos possíveis). O compilador o identificará durante a compilação do programa, ou seja, não gerará um efeito inesperado durante a execução do programa. Os erros léxico/sintático são gerados quando criamos programas que estão fora da construção esperada pela gramática da linguagem.

Em geral, quando os compiladores encontram um erro eles não terminam imediatamente, mas continuam procurando até o final do programa e assim listam todos os erros detectados e do que se tratam. O programa é somente compilado até o final (geram códigos executáveis) quando erros léxicos e sintáticos não mais existirem.

É bastante traumatizante para alguém fazendo seu primeiro programa obter um erro e ver diversas linhas de mensagens que aparentemente não fazem sentido. Não se assuste nem se desmotive, pois pode ser um simples ponto e vírgula ou um detalhe bobo. Os compiladores não são inteligentes o suficiente para identificar exatamente qual foi o erro e indicar soluções para isso. Com o tempo e a experiência você começará a se acostumar e aprender a lidar com isso.

Caso erros léxico-sintáticos não estejam mais presentes, o compilador transformará o seu código fonte (texto puro) em código de máquina (os tais 0's e 1's, o que a máquina entende) e você poderá executa-lo. Bem, mas se a lógica do programa estiver errada? Este tipo de erro não pode ser detectado pelo compilador.

Erros lógicos se assemelham a entregar uma receita de bolo de mandioca quando se espera um bolo de fubá. Se a receita estiver gramaticalmente correta, o cozinheiro pode perfeitamente transformar a receita em algum produto, mas não estará preocupado se realmente foi esse produto que o cliente queria (nem estará preocupado se esse produto será digerível por algum ser humano). No caso de um compilador, o compilador somente transformará o código fonte para código de máquina e não verificará se o programa descrito pelo código fonte realmente resolverá o problema desejado.

Fazendo uma analogia:

1. Você pode falar com seu empregado e ele não entender o que você está expressando e assim ele não conseguirá executar a tarefa e reclamará. Por exemplo, falando em japonês com ele! Nesse caso, houve um erro léxico/sintático.

2. Você explica tudo gramaticalmente correto para o seu empregado. Ele entende tudo, porém a idéia passada é inconsistente. Por exemplo, manda ele ir para uma rua que não existe ou comprar algo sem que haja dinheiro suficiente. Neste caso, o erro foi de lógica.

Tais erros podem acarretar algumas conseqüências graves como:

O programa termina repentinamente e às vezes dê uma advertência inesperada; O programa funciona incorretamente e gera dados inconsistentes; O programa leva o sistema a um estado instável ou lento.

Alguns erros lógicos simples podem ser detectadas pelo compilador (como diferenças de tipos de dados ou visibilidade/escopo), mas no geral os erros lógicos podem ser resolvidos somente por inspecção e avaliação lógica.

Detalhes Importantes

Depois de corrigir um erro no código fonte, você deve compilar o programa novamente para que este tenha efeito, caso contrário o executável não reflitirá o código fonte atual. O compilador não detectará automaticamente essas mudanças e compilará automaticamente.

O C, como tudo no mundo Linux/UNIX, difere as letras maiúsculas das minúsculas (o dito "case sensitive" em inglês); esse critério deve ser considerado com cuidado. Existem padrões que normalizam o uso de maiúsculas e minúsculas em nomes utilizados nos programas C. No geral, recomenda-se priorizar o uso de minúsculas, com exceção dos dados constantes, strings e nomes compostos (por exemplo, nome de variáveis como "minhaVariavel" ou nomes de tipos de dados como "meuTipo").

Outro detalhe importante: o C (como a maioria das linguagens atuais) exige que se faça uma listagem de todas as variáveis do programa previamente. Ou seja, não existe uso dinâmico de variáveis e tudo que você usa tem que ser previamente declarado.

O ";" (ponto-e-vírgula) é o "terminador de comando" no C, ou seja, é o indicador de fim de comando. Diferente das linguagens como o Pascal (que possui "separador de comando", não o "terminador"), todos os comandos em C (com exceção de alguns blocos de comando) devem ser terminados com o ponto-e-vírgula. Em linguagens com "separadores de comandos" (como Pascal), o último comando não precisa desse elemento, pois o último comando não tem algo a seguir para precisar separar.

Lição 2 - Elementos léxicos e sintáticos da linguagem C, parte 1

Palavras Reservadas

Um dos elementos léxicos mais importantes de uma linguagem de programação são as palavras reservadas. As palavras

Page 7: Linguagem de Programação C

reservadas, os operadores (unários, binários e ternários), os símbolos terminadores de comandos e os delimitadores de blocos/expressões formam os elementos léxicos constantes da gramática da linguagem C.

O que vem a ser palavras reservadas? São palavras que têm significado especial na linguagem. Cada palavra tem significado especial em C e as instruções são executadas através do uso desses conjuntos de palavras. Existem algumas palavras chaves que são previamente determinadas pelo projeto da linguagem. Chamamos essas palavras de palavras reservadas. A priori, elas não poderão ser usadas para fins além do determinado pela linguagem. As funções que cada palavra chave exerce serão esclarecidas no decorrer do curso.

Um compilador C precisaria ser muito inteligente para poder distinguir, através da análise de contexto, as palavras reservadas dos comuns casos a mesma seqüência de letras possam ser utilizadas tanto como reservadas em alguns casos quanto como nomes próprios em outros. Por isso, foi determinado que as palavras reservadas seriam utilizadas somente para seus fins designados (que são geralmente comandos e afirmativas) .

Abaixo está a lista dessas palavras. Relembrando, o C entende tais palavras apenas em letras minúsculas (não funcionará se você colocar em maiúsculas).

auto double int struct

break else long switch

case enum register typedef

char extern return union

const float short unsigned

continue for signed void

case goto sizeof volatile

do if signed while

Todo conjunto de palavras reservadas acima são o conjunto das instruções básicas do C. Aparentemente, parecem poucas e você, na prática, usará apenas algumas poucas delas. Tal fato acontece pois uma das facilidades do C é a utilização muito natural de bibliotecas que funcionam como acessórios para o C.

As bibliotecas (conjunto de funções) não fazem parte intrínseca do C, mas você não encontrará nenhuma versão do C sem nenhuma delas. Algumas são até tratadas como parte da linguagem por serem padronizadas.

Variáveis

São espaços reservados que guardam valores durante a execução de um programa. Como o nome diz, elas tem a capacidade de "variar" no tempo. Em geral, são exatamente um pedaço de memória (o tamanho depende do que se esteja guardando) no qual botamos alguma informação e podemos nos referir a ela, utilizá-la, alterá-la ou fazermos o que bem entendermos durante a execução de um programa.

Nome das variáveis

Toda variável tem um nome pela qual é chamada (identificada) e um tipo (o que ela guardará). Os nomes podem ser de uma letra até palavras.

Dizendo de forma simplificada, os nomes das variáveis obrigatoriamente devem começar por uma letra ou underscore (o sinal de menos achatado: "_"). O restante pode ser letras de A até Z maiúsculas, minúsculas, números e o underscore. Exemplos: e, variável _essa_e_uma_variavel, tambem_variavel. Vale ressaltar que ela é "case sensitive", o que significa que diferencia maiúsculas e minúsculas.

Recomendações: evite o uso de variáveis com o nome iniciando com o underscore ("_"), pois elas são freqüentemente utilizadas por bibliotecas padrões do C (explicações sobre bibliotecas serão feitas mais adiante) e podem causar conflitos de nomes (quando um mesmo nome é utilizado em variáveis declaradas em mesmo nível de escopo - maiores detalhes sobre nível de escopo serão dados mais adiante). Crie também o hábito de nomear variáveis utilizando letras minúsculas, pois essa prática é comum no mundo da programação.

As variáveis possuem tipos. Os tipos dizem ao compilador que tipo de dado será armazenado. Isso é feito com o intuito do compilador obter as informações necessárias sobre quanto de memória ele terá que reservar para uma determinada variável. Também ajuda o compilador com uma lista de variáveis em um lugar reservado de memória, para que ele possa fazer referências, checar nomes e tipos e que possa determinar erros. Basicamente possibilita uma estrutura bem definida do que é usado e uma arrumação conveniente na memória. Podemos dizer que as variáveis em linguagem C são fortemente "tipadas", pois todas as variáveis possuem tipos associados e operações sobre as variáveis somente poderão ser feitas entre tipos compatíveis (mas não necessariamente iguais).

NOTA: o tipo mais comum nos programas C é o int (número inteiro). Maiores detalhes sobre os tipos serão dados adiante.

Declaração de uma variável

Page 8: Linguagem de Programação C

Antes de utilizarmos uma variável, precisamos declarar a variável. Veja abaixo a sintaxe da declaração de uma variável:

<tipo> <nome>;

ou

<tipo> <nome> = <valor_inicial>;

Exemplos:

int minha_variavel;int i = 10;int j = i;

Podemos também declarar várias variáveis de uma só vez, separando-as usando vírgulas:

int a , b = 0 , c , d = 11 , e , f;

No caso acima, as variáveis "a", "b", "c", "d", "e" e "f" serão do tipo "int"; cujo "b" terá valor inicializado em 0 e "d" terá valor inicial de 11.

Recomendação: para melhorar a legibilidade, é sempre uma boa prática de programação atribuir valores iniciais das variáveis no momento de sua declaração e evitar que a mesma variável seja "reutilizada" (que uma variável seja utilizada para um determinado propósito em um trecho do código e para um propósito completamente diferente em um outro trecho). Atualmente, um código manutenível (legível, fácil de alterar, reutilizável, portável e predizível) é muito mais apreciada do que um código simplesmente compacto.

Atribuição de valores

Utilizamos o operador "=" para atribuir valores novos às variáveis. O comando de atribuição de variáveis pode ser definido como abaixo:

<nome_da_variável> = <expressão>;

Onde <expressão> pode ser um número, uma outra variável, uma expressão matemática (ex. (a+b)-10, onde a e b são variáveis), uma função, etc.

Exemplo:

minha_variavel = 10 + 2 - outra_variavel;

O que vamos aprender agora é importante no sentido de que vamos usar isto praticamente em qualquer programa que implementemos. Vamos aprender a traduzir, para o nosso código, as fórmulas e expressões matemáticas que usamos no dia-a-dia ou que tenham alguma utilidade. Vamos falar um pouco sobre prioridade dos operadores e precedência, dois conceitos que juntos vão fazer com que todas as nossas fórmulas e expressões sejam corretamente analisadas pelo compilador.

Os operadores

Os seguintes operadores podem ser utilizados tanto com os inteiros quanto com os pontos flutuantes:

+ : Soma;- : Subtração;* : Multiplicação;/ : Divisão;++ : Incremento;-- : Decremento.

Já o seguinte operador só tem serventia no âmbito dos números inteiros:

% : Resto de divisão entre dois inteiros

Vamos falar um pouco sobre aridade. A aridade é um conceito que trata da quantidade de parâmetros que uma determinada operação envolve. Em alguns casos, precisamos de apenas um parâmetro para executar uma operação, como a seguinte: x = -y. Para poder saber corretamente o que está sendo atribuído, precisamos saber apenas o valor de y, e o operador '-' está sendo aplicado apenas ao y. Por esse motivo dizemos que ele é um operador unário. Já os outros operadores conhecidos são chamados de binários porque precisam de dois parâmetros para serem corretamente definidos. Exemplo:x = y + 2. Nesse caso precisamos saber o valor da variável y e o valor do número 2, paracorretamente definir o valor que vai ser atribuído. Esse mesmo conceito pode ser aplicado a funções, por isso é importante entendê-lo bem.

Sempre que fizermos uma atribuição em que a variável de destino é um inteiro, o número que sendo atribuído é convertido em um inteiro, mas não arredondado; a parte decimal é apenas desconsiderada.

Page 9: Linguagem de Programação C

Para exemplificar veja o seguinte exemplo:

#include <stdio.h>

int main() {

int intNumero1=0, intNumero2;

float floatNumero1=0.5, floatNumero2;

printf("intNumero1=%d e floatNumero1=%f\n",intNumero1, floatNumero1);

intNumero2 = intNumero1 + floatNumero1;

printf("inteiro = inteiro + flutuante: %d\n\n",intNumero2);

intNumero1 = 1;

floatNumero1 = 2.5;

printf("intNumero1=%d e floatNumero1=%f\n",intNumero1, floatNumero1);

intNumero2 = intNumero1 * floatNumero1;

printf("inteiro = inteiro * flutuante: %d\n\n",intNumero2);

intNumero1 = 10;

floatNumero1 = 3;

printf("intNumero1=%d e floatNumero1=%f\n",intNumero1, floatNumero1);

intNumero2 = intNumero1 / floatNumero1;

printf("inteiro = inteiro / futuante: %d\n\n",intNumero2);

return(0);

}

Os operadores de incremento e decremento são operadores unários, ou seja, precisam de apenas um parâmetro para serem corretamente utilizados. Eles não implementam uma funcionalidade nova na linguagem, mas facilitam uma operação que é extremamente comum, a de aumentar e diminuir uma variável em 1. Por exemplo:

intNumero1 = 1;intNumero1++;

O valor contido na variável x agora é 2. A seguinte operação tem o mesmo efeito:

intNumero1 = 1;++intNumero1;

Utilizados assim, os dois métodos fazem a mesma coisa, mas é bom evitar o uso desses métodos com o mesmo objetivo porque nos casos mais freqüentemente utilizados os dois métodos são essencialmente diferentes:

intNumero1 = 1;intNumero2 = 1;

intNumero1 = intNumero2++;

--> Nesse ponto temos que intNumero1=2 e intNumero2=1.

intNumero1 = 1;intNumero2 = 1;

intNumero1 = ++intNumero2;

--> Agora temos que intNumero1=2 e intNumero2=2. Isto acontece porque da primeira vez, apenas passamos o valor intNumero2+1 para a variável intNumero1. No segundo método, primeiro incrementamos a variável intNumero2, para depois passar o seu valor para a variável intNumero1.

Métodos interessantes de atribuição

Page 10: Linguagem de Programação C

As formas de atribuição que vamos ver agora são muito elegantes, e devem ser utilizadas quando possível. Com elas o nosso código fica mais enchuto, mais polido e mais legível. Vamos lá!

varNumero1 += varNumero2 equivale à varNumero1 = varNumero1 + varNumero2varNumero1 -= varNumero2 equivale à varNumero1 = varNumero1 - varNumero2varNumero1 *= varNumero2 equivale à varNumero1 = varNumero1 * varNumero2varNumero1 /= varNumero2 equivale à varNumero1 = varNumero1 / varNumero2

Essas atribuições funcionam como se fossem auto atribuições. As variáveis utilizam elas mesmo e mais uma variável para determinar o seu novo valor. Estas atribuição são especialmente úteis quando percebemos que o mesmo valor é utilizado de alguma forma, por exemplo:

float floatValorAplicado = 700;float floatJuros = 0.0079;int intNumMeses = 10;int intContador;

for (intContador=0 ; intContador < intNumMeses ; intContador+=1)floatValorAplicado += floatValorAplicado * floatJuros;

Como podemos ver, utilizamos duas vezes as atribuições elegantes. Na primeira vez, vimos que intContador++ equivale à intContador+=1 que equivale à intContador = intContador + 1. Na segunda, podemos ver que floatValorAplicado += floatValorAplicado * floatJuros equivale à floatValorAplicado = floatValorAplicado + floatValorAplicado * floatJuros. É uma boa economia de código, certo?

Vamos ver agora o último método interessante de atribuição, que também torna o código mais elegante, legível e enxuto:

intNumero1 = intNumero2 = intNumero3 = 4;

O que acontece aqui é que todas as três variáveis vão receber o valor 4. É como se a atribuição fosse viajando pela instrução da direita para a esquerda. Pense nas atribuições de incremento e decremento (x++) desta forma, como uma viajem da direita para a esquerda. Facilita vermos como essas execuções funcionam

Tipos de dados

Para utilizar uma variável, precisamos levar em consideração os seguintes aspectos:

Escolher um nome claro e conciso para a variável; Escolher a área de atuação da variável (veja "regras de escopo" nas próximas páginas); Escolher qual o TIPO da variável será necessária para um dado.

Existem vários tipos de dados em C. Todos eles são palavras reservadas. O C é bem flexível e possibilita a criação de novos tipos baseando nos tipos elementares (iremos ver isso nas lições seguintes).

Os tipos elementares da linguagem C são:

char: tipo caractere. Exemplos são 'a', 'b', 'x'... São os símbolos do nosso alfabeto mais os outros representáveis com 1 Byte (256 elementos ao todo, incluindo os "dígitos numéricos", '@', '#', etc). No C, usamos o apóstrofe (') para indicar que um símbolo deve ser tratado como um conteúdo de uma varíável do tipo "char". Este tipo pode ser usado também como um subconjunto do tipo "int" (afinal, em baixo nível os caracteres são números - seqüência de bits);

int: número inteiro; float: um número real. Significa "ponto flutuante", indicando que o ponto decimal (ou seja, a precisão) é variável de

acordo com a grandeza do número (se um número "float" é grande, sua precisão precisa ser menor; se pequeno, sua precisão pode ser maior);

double: número real extendido, ou "float" com dupla precisão. Pode representar um conjunto maior do que o float; void: tipo especial que indica "nenhum tipo". Pode ser utilizado para simular um tipo universal.

Podemos ter também um conjunto de modificadores de tipos. Eles são declarados antes do nome do tipo (ex. unsigned short int - nesse caso, o "unsigned" e o "short" são os modificadores). Veja abaixo os modificadores elementares:

signed: usado para int e char. Indica que os números negativos devem ser considerados. É o default, podendo ser omitido;

unsigned: usado para int e char. Indica que números negativos devem ser desconsiderados. Permite que números positivos maiores possam ser armazenados nas variáveis (em contrapartida, números negativos não poderão ser armazenados);

short: usado para int. Indica que a variável deve usar menor ou igual quantidade de bits de armazenamento do que o convencional;

Page 11: Linguagem de Programação C

long: usado para o int. Indica que a variável deve usar menor ou igual quantidade de bits de armazenamento do que o convencional.

Existem também os qualificadores de tipos. Eles são freqüentemente utilizados para otimização do programa. Seu uso básico é:

<qualificador> <declaração>

Onde <declaração> pode ser uma declaração de variável (como unsigned short int i = 0;) ou de função.

Os principais qualificadores são: auto: indica ao compilador que o objeto (variável ou função) declarada a seguir deve ter seu escopo restrito ao bloco

que ela foi declarada (no caso de uma variável, indica que não deve ser visível fora do "{" e "}" em que ela foi declarada). Esse qualificador é raramente utilizado, pois é o padrão na linguagem C;

extern: indica que o objeto declarado a seguir (variável ou função) já foi declarado fora do bloco (seja, fora do "{" e "}" ou até em um arquivo diferente) e que o objeto previamente declarado deve ser usado no seu lugar. É útil quando diversos arquivos fontes são usados para um mesmo programa;

register: indica que a variável declarada a seguir deve estar presente em um armazenamento temporário mais veloz o possível. Antigamente, esse qualificador indicava que a variável deve estar presente no registrador da CPU, mas atualmente ele só indica que a varíavel deve estar no dispositivo mais veloz e utilizável no momento;

volatile: indica que a variável a seguir pode ter seu conteúdo alterado por um fator externo ao programa (ex. Sistema Operacional, processos concorrentes, threads paralelos, interrupções do programa, etc). São úteis nos seguintes casos:

o utilizar como um objeto que é uma porta de entrada/saída mapeada em memória; o utilizar o objeto entre diversos processos concorrentes (quando diversos programas em execução devem utilizar

uma mesma variável ou porção da memória); o quando um objeto terá seu conteúdo alterado com um serviço de interrupção (ex. o famoso comando "kill" envia

um sinal de interrupção para terminar um processo em execução).

Como podem ver nas explicações acima, esses qualificadores de tipos/variáveis são usados para gerar programas avançados e seu uso será explicado mais adiante.

Constantes

Vamos tratar rapidamente da utilização de constantes em programas feitos em C. A utilização de constantes é uma técnica muito importante na linguagem C, que torna os programa bem mais legíveis e mais polidos fazendo com que qualquer alteração seja bem mais simples de fazer sem a necessidade de procurar, às vezes por mais de uma hora, o bloco de código em que a variável está para que ela seja alterada.

Para exemplificar, imagine que você fez um programa para uma empresa que possua três departamentos. Você precisa manter o cadastro de todos os funcionários desses departamentos e mais algumas tarefas relacionadas. Imagine agora que você vai vender esse software para uma segunda empresa, sendo que esta precisa organizar a ficha de todos os funcionários dos cinco departamentos da empresa. Para preparar o programa para essa segunda empresa, você vai ter que procurar todas as ocorrências dasvariáveis que controlam o número de departamentos e alterar todas. A não alteração de uma ou mais aparições dessas variáveis fazerá com que o programa funcione de forma não planejada, gerando reclamações por parte dos clientes. Se ao invés de utilizar uma ou mais variáveis, você utilizar apenas uma constante que é utilizada em todo o programa e se a especificação da constante estiver no começo do programa, tudo que você precisa fazer para vender o software para a segunda empresa é trocar o valor da constante. Simples, não?

#include <stdio.h>

#define NUM_DEPT 4

int main() {

{codigo}

for (intDept = 0 ; intDept < NUM_DEPT ; intDept++) {codigo}

}

E assim constantes podem ser utilizadas para praticamente qualquer variável, inclusive stings. Por exemplo:

#define STR_ERRO404 "A página não pode ser encontrada. Verifique o endereço e tente novamente"

#define ERRO_PAGINA_NAO_ENCONTRADA 404

#define PI 3.1415

E assim as constantes se tornam estruturas imprescindíveis no cotidiano de um programador. Utilize os conhecimentos que você já agregou e escreva alguns programas utilizando constantes. Tomando o cuidado de não exagerar, utilize constantes sempre

Page 12: Linguagem de Programação C

que possível, isto é, sempre que você perceber que está utilizando o mesmo valor várias vezes. Nesses casos, o uso de uma constante é recomendável.

Lição 3 - Elementos léxicos e sintáticos da linguagem C, parte 2

Introdução às funções

Já iremos apresentar aqui as "funções" para vocês. Na verdade, as funções são um conceito relativamente avançado, mas decidimos mostrar já neste ponto, pois são o núcleo dos programas escritos em C (podemos dizer que um programa em C é um conjunto de funções, pois não há como existir um programa em C sem uma função).

Como o nome diz, funções são "coisas" que desenvolvem tarefas. Brilhante...

Funções são caixas pretas, onde você passa algum tipo de dado e espera receber algum tipo de saída. Explicando técnicamente, são módulos ou blocos de código que executam uma determinada tarefa. Essa é a melhor definição. Ele é carregado somente uma vez e é usado diversas vezes durante a execução do programa. Elas são o núcleo da sintaxe da linguagem C.

Os exemplos abordados nesta página utilizam conceitos que ainda não foram apresentados neste curso. Ainda não se preocupe com os exemplos, pois serão mais interpretáveis futuramente (quando todos os elementos léxicos/sintáticos da linguagem forem apresentados).

Para que servem elas?

As tais funções existem por dois motivos básicos:

depuração de erros - quando se quebra um problema em pedaços menores, fica mais fácil detectar onde pode existir um problema;

reutilização - é visível que grande parte dos códigos que existem em um programa são repetidos, só se diferenciando as variáveis que são passadas a eles.

Expliquemos então usando duas situações hipotéticas. A primeira, eu tenho que montar um carro. Posso fazer uma máquina que eu passe todas as peças e ela me retorne o resultado. Ou posso criar uma máquina que gerencie várias outras pequenas que desempenham tarefas diferentes que, juntando-as, eu obtenho o carro pronto. Intuitivamente, gostaríamos da primeira, mas devemos pensar que uma grande caixa preta é pior de se depurar do que várias outras pequenas e de tarefas específicas. Imagine se acontece algum problema na caixa preta grande, teríamos que abri-la toda, mexer em várias coisas e tentar chegar a uma conclusão em relação ao problema. Já em um monte de caixas pequenas especializadas, detectaríamos o problema muito mais facilmente, só pelas tarefas que elas realizam. Podemos citar não só questão de problemas, como performance, entendimento e confiabilidade.

Outra situação. Imagine que eu precise fazer uma calculadora. Eu poderia fazer um conjunto de operações (função), que em um bolo de código calculasse todos os tipos de operações matemáticas desejáveis em minha calculadora no momento. Agora pense, depois de 1 ano, eu preciso de 2 operações matemáticas das 15 que minha calculadora antiga fazia, o que fazer ? Agregar o bolo de código com 15 funções, 13 delas desnecessárias? A modularização serve para o reaproveitamento de código, devemos chegar a pedaços razoáveis e especializados de código que nos resolvam problemas e que possamos utilizá-los depois.

Lembre-se, isso é uma prática não muito fácil, depende da experiência do profissional e como ele faz a análise inicial do problema, quebrando-os em menores pedaços e chegando a módulos pequenos e ao mesmo tempo usuais.

Resumindo, o uso de funções:

economiza memória e aumenta a legibilidade do programa; melhora a estruturação, facilitando a depuração e a reutilização.

Nomes

Bem, podemos dar nomes às funções assim como em variáveis. Letras de A até Z, sem preocupação de maiúscula/minúscula, de 0 a 9 e com underscore (aquele menos achatado, "_"). Precisa começar por caracteres ou underscore.

É "case sensitive", ou seja, funções com o mesmo nome, mas letras diferentes (em case) não são consideradas iguais. Podemos exemplificar: esta_e_uma_funcao e Esta_e_uma_funcao, o "E" ("e") é diferente!

A estrutura de uma função

Page 13: Linguagem de Programação C

A estrutura básica de uma função é:

tiponomeDaFuncao ( tipo1parametro1 , tipo2parametro2 , ... ) { código1 ; . . . códigoN ; }

OBS: Elementos sublinhados podem ser substituídos por algum elemento sintático da linguagem (exemplo: tipo pode ser substituído por int, que é um elemento sintático de tipo de dado no C).

A cara de uma função é basicamente essa, veja abaixo para um exemplo:

void imprimeSoma ( int fator1 , int fator2 ) { int total; total = fator1 + fator2; printf ("A soma vale: %d",total); }

Ignore a palavra void por enquanto. Ela somente indica que a função não tem tipo (isso indica que a função não tem valor de retorno - veja "tipo de funções" adiante). Quando chamo a função usando o comando imprimeSoma(5,3); , eu recebo a mensagem da adição de 5 por 3, e retorno ao meu programa. Conseguiu materializar?

Note que as chaves (o "{" e o "}") delimitam o que faz parte da função (bloco) e o que não o é.

A função main()

A função main() é a função principal de um programa. Ou seja, todo programa tem que ter a função main(), caso contrário o compilador reclama e não gera o executável.

Um programa começa executando a função main() e termina quando a função main() termina. Porém, dentro da função main() você pode chamar (executar) outras funções. Falaremos mais sobre o main() adiante.

O ponto inicial de execução do programa é chamado de "entry point", logo, a função main() é o entry point de qualquer programa escrito na linguagem C.

Ela pode retornar um valor de tipo int. Ou seja, retorna um número, em geral para o sistema operacional, com o código de sucesso ou indicando qual o erro (número do erro) ocorreu durante a execução do programa. O número de erro retornado pelo main() é conhecido pelos programadores como o "condition code".

A função main() pode ter as seguintes estruturas:

int main() int main (int argc , char *argv[])

As estruturas acima são as mais aceitas como padrão. Adicionalmente, muitos compiladores aceitam o tipo de retorno do main() omitido (ou seja, o "int" seria desnecessário) ou como "void" (sem tipo), mas as construções da lista acima são mais recomendadas para maior portabilidade (capacidade de rodar/compilar seu programa em diversas plataformas).

A função main() aceita dois argumentos (parâmetros entre parênteses). Eles são parâmetros passados pelo sistema operacional quando os programas são ativados. Por exemplo, no terminal de comando do Linux você pode digitar o comando ls-l. Nesse caso, o ls seria o nome do programa e o -l seria o parâmetro que o sistema operacional repassará para o programa fazer o devido tratamento. Os parâmetros do main() representam esses argumentos.

Veja abaixo para uma breve descrição desses parâmetros:

argc: é do tipo inteiro (numeral). Indica a quantidade de argumentos que foram repassados pelo sistema operacional, ou seja, indica a quantidade de elementos contidos no vetor argv. Seu valor é sempre maior ou igual à 1 (um), pois o próprio nome do programa compilado é considerado como um argumento.

argv: é um vetor de strings (string é uma palavra ou um conjunto de letras/caracteres). Eles contêm todos os argumentos repassados pelo sistema operacional. O primeiro elemento (o elemento 0 - zero) é sempre o nome do próprio programa executável.

Esses parâmetros são úteis para fazer um programa que opere de forma distinta dependendo do que o usuário tem passado no terminal de comando. Ainda não se preocupe muito com o uso correto desses parâmetros. Como eles usam vetores (conceito ainda não explicado detalhadamente), você não tem a obrigação de saber utilizá-los neste momento. Basta saber que um mecanismo tão útil já existe na linguagem C.

Chamando funções

Page 14: Linguagem de Programação C

Bem, podemos chamar (executar) as funções do ponto que desejamos, desde que ela já tenha sido declarada. Ela desvia o fluxo do programa, por exemplo:

int main() { int a=10,b=3; ação1; ação2; imprimirSoma(a,b); ação3; }

Nota: neste exemplo ação 1, 2 e 3, podem ser quaisquer comandos (Até mesmo outra função).

O programa desviará o fluxo na chamada da função "imprimirSoma", logo após a "ação2". Isto suspenderá temporariamente a execução do programa para poder executar a função diminuir, até que a mesma termine (retorne).

Tipos de funções

Existem basicamente dois tipos de funções. Aquelas que retornam alguma coisa a quem a chamou e aquelas que não retornam nada.

Começando pelas que não retornam, elas simplesmente realizam tarefas, como o exemplo anterior. Ela faz uma série de passos e retorna o fluxo ao programa principal, sem interferir em nada em sua execução, a não ser pelo tempo de execução, saída na tela e mudanças em alguns dados compartilhados.

Outra opção são funções que retornam um valor de um tipo. Lembre-se, como declaramos uma função? tipoX nome(tipo1 var1,tipo2 var2); e assim por diante. Ou seja, o tipoX equivale ao tipo de dado que a função vai retornar. Vamos entender:

int diminuir(int parcela1, int parcela2) { int total; total = parcela1 - parcela2; return total; }

int main() { int a=10,b=3,total; ação1;ação2; total = diminuir(a,b); printf ("A subtracao vale: %d",total); ação3; }

O efeito é exatamente o mesmo, só que agora o programa principal é que estará jogando a mensagem na tela e a variável do programa, chamada total, que terá o valor da subtração (resultado, tipo int, retornado de diminuir(a,b)). Aos poucos vamos juntando as peças.

Vale ressaltar, o que determinou a saída da função, no caso, foi a chamada ao comando return (que é um comando de desvio), que interrompe o fluxo do bloco que está sendo executado (saindo deste bloco) e volta aquele imediatamente anterior. Não é necessário chegar até a última linha da função, pois o return pode estar na 1a, 2a, onde quer que seja.

Bibliotecas

Já que mostramos o que é uma função, aproveitamos para apresentarmos o que é uma biblioteca.

Você pode entender as bibliotecas como um conjunto de declarações (seja de funções, tipos, variáveis, etc) que foram criadas de forma estratégica para possibilitar sua utilização em diversos programas.

Como dito anteriormente, funções são uma forma genérica de resolvermos problemas. É como uma caixa preta. Você passa os dados para ela e recebe o resultado.

Supondo que tenho uma função de realizar soma, eu só me preocupo em passar para ela os números que desejo ver somado e a função se preocupa em me entregar o resultado, o que acontece lá dentro é problema dela.

Através deste método, dividimos os programas em pedaços de funcionalidades, genéricos e pequenos de preferência, com intuito de utiliza-lo futuramente em situações que sejam convenientes.

Assim como soma, pode-se fazer uma função de subtração, multiplicação, divisão e várias outras e juntando-as se cria a tal famosa biblioteca. As bibliotecas em si podem ser utilizadas por vários programas.

Só para esclarecer, tenho uma biblioteca que desenha botões em janelas(GTK faz isso). Na hora que se for criar uma agenda, por exemplo, utilizo as funções desta biblioteca sem precisar rescrever estas mesmas funções neste programa. Isso pouparia meu tempo e espaço de HD (apesar de um código fonte não ser algo que ocupe TANTO espaço).

Veja abaixo alguns exemplos de bibliotecas que podem ser encontradas sem muito esforço em distribuições Debian. Eles se encontram na pasta /usr/include.

aio.h expat_config.h jerror.h printf.h termio.haliases.h expat_external.h jmorecfg.h pthread.h termios.halloca.h expat.h jpegint.h pty.h tgmath.ha.out.h fcntl.h jpeglib.h pwd.h thread_db.h

Page 15: Linguagem de Programação C

argp.h features.h langinfo.h re_comp.h tiffconf.hargz.h fenv.h lastlog.h regex.h tiff.har.h FlexLexer.h libgen.h regexp.h tiffio.hassert.h fmtmsg.h libintl.h resolv.h tiffvers.hautosprintf.h fnmatch.h libio.h sched.h time.hbyteswap.h fpu_control.h limits.h search.h tls.hcomplex.h fstab.h link.h semaphore.h ttyent.hcpio.h ft2build.h locale.h setjmp.h ucontext.hcrypt.h fts.h malloc.h sgtty.h ulimit.hctype.h ftw.h math.h shadow.h unistd.hdialog.h _G_config.h mcheck.h signal.h ustat.hdirent.h gconv.h memory.h spawn.h utime.hdlfcn.h getopt.h mntent.h stab.h utmp.hdlg_colors.h gettext-po.h monetary.h stdint.h utmpx.hdlg_config.h glob.h mqueue.h stdio_ext.h values.hdlg_keys.h gnu-versions.h netdb.h stdio.h wait.hdts.h grp.h nl_types.h stdlib.h wchar.helf.h gsm.h nss.h string.h wctype.hendian.h iconv.h obstack.h strings.h wordexp.henvz.h ieee754.h oss-redir.h stropts.h xlocale.herr.h ifaddrs.h paths.h syscall.h zconf.herrno.h initreq.h pngconf.h sysexits.h zlib.herror.h inttypes.h png.h syslog.hexecinfo.h jconfig.h poll.h tar.h

Claro, você não precisa saber de todas elas. O .h é a extensão do arquivo cabeçalho que contém as definições da biblioteca - header em inglês. Os arquivos cabeçalho são arquivos texto (você pode abri-lo em qualquer editor de texto para lê-lo), mas conterão somente declarações/protótipos das funções (são somente "assinaturas" das funções, ou seja, funções sem corpo) e a implementação dessas funções (os códigos em C) geralmente estarão em outros arquivos (que raramente são textos). Técnicas de como se fazer isso (criar bibliotecas que contenham o código em outros arquivos) serão tratadas mais adiante.

Em geral, utilizamos algumas funções já prontas para fazer determinadas tarefas que são consideradas básicas. O programador não costuma fazer uma rotina que leia diretamente do teclado ou imprima na tela um caractere.

Isso já existe e é bem implementado (uma coisa interessante de se entender em programação é: o que já existe de bem feito e pode ser utilizado deve ser utilizado). Seu sistema não será menos digno ou pior se você utilizar uma rotina que todo mundo utiliza em vez de ter a sua própria. O que importa é a finalidade do programa e o quão bem implementado ele esteja.

Tais funções, que falamos básicas, fazem parte da biblioteca C padrão (as que geralmente começam com a seqüência "std", que significa standard ou "padrão"). Todo compilador C a possui e ele faz parte da padronização ANSI C. Seu compilador, independente do sistema que você utiliza, deve possuir essas bibliotecas (ou seria um furo inquestionável). Outras bibliotecas a mais, além das padronizadas pelo ANSI, também vem junto com seu compilador, porém não é recomendado para a utilização caso você queira escrever programas portáveis (que rode em todas as plataformas). Podemos aqui citar a programação gráfica de rede e etc como casos que são "perigosos" para programação portável. Não estou dizendo que você não deve programar para estas áreas, futuramente poderão ter cursos para essas áreas por aqui, porém deve atentar-se que tal programação é peculiar à plataforma que você está utilizando e não reclame se ele só funciona no Linux ou no BSD ou no Solaris ou no Windows Vista ou... ETC.

As bibliotecas são incorporadas ao seu programa utilizando uma diretiva de compilação (explicações sobre as diretivas de compilação serão feitas mais adiante) chamada "include". Para utilizar uma biblioteca, um "include" deve ser feito antes de qualquer declaração.

Para incluir uma biblioteca padrão do C (os contidos na pasta /usr/include/): o #include <NOME_DA_BIBLIOTECA.h>

Para incluir uma biblioteca pessoal: o #include "NOME_DA_BIBLIOTECA.h"

Cujo NOME_DA_BIBLIOTECA pode conter o caminho para o arquivo (ex. ../bibliotecas/meubib.h). Recomendo que, ao definir o caminho, esse caminho seja relativo (não utilize a organização absoluta das pastas).

O famoso printf()

Se desejamos citar uma função invariável e já consagrada, mas que não propriamente é da linguagem C, porém já pode até ser considerada como se fosse própria da linguagem, é a função printf(). Ela está contida na biblioteca padrão de entrada/saída (tal biblioteca se chama stdio.h. O stdio significa STanDard Input/Output).

A função printf quer dizer print-formated, ou imprimir formatado. A maneira mais simples de imprimir algo é:

printf("algum texto aqui!");

Bem, comecemos então. Caso você não queira imprimir um texto fixo, mas sim algo que varie durante a execução de um programa (digamos uma variável - veja maiores detalhes sobre variáveis adiante), usaremos as controladoras de seqüência. Chamemos de controladores de seqüência os caracteres especiais que significarão as variáveis que serão impressas pela função.

Page 16: Linguagem de Programação C

O lugar onde o controlador for colocado é o lugar onde a variável será impressa. Por exemplo, caso queiramos imprimir um inteiro que esteja armazenada em uma variável com o nome algum_inteiro:

printf ("Nossa! O inteiro vale %d!!! Oh! Credo!", algum_inteiro);

A saída será:

Nossa! O inteiro vale 24!!! Oh! Credo!

NOTA: O "24" é o valor dado a variável chamada "algum_inteiro" (sem aspas).

Maiores detalhes de como usar o "printf" (e o "scanf", seu par íntimo) serão esclarecidos mais adiante (depois de explicarmos o que é um "tipo" de dado).

Veja abaixo um exemplo super-simplificado de um programa C

/*************************************************************//* Primeiro exemplo de um programa *//************************************************************/ #include <stdio.h> /* Aqui incluímos a biblioteca de */

/* C padrão de Entrada/Saída */ /***********************************************************/int main () {/* Comentários em C ficam entre /* e */

printf ("OH! Meu Deus! Este eh o exemplo numero %d em C! \n", 1); printf ("Huahuahua!! Que desse exemplo %d surja o %d... \n",

1, 1 + 1); printf ("E depois o %d! \n", 3); printf ("...Desculpe... Estou sem criatividade ");

printf ("hoje dia %d de janeiro de %d para criar exemplos decentes...\n", 31, 2007);

printf("... Foi o sono..."); }

Saída (normalmente no terminal de comandos) :

OH! Meu Deus! Este é o exemplo número 1 em C!

Huahuahua!! Que desse exemplo 1 surja o 2...

E depois o 3!

...Desculpe... Estou sem criatividade hoje dia 31 de janeiro de 2007 para criar exemplos decentes...

... Foi o sono...

Algumas coisas que vamos ver serão mencionadas então essa é uma parte de introdução e de aprender mais algumas coisas. Vamos falar um pouco sobre comparações, operadores lógicos e a parte que parece menos útil, mas na verdade é bastante útil, os operadores lógicos bit a bit. Vamos ao trabalho, então:

Operadores de comparação

Estes operadores introduzem ao C conceitos que fazem parte do nosso dia-a-dia. Não precisamos pensar muito para saber que uma pessoa de 1,80m de altura é mais alta que uma pessoa de 1,50m. Não precisamos pensar muito para ficar na porta de entrada de uma boate, permitindo que apenas pessoas com 18 anos ou mais entre. São comparações que fazemos diariamente e por isso têm um papel tão importante em qualquer linguagem de programação. Vamos ver os operadores e alguns exemplos:

> : Maior que>= : Maior ou igual que< : Menor que<= : Menor ou igual que== : Igual!= : Diferente

Todos esses operadores são utilizados dentro de funções que podem ser verdadeiras. Intuitivamente, podemos ver que:

Page 17: Linguagem de Programação C

intNumero1 == 10;

não faz muito sentido, já que nós vimos que o operador '==' é um operador lógico e não um operador de atribuição. O compilador C não vai pegar no seu pé por isso, mas essa instrução não vai ter efeito algum, vai retornar um verdadeiro ou um falso para o compilador, mas ele não vai fazer nada com isto.

Operadores lógicos

Esses operadores servem para "conectar" duas expressões lógicas. Estas também fazer parte do nosso cotidiano, quer ver? Imagine que você vai comprar os materiais para fazer uma reforma na sua residência. Quem te atende na empresa te diz que você pode pagar com entrada daqui a trinta dias e dividir o restante em três vezes ou então dividir tudo em doze vezes no cartão. Essa é uma escolha que você tem que fazer. Vamos ver isto de outra forma:

<entrada em 30 dias> E <dividir em 3x> OU <dividir em 12x no cartão>

Aparentemente a expressão completa seria esta, mas ela está um tanto confusa. Não sabemos quais expressões estão mais "fortemente" ligadas, o que pode nos levar a sérios erros de lógica. Por isto, sempre que quisermos utilizar expressões compostas, como esta, deixamos bem claro, com o uso de parênteses, do que se trata. Veja como ela fica mais legível:

( <entrada em 30 dias> E <dividir em 3x> ) OU <dividir em 12x no cartão>

Melhor, né? Agora sabemos que temos duas opções: uma ou a outra. E sabemos também que a primeira é formada por um conjunto de duas premissas, que juntas formam a primeira opção. É altamente recomendável utilizar os parênteses para deixar bem claro a finalidade da expressão lógica; isso faz com que o código tenha uma menor probabilidade de conter erros e fique mais legíveis.

&& : Conjunção lógica: uma coisa E outra|| : Disjuntção lógica: uma coisa OU outra! : Negação lógica: NÃO uma coisa

Estes são os operadores. Intuitivamente, percebemos que os dois primários são binários, ou seja, precisam estar entre duas expressões para serem corretamente empregados enquanto que o segundo é unário, ou seja, deve ser aplicado a apenas uma expressão. Vamos ver alguns exemplos:

intNumero1 = 10;intNumero2 = 20;

(intNumero1 < intNumero2)

Fácil, né? Vamos complicar um pouco.

(!(intNumero1 < intNumero2> && (intNumero1 > 10)))

E aí, verdadeiro ou falso? Percebeu que os operadores são bem flexíveis e podem ser aplicados a expressões moleculares, ou seja, formadas por uma variável, ou expressões longas como uma grande expressão lógica?

Operadores bit a bit

Essa é uma parte que normalmente gera dúvidas. Não é muito complicado e com um pouco de cuidado todos podem entender como as operações bit a bit funcionam. Normalmente, trabalhamos com variáveis com números inteiros ou ponto flutuante. Somamos, subtraimos e etc. Além disso, comparamos se um número inteiro é maior que outro ou não. Podemos fazer operações análogas com os bits e isto é essencialmente útil por ser barato do ponto de vista computacional. Uma variável do tipo inteiro ocupa 16 bits, ou 2 bytes. Se quisermos usar variáveis do tipo inteiro para fazer contagens, precisamos de 2 bytes para cada variável, enquanto que com um byte, podemos fazer uma contagem da mesma forma. Vamos ver alguns exemplos:

Imagine que temos em casa uma cadela e ela teve oito filhotes. Esses filhotes nasceram prematuros e precisam ficar em locais especiais com aquecimento e cuidado constante. Como agora sabemos programar, fizemos um programa que controla essas encubadoras, mantendo o sistema de aquecimento e alimentação de cada encubadora ligado ou desligado, dependendo se o cãozinho está ou não lá. Para controlar se há ou não um cãozinho em cada encubadora, precisaríamos de oito variáveis do tipo inteiro, e poderíamos dizer que "1" quer dizer ocupado e "0" quer dizer desocupado. 8 * 16 = 128 bits. Ao invés disso, podemos utilizar apenas uma variável do tipo char, que ocupa 8 bits, uma eocnomia de 93,75%!!

Inicialmente, temos a variável igual a 00000000, ou seja, os cãezinhos não estão lá, estão sendo preparados. Então, resolvemos colocar o primeiro cãozinho na primeira posição da esquerda. Como sabemos que as variáveis do tipo char em C são tratadas como inteiro poderíamos fazer binOcupacao = 128 que teríamos 10000000. Mas então, quando formos colocando os outros essa operação ficaria muito complicada. Para facilitar a nossa vida, existem as operações bit-a-bit. Podemos utilizar os seguintes operadores:

& : Conjunção - uma coisa E outra| : Disjunção - uma coisa OU outra^ : Disjunção exclusiva - uma coisa OU e explicitamente OU outra~ : Negação - inversão do número binário<<: Deslocamento de bits à esquerda>>: Deslocamento de bits à direita

Então vamos lá: para conseguir o número 10000000, basta pegarmos o número básico 00000001 e "deslocarmos" o

Page 18: Linguagem de Programação C

"1" sete casas para a esquerda, assim:

binBase = 1;binBase << 7;

Pronto, temos o número 10000000 em mãos. Agora, o que queremos é setar o primeiro bit da esquerda da variável binOcupacao em "1", denotando que agora o cãozinho está na encubadora. Para isto, basta somar os dois números, da seguinte forma:

binOcupacao 00000000+ binBase 10000000---------------Resultado 10000000

Pronto. Agora o cãozinho número três chegou, e queremos setar o terceiro bit da esquerda para a direita em "1".

binBase = 1;binBase << 7-3+1;

Agora vamos somar denovo

binOcupacao 10000000+ binBase 00100000---------------Resultado 10100000

Essa "soma" é feita com o operador "ou". Com ele, setamos o bit em "1" se o primeiro ou o segundo (na mesma posição) forem iguais a "1". Se tivéssemos utilizado o "ou exclusivo", setaríamos o bit em "1" se apenas um dos dois bits naquela posição fossem iguais a "1". Vamos exemplificar:

(11111111 & 00001100) = 00001100(10000001 | 10001101) = 11111111(10101010 ^ 11001100) = 01100110

Um detalhe que deve ser lembrado com relação aos operadores de deslocamento, é que quando se desloca bits para a esquerda por exemplo, se estamos deslocando todos os bits quatro casas para a esquerda, as quatro primeiras casas serão perdidas e as quatro casas da direita serão "geradas" com zero, e a operação inversa não vai fazer com que os bits perdidos sejam recuperados.

Estruturas de Controle de Fluxo

Vamos agora introduzir à nossa lista de ferramentas do C algumas das que são mais utilizadas nos programas codificados em C. Vamos aprender a implementar as estruturas condicionais que, utilizando comparações lógicas puras ou aritméticas, determinam por que caminho nossos programas serão executados. Vamos aprender também a implementar as estruturas iterativas que, também utilizando proposições (afirmações), determinam até quando os nossos programas devem executar determinada rotina.

Verdadeiro e falso

É importante, antes de iniciarmos o desenvolvimento de programas utilizando as estruturas de controle, que o conceito de verdadeiro e falso esteja bem claro do ponto de vista da programação, mais específicamente das proposições.

Para a linguagem C, quando uma variável contém o valor 0 (zero), ela está com o valor falso. Qualquer outro valor diferente de zero (positivo ou negativo) significa verdadeiro. Generalizando, qualquer coisa diferente de zero é verdadeiro e qualquer coisa igual a zero é falso. Você deve estar se perguntando agora qual a utilidade disto. Veremos a seguir.

O Comando de controle IF

O comando if (a palavra em inglês para se) é sem dúvida o mais conhecido e um dos mais usados em todas as linguagens de programação. Sua finalidade é, como já foi dito anteriormente, direcionar a execução do programa baseando-se em uma afirmação, que é valorada pelo próprio programa como sendo verdadeira ou falsa. De uma forma intuitiva, podemos perceber que o comando if não faz nada se perceber que a afirmação que ele contém é valorada logicamente como falsa, e por outro lado, inicia uma rotina (um bloco de código) se perceber que a afirmação é verdadeira. Vale lembrar que essa rotina pode ser qualquer coisa, desde um comando simples de atribuição até um programa inteiro; fica à escolha do programador.

A utilização do comando if segue o seguinde padrão:

if (expressão lógica) comando;

Se desejarmos que o comando if inicie uma rotina de mais de um comando, o fazemos assim:

if (expressão lógica) {bloco de codigo}

Observação: O comando if, assim como todas as outras expressões reservadas da linguagem C, deve ser escrito em letras minúsculas. Vale lembrar também que a linguagem C sempre diferencia letras minúsculas de letras maiúsculas, ou seja, a variável nota1 e a variável Nota1 não podem ser utilizadas como sendo a mesma variável.

Tipos de comparações e seus usos

Page 19: Linguagem de Programação C

Vamos ver agora algumas formas de utilizar as comparações que podem ser inseridas no comando if, ou em qualquer outro comando de controle do C.

1. Inserções lógicas puras: Como foi dito anteriormente, sempre que alguma expressão tiver como resultado ou valor o zero, ela é equivalente a um falso lógico. Por outro lado, sempre que uma expressão tiver como resultado qualquer outro valor, ela é equivalente a um verdadeiro lógico. Vamos ver um exemplo:

Se dissermos que a variável okCadastro, que determina se um cadastro qualquer está de acordo ou não com as normas, vale 1, ou seja, está de acordo, e fizermos:

if (okCadastro) printf(“O cadastro está de acordo com as normas”);

O comando if vai perceber que a expressão nele contida tem como resultado o número 1 e vai valorar esse resultado como verdadeiro. Sendo assim, ele vai imprimir na tela a mensagem: “O cadastro está de acordo com as normas”.

É importante introduzir aqui um operador lógico bastante utilizado na linguagem C. Para que possamos criar expressões e manter nelas um significado lógico que nos permite entender facilmente a sua valoração, muitas vezes utilizamos a negação ! que, sendo um operador unário, é sempre aplicado à uma única expressão. Vamos ver um exemplo:

if (!isCadastro) printf(“O cadastro não está de acordo com as normas”);

Vimos que não precisamos criar uma variável chamada isnotCadastro. Com esse operador, conseguimos criar uma expressão lógica que expressa de uma forma simples a possibilidade do cadastro ter problemas. Percebemos também que esse operador sempre inverte a valoração da expressão à ele aplicada.

2. Comparações binárias quantitativas e qualitativas

Como o nome já diz, trataremos agora das comparações dois a dois, ou seja, que utilizam operadores que devem sempre estar entre duas expressões. Todos eles podem ser utilizado em qualquer expressão lógica dentro da linguagem C. Vamos lá:

== : Igual!= : Diferente> : Maior>= : Maior ou igual< : Menor<= : Menor ou igual

Vamos ver alguns exemplos:

/* Realiza comando apenas se intVariavel1 e intVariavel2 tiverem o mesmo valor numérico */if (intVariavel1 == intVariavel2) comando;

/* Paga o salário do funcionário se ele já tiver trabalhado 27 dias ou mais */if (intDiasTrabalhados >= 27) pagaSalario(funcionario);

E assim todos os operadores podem ser utilizados, cada um para a sua finalidade lógica. Vamos ver um exemplo de programa utilizando alguns deles:

int main() {

/* Atribui os valores às variáveis */

int numero_a = 5;

int numero_b = 10;

/* Analisa os valroes */

if (numero_a >= 0) printf(“O primeiro número é não negativo”);

if (numero_b > numero_a) printf(“O segundo número é maior que o primeiro”);

/* Diz ao sistema operacional que o programa rodou com sucesso */

return(0);

}

Correndo o risco de tornar essa lição levemente repetitiva, lembramos que na linguagem C, assim como em outras linguagens de computação, qualquer expressão que tiver seu valor igual a zero é valorada como falsa, e qualquer expressão que tiver um valor igual a qualquer coisa diferente de zero é valorada como verdadeira.

Vamos ver agora uma forma de ensinar o programa como proceder caso a expressão condicional seja falsa. Podemos dizer ao programa que se a expressão for valorada como falsa, ele deve iniciar uma outra rotina alternativa. Vamos ver um exemplo:

Page 20: Linguagem de Programação C

if (intNota1 >= 5) printf("O aluno está aprovado");else printf("O aluno não está aprovado");

Como podemos ver nesse exemplo, se a nota do aluno for maior ou igual a cinco, o sistema diz que ele está aprovado. Se por outro lado a nota dele for menor que cinco, o sistema dirá que ele não está aprovado.

Ainda dentro do assunto de controle condicional, é importante conhecer os operadores binários responsáveis por interligar expressões. Por exemplo, se quisermos que um determinado programa seja responsável por dizer se um aluno está ou não aprovado e além de ter nota cinco na prova ele precisar de nota cinco nos trabalhos, podemos fazer assim:

if ((intNotaProva >= 5) && (intNotaTrabalhos >= 5)) printf("O aluno está aprovado");else if (intNotaProva < 5) printf("O aluno não está aprovado porque teve nota menor que 5 na prova");else printf("O aluno não está aprovado porque teve nota menor que 5 nos trabalhos");

Outro conceito importante que precisamos aprender nesse ponto é o conceito que envolve o uso dos parênteses em C. Intuitivamente sabemos que diferentes operadores, em qualquer linguagem, têm prioridade uns sobre os outros. Por exemplo: a expressão 4*x+4 é diferente da expressão 4*(x+4). Isto acontece porque na linguagem matemática o operador de multiplicacão tem prioridade sobre o operador de soma, logo será calculado primeiro. Quando colocamos os parênteses, negligenciamos essa regra, obrigando quem lê a expressão a calcular primeiro o que está dentro dos parênteses para depois continuar calculando o que está fora. Assim, no exemplo, primeiro somamos e depois multiplicamos. Em C essa idéia funciona do mesmo jeito, com a diferença de que normalmente usamos os parênteses também para tornar as expressões mais legíveis, ou seja, mais fáceis de entender para quem as lê.

Nesse caso, o uso dos parênteses no primeiro condicional é obrigatório, porque estamos combinando duas expressões condicionais. Vamos dar mais uma olhada nessa técnica:

/* Exemplo 1 : Combinação de três expressões condicionais */if ( (expressao1) && (expressao2) && (expressao3) ) comando;

Podemos ver que, na prática, temos uma expressão que é equivalente à combinaçao das outras três.

- Exemplo 2: Se tivermos a seguinte combinação: O aluno é aprovado se tiver nota 5 nas provas E nos trabalhos, OU se tiver ótimo comportamento

-- Passo 1: Montar o esqueleto do comando*/if ( ( () && () ) || () )

-- Passo 2: Inserir as expressões e os comandosif ( ( (intNotaProva >= 5) && (intNotaTrabalhos >= 5) ) || (isBomAluno) ) printf("O aluno está aprovado");

Como podemos ver, a nossa combinação de expressão funciona como se fosse composta por apenas duas: (aprovado pelas notas) OU (aprovado pelo comportamento) e a nossa expressão "aprovado pelas notas" é uma combinação de duas: (aprovado na nota da prova) E (aprovado na nota do trabalho).

Assim montamos qualquer expressão lógica, combinando qualquer número de afirmações e criando hierarquia entre elas. O programador pode tudo!

O comando de controle While

Agora que aprendemos a controlar o fluxo do nosso programa, ou seja, o caminho que ele percorre dentro do código durante a sua execução, vamos ver como extender ainda mais esse nosso controle do programa, utilizando estruturas que além de controlar que caminhos nosso programa percorre, diz ao programa por quanto tempo ele deve ficar executando aquela rotina, dando a ele uma condição de parada. Vamos ver também que podemos controlar de diversas formas essas repetições, antes e depois da execução da rotina. Vamos lá?

Estruturas de repetição com controle antes da execução

Vamos agora falar sobre o famoso comando while do C. Toda vez que a execução do programa chegar no comando de entrada do while (incluindo a sua primeira execução) é feita uma checagem da expressão contida no comando. Caso ela seja valorada como verdadeira, a rotina é executada. Caso contrário, seja a execução em questão a primeira ou não, o bloco de comandos contido no while é simplesmente ignorado. Vamos à um exemplo:

/* Imprime todos os números entre 0 e 100 */intNumero = 0;while (intNUmero <= 100) {

printf("%d ",intNumero);

intNumero = intNumero + 1;

}

Como podemos ver, o bloco de comandos contido na instrução while vai ser executado 101 vezes, imprimindo do 0 até o 100. Cada vez que a execução do programa entrar no while, um número vai ser impresso (o contido na variável intNumero) e então essa variável vai ser incrementada, ou seja, vai ser aumentada, em um. Após a finalização da execução do bloco a validade da expressão (intNumero <= 100) será novamente checada.

Page 21: Linguagem de Programação C

O segundo exemplo de instrução de repetição com controle antes da execução é o comando for. Com o comando for, não precisamos mais modificar o valor da variável que está controlando as repetições dentro do bloco de comandos. No próprio cabeçalho da instrução vamos dizer ao programa qual a variável de controle, qual o seu valor inicial, qual a sua condição de parada e como queremos que ela seja modificada. Vale lembrar também que nesse caso todos esses parâmetros mencionados são opcionais. Vamos ver alguns exemplos:

/* Imprime todos os números entre 0 e 100 */for (intNumero = 0; intNumero <= 100; intNumero + 1) printf("%d ",intNumero);

Como podemos ver, nossa estrutura de repetição ficou mais simples e mais legível. É importante aprender aqui que a variável de controle utilizada na estrutura, no caso intNumero, não precisa estar inicializada (com um valor pré-definido), mas precisa ter sido previamente declarada (no começo do programa).

No nosso próximo exemplo, o uso do comando while seria mais adequado. Você sabe dizer o motivo?

/* Imprime todos os números entre dois quaisquer escolhidos pelo usuário */printf("Entre com o primeiro número: "); scanf("%d",&ampintNumero1);printf("Entre com o segundo número: "); scanf("%d",&ampintNumero2);

for ( ; intNumero1 <= intNumero 2 ; intNumero1 + 1) printf("%d ",intNumero1);

Como podemos ver, o primeiro parâmetro do comando for foi deixado em branco. Como mencionado anteriormente, isso não é um erro e podemos até criar uma repetição em que todos os parâmetros do for estão em branco. O que você imagina que acontecerá nesse caso?

Ah! Há no nosso último exemplo uma instrução que parece um erro de lógica, mas não é. Você sabe dizer qual é? Tente analisar várias entradas possíveis para os dois números. Comente sua opinião no fórum de dúvidas.

Vamos ver o padrão do comando for:

for ( <inicialização> ; <expressão de continuação> ; <modificação> ) {bloco}

Questão

Freqüentemente um programador tem que ficar horas grudado no computador depurando um de seus programas, atrás de erros de lógica. O grande objetivo que qualquer programador tem que ter bem definido antes de sentar para produzir código é produzir esse código sempre com a menor quantidade possível de erros, sejam eles provenientes da má utilização da lógica de programação ou da linguagem suas sintaxes e semânticas. Ou seja, temos sempre que tentar minimizar a quantidade de faltas cometidas em cada bloco de código. Por este motivo vamos exercitar descobrindo qual o erro de lógica contido nesse exemplo. É um problema simples e não deve dar muito trabalho à vocês.

Para os que não lembram como funciona o algoritmo que encontra o mínimo múltiplo comum de dois números: Utiliza-se o método da fatoração, em que os dois números envolvidos são divididos seqüencialmente pelos números primos até que os dois sejam iguais a um. Quando isso acontecer, multiplicamos todos os primos utilizados, mesmo que os que utilizados mais de uma vez. Desse produto tempos o MMC procurado. Vale lembrar também que apenas trocamos o número primo utilizado para dividir os dois números quando nenhum dos dois números envolvidos pode ser dividido pelo número primo, por exemplo, quando temos 5 e 15 e estamos tentando dividí-los por 2. Então nesse caso temos que trocar o primo dois pelo primo três e depois pelo primo cinco, o qual poderemos utilizar para dividir os números.

/* Este programa calcula o mínimo múltiplo comum entre dois inteiros dados pelo usuário */

#include <stdio.h>

int main() {

int intNumero1=0, intNumero2=0, intMMC=1;

int intContadorPrimo=0;

int intPrimos[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59 };

printf("Entre com o primeiro numero: "); scanf("%d",&ampintNumero1);

printf("Entre com o segundo numero: "); scanf("%d",&ampintNumero2);

while (intNumero1 >= intNumero2) {

printf("Erro: O segundo número tem que ser maior que o primeiro!\n");

printf("Entre com o primeiro número: "); scanf("%d",&ampintNumero1);

printf("Entre com o segundo número: "); scanf("%d",&ampintNumero2);

Page 22: Linguagem de Programação C

}

for (; ((intNumero1 > 1) || (intNumero2 > 1)) ; intContadorPrimo += 1) {

if ( !(intNumero1 % intPrimos[intContadorPrimo]) && !(intNumero2 % intPrimos[intContadorPrimo]) ) {

intNumero1 /= intPrimos[intContadorPrimo];

intNumero2 /= intPrimos[intContadorPrimo];

intMMC *= intPrimos[intContadorPrimo];

}

else if ( !(intNumero1 % intPrimos[intContadorPrimo]) ) {

intNumero1 /= intPrimos[intContadorPrimo];

intMMC *= intPrimos[intContadorPrimo];

}

else if ( !(intNumero2 % intPrimos[intContadorPrimo]) ) {

intNumero2 /= intPrimos[intContadorPrimo];

intMMC *= intPrimos[intContadorPrimo];

}

}

printf("O MMC dos numeros dados eh: %d\n",intMMC);

return (0);

}A sua resposta :

A instrução for está incrementando a variável intContadorPrimo toda vez que ela executa o bloco de códico nela contido.

Realmente. Da forma como a instrução for está feita, a variável vai ser incrementada todas as vezes que o loop for iniciado, sendo que de acordo com o algoritmo de cálculo do MMC, essa variável só deve ser incrementada quando nenhum dos dois números for divisível pelo primo envolvido.

Lição 4 - Elementos léxicos e sintáticos da linguagem C, parte 3

Vetores e Matrizes

Essa é uma parte bem importante de qualquer linguagem de programação porque torna muito mais fácil a vida do programador. Imagine que você esteja desenvolvendo um software de agenda de contatos e que atualmente você tenha 100 nomes armazenados. Você inicializaria 100 variáveis? E se amanhã você cadastrasse mais um usuário, você teria que modificar o código? Vamos ver como guardar grandes quantidades de informação de uma forma padronizada e com pouco código.

Suponha que nós queremos fazer a média das notas de um aluno e que essa média seja composta por quatro provas, dois trabalhos e uma nota conceitual, ou seja, precisamos guardar sete valores de notas. Ao invés de criar sete variáveis do tipo ponto flutuante, vamos criar um vetor com sete posições, lembrando que em C as posições começam no zero.

A média do aluno deve ser calculada utilizando-se o seguinte critério: ( (4 * média das provas) + (3 * média dos trabalhos) + (3 * nota conceitual) ) / 10;

#include <stdio.h>

#define PESO_NOTAS 4#define PESO_TRABS 3#define PESO_OUTRAS 3#define PESO_TOTAL 10

Page 23: Linguagem de Programação C

#define NUM_NOTAS 7#define NUM_PROVAS 4#define NUM_TRABS 2#define NUM_OUTRAS 1

int main() {

float vetNotas[7], floatMedia;

int intContador;

for (intContador=0; intContador < NUM_PROVAS; intContador++) {

printf("Digita a nota da %da prova: ",intContador+1);

scanf("%f",&ampvetNotas[intContador]);

}

for (intContador=0; intContador < NUM_TRABS; intContador++) {

printf("Digite a note do %do trabalho: ",intContador+1);

scanf("%f",&ampvetNotas[intContador+NUM_PROVAS]);

}

for (intContador=0; intContador < NUM_OUTRAS; intContador++) {

printf("Digite a %da nota conceitual do aluno: ", intContador+1);

scanf("%f",&ampvetNotas[intContador+NUM_PROVAS+NUM_TRABS]);

}

floatMedia=0;

for (intContador=0; intContador < NUM_PROVAS; intContador++) floatMedia += PESO_NOTAS * vetNotas[intContador] / (NUM_PROVAS * PESO_TOTAL);

for (intContador=0; intContador < NUM_TRABS; intContador++) floatMedia += PESO_TRABS * vetNotas[intContador+NUM_PROVAS] / (NUM_TRABS * PESO_TOTAL);

for (intContador=0; intContador < NUM_OUTRAS; intContador++) floatMedia += PESO_OUTRAS * vetNotas[intContador+NUM_PROVAS+NUM_TRABS] / (NUM_OUTRAS * PESO_TOTAL);

printf("A media do aluno eh: %.2f\n",floatMedia);

return(0);

}

Como pode ser visto no exemplo, utilizamos uma forma bem intuitiva para saber em que posição do vetor estamos. Se temos um vetor com sete posições válidas, não podemos nunca esquecer que apenas podemos ler da posição 0 até a posição 6. Dizem por aí que o compilador deixa você se estrangular, se você tentar - parece ser verdade. Por isso, tente sempre utilizar constantes e utilizar essas constantes para controlar a leitura dos vetores. O interessante é que qualquer mudança no programa com relação as notas, provas e trabalhos pode ser feita apenas modificando o valor das constantes.

Matrizes

Page 24: Linguagem de Programação C

Essa subseção é apenas uma extensão da anterior. Imagine um vetor de 10 números inteiros. Imaginou? Agora copie esse vetor e cole ele 10 vezes embaixo do inicial - temos então 10 vetores, um acima do outro, formando uma matriz. Agora, para saber em que posição estamos, precisamos não só da posição na linha, mas também da linha em que estamos. Por exemplo, imagine se queremos analisar como a temperatura varia durante o dia. Queremos medir a temperatura de 1 em 1 minuto e guardar tudo em uma matriz. Podemos fazer assim:

float vetTemperatura[24][60];

dessa forma, o elemento vetTemperatura[12][0] armazena a temperatura medida no minuto 12:00, enquanto que o elemento vetTemperatura[23][59] armazena a temperatura medida exatamente às 23:59.

A título de curiosidade, quantas temperaturas vamos medir por dia dessa forma? 24 * 60 = 1440 elementos. E quantos bytes estamos ocupando? Bem, sabemos que um número ponto flutuante ocupa 4 bytes - logo ocupamos 4 * 1440 bytes = 5760 bytes.

Strings

Vamos ver agora uma parte bastante importante do C. Já comentamos antes que introduzimos na nossa linguagem de programação aquelas operações que nós utilizamos no cotidiano e que conseqüentemente se tornam necessárias nos nossos programas. Com as strings não é diferente - na grande maioria dos programas que fazemos precisamos ler algo da tela, mais específicamente frases como nomes, endereços e texto digitado pelo usuário. No entanto enquanto formos aprendendo a manipular as strings, vamos ter que paralelamente ir aprendendo a realizar essas operações com segurança, já que muitas vezes o compilador do C nos permite fazer coisas proibitivas, que têm alta probabilidade de gerar problemas.

O primeiro conceito importante sobe as strings que precisamos aprender é que as strings não existem. O que chamamos de string na verdade é um vetor de caracteres, ou seja, uma seqüência de caracteres que, quando impressa gera a frase desejada - lembrando que como qualquer outra estrutura da linguagem C, começamos a contagem do índice da string a partir do número zero.

O segundo conceito importante que vamos ver é que as strings são vetores especiais e por isso precisam de cuidados especiais. Quando manipulando-as precisamos saber bem quando elas acabam. Para isso existe o caractere nulo - '\0'. Todas as nossas strings precisam de um caractere desse depois do seu último caractere válido. Sem esse caractere, podemos acabar lendo ou escrevendo em áreas da memória que não nos pertencem, gerando problemas.

gets

Essa é a primeira das funções que manipulam strings que vamos ver. A gets é uma função poderosa e versátil, mas provavelmente por esses mesmos motivos é uma função bastante perigosa. Com ela podemos ler uma string que contenha qualquer caractere diferente do caractere de quebra de linha '\n'. Quando a função começa a ler uma string, ela vai lendo caractere a caractere, até encontrar o caractere de quebra de linha. Por um lado, a utilidade dessa função está no fato de conseguirmos, com ela, ler strings que contenham espaços como nomes completos e endereços o que não é possível com a função scanf, por exemplo. Além disso ela se preocupa em colocar, no final da string, o tão importante caractere nulo '\0'. Por outro lado, no seu poder está o seu perigo. Por não se preocupar em que área da memória ela está escrevendo a string e não se preocupar em parar de ler enquanto não encontrar a quebra de linha, é possível que, se o programador não preparar a string antes e o usuário inserir uma string muito grande, a string seja maior do que o espaço alocado, o que normalmente causa problemas de escrita em áreas indevidas da memória. O protótipo da função é o seguinte, sendo que ela faz parte do header stdio.h:

char * gets ( char * str );

Vamos falar sobre os ponteiros em breve, mas desde já é importante perceber que o que essa função recebe como parâmetro é um ponteiro para um char. Isso quer dizer, que na verdade, estamos passando para a função gets não uma string, assim como passamos para algumas funções um inteiro, mas sim um endereço da memória RAM que já foi preparado para receber a string (assim esperamos que nunca esqueçam disso). A função gets, quando receber esse endereço da memória, começará a escrever exatamente nesse endereço, se movendo na memória conforme vai escrevendo. Assim, concluímos que estamos enviando para a função não o endereço da string, mas sim o endereço do primeiro caractere passível de escrita da string.

strcpy

Essa é a primeira das quatro funções que vamos ver que necessitam do header string.h para funcionarem. O que ela faz é copiar uma string de um lugar da memória para outro, caractere a caractere. Vale lembrar que ela não vai se preocupar se o programador preparou a área da memória que vai receber a string ou não. É muito importante que o programador prepare as áreas da memória que vai utilizar, com o objetivo de evitar escrita de áreas de memória que não lhe pertencem. O protótipo da função é:

char * strcpy ( char * destination, const char * source );

Podemos ver que, assim como a função gets e praticamente todas as outras do C, utilizamos endereços de memória ao invés de nomes de variáveis.

strcat

O que essa função faz é concatenar duas strings - isto é, sobrescrever o caractere '\0' da string final com o primeiro caractere válido da string que vai ser adicionada à string final. Assim, temos uma string que é como se fosse a soma das duas, mas nunca faça "strString1 = strString1 + strString2". Lembrando que assim como as outras funções que manipulam as strings, a strcat não tem como controlar em que área da memória ela está

Page 25: Linguagem de Programação C

escrevendo, então lembre-se de checar antes se há espaço para receber a nova string.

O protótipo da função é

char * strcat ( char * destination, const char * source );

strlen

Essa string é de extrema importância porque com ela podemos descobrir o tamanho atual das strings. Sabendo o tamanho atual delas e o tamanho máximo que elas podem ter, descobrimos se podemos ou não realizar determinadas operações, como as concatenações por exemplo. O que ela faz é contar do primeiro caractere até o caractere '\0', sem incluir este. Ou seja, se tivermos a seguinte string:

[0] [1] [2] [3] [4] [5] [6]B r a s i l \0

Ao utilizarmos a função strlen vamos receber dela o número seis, ou seja, temos seis caracteres válidos da posição zero até a cinco e temos o caractere nulo na posição seis. Seu protótipo é:

size_t strlen ( const char * str );

Podemos ver que ela retorna um tipo chamado de site_t, um tipo específico de algumas funções. No entanto podemos normalmente atribuir o valor retornado por essa função a um inteiro, imprimir direto na tela ou utilizar como limite em um loop.

strcmp

Essa é a quarta e última função da biblioteca string que vamos ver aqui. Com ela podemos comparar duas strings, uma operação muito importante que é análoga à operação "strString1 == strString2" (nunca faça isso). Ela nós dirá se uma string é menor, igual ou maior que outra. Seu protótipo é:

int strcmp ( const char * str1, const char * str2 );

Vemos que ela retorna um número inteiro. O importante aqui é que ela retorna três tipos de números. Se as duas strings forem iguais, ela retorna zero. Se o primeiro caractere que diferencia as duas for maior (tabela ASCII) na str1, ela retorna um número positivo, e retorna um número negativo caso o primeiro caractere que diferencia as duas for menor na str1 do que na str2. Esse "menor" e "maior" se referem à tabela ASCII, ou seja, o "maior" é o caractere cujo respectivo número inteiro é maior de acordo com a tabela ASCII

ps: Para ver a tabela ASCII basta digitar "tabela ASCII" no Google.

Introdução ao uso de ponteiros

Vamos ver agora o que é uma das partes mais úteis da linguagem C. Os ponteiros são responsáveis por aumentar exponencialmente o poderio e a velocidade do C, tornando inicialmente a vida do programador mais complicada, mas muito mais eficiente assim que ele aprende a utilizar os ponteiros sem gerar erros de segmentação. Vamos ver que esses erros são muito comuns ao se iniciar a utilização dos ponteiros porque estamos dizendo ao programa aonde ele deve escrever determinados dados, o que pode levar a erros de segmentação se não fizermos isso com cuidado. Vamos ver também que os ponteiros são tão utilizados que a grande maioria das funções do C os recebem como parâmetro e os devolvem também depois da execução da função, e é exatamente isso que torna o C tão rápido.

Imagine a seguinte situação: queremos calcular a média entre cinco números. Para isso queremos criar uma função que recebe esses números e calcula a sua média, devolvendo-a ao final do processo. Podemos criar essa função de várias maneiras:

float calculaMedia1 (float floatNumero1, float floatNumero2, float floatNumero3, float floatNumero4, float floatNumero5);

Dessa forma, não só a chamada para a nossa função ficou bastante extensa (o que gera perda de legibilidade no código) como estamos literalmente "copiando" cinco variáveis para a área de execução da função na memória.

Para entender isso vamos falar um pouco sobre como funcionam as chamadas à função no C. No C, até as funções podem ser associadas a um ponteiro - um ponteiro de função. O que acontece é que quando chamamos uma função, ela (o seu bloco de código) é copiada para uma área da memória em que ela vai ser executada. Junto com o bloco de código da função, são copiadas para essa nova área todas as variáveis passada pelos parâmetros da função, para que a função possa utilizar essas variáveis. Assim estamos copiando todas as cinco variáveis toda vez que chamamos a função que criamos. Imagine chamando essa função um milhão de vezes.

float calculaMedia2 (float vetNumeros[]);

Dessa forma, a chamada à função ficou um pouco mais legível, mas ela não ficou nem um pouco mais eficiente. Isto quer dizer que estaremos copiando todas as variáveis para a área de execução da função, assim como no método anterior.

float calculaMedia3 (float * vetNumeros);

Page 26: Linguagem de Programação C

Dessa forma mantemos a legibilidade da chamada à função e agora estamos passando para ela o endereço de memória do primeiro elemento de um vetor de pontos flutuantes. Assim, só o que a função ocupa de espaço é o espaço necessário para se armazenar um endereço de memória, e nada é copiado. A função então acessa a área de memória utilizada pelos números, lendo-as lá mesmo. Temos apenas um problema: A função não vai saber quando o vetor acaba. Para isso podemos fazer a função de uma quarta forma:

float calculaMedia4 (float * vetNumeros, int intTamanho);

Dessa forma vamos poder passar para a função o tamanho do vetor, o que provavelmente nós sabemos durante a sua criação.

Um conceito importante que precisamos aprender à respeito dos ponteiros é a possibilidade de escrita. Talvez você tenha percebido que nos parâmetros de algumas funções aparece a palavra "const". Esta palavra indica que, além da variável ser do tipo ponteiro, quem acessa aquela área da memória a partir daquele endereço passado não pode alterar o que está lá. Isto é muito importante principalmente quando há um grupo de programadores trabalhando, cada um com uma parte do projeto. Nesse caso, o programador que faz uma parte do projeto libera para os outros programadores as funções que a área dele implementa e nessas funções ele tem a opção de utilizar a palavra const com o objetivo de impedir que os outros programadores modifiquem variáveis importantes para a área dele. Algumas alterações poderiam gerar problemas em todo o projeto e seriam difíceis de encontrar porque as pessoas procurariam o problema na área da pessoa e não encontrariam nada, já que o que gera o erro está na escrita indevida de variáveis por outras áreas do projeto.

Para utilizar um ponteiro, precisamos do operador '*'. Com ele indicamos para um compilador que aquela variável não é do tipo identificado, mas um ponteiro para aquele tipo identificado. Podem existir ponteiros para quase todos os tipos implementados na linguagem C, mas os mais comuns são os ponteiros para char e para int. Criamos os ponteiros assim:

int* ptrNumero1;

Assim criamos uma variável que não pode ser manipulada assim:

ptrNumero1 = 10;

Isto é um erro grave porque o que a variável armazena não é um número e assim um endereço de memória. Podemos manipular os ponteiros assim:

int intNumero1 = 10;int* ptrNumero1 = NULL;

/* Armazena o endereço de memória da variável intNumero1 no ponteiro ptrNumero1 */ptrNumero1 = &ampintNumero1;

/* Utiliza o vetor para alterar o conteúdo da variável do tipo inteiro */*ptrNumero = 20;

Vamos analisar os novos operadores aprendidos. Quando fazemos "ptrNumero1 = " estamos atribuindo à variável do tipo ponteiro um endereço de memória. Já quando fazemos "*ptrNumero1 = " estamos atribuindo ao conteúdo do endereço de memória armazenado pelo ponteiro alguma coisa. Assim, podemos utilizar "*ptrNumero1" como se fosse uma variável do tipo inteiro, com todas as operações aritméticas cabíveis aos inteiros. Já com "ptrNumero1" podemos apenas realizar algumas operações que vamos aprender em breve.

Vimos também o operador '&'. Você se lembra dele na função scanf? Esse operador quer dizer "endereço de". Com ele, fazemos o que pode ser considerada a operação inversa do operador '*'. Enquanto que o operador '*' quer dizer "conteúdo de", o operador '&' quer dizer "endereço de" (faria sentido fazer "*&ampintNumero1"?).

O terceiro operador que vimos é o operador NULL. O que acontece é que quando criamos uma variável do tipo ponteiro, assim como as outras variáveis, ela não é criada automaticamente inicalizada com um endereço nulo (o análogo ao zero de um inteiro, por exemplo). Isto quer dizer que quando um ponteiro é criado ele contém um endereço de memória aleatório, que pode existir ou não, e pode ser escrita ou não. Imagine que em um pior caso seja uma área de memória do sistema operacional e que seja muito importante para ele. O que aconteceria se você tentasse escrever nessa área? Para isto serve o operador NULL, com ele dizemos que aquele ponteiro não aponta para lugar algum.

Operações com os ponteiros

Considere o seguinte programa:

int vetNumeros[100];int i;

int* ptrNumero = NULL;

/* Numeros de 1 a 100 no vetor */for (i=0 ; i<100; i++) vetNumeros[i] = i+1;

/* Passando o endereço de memória do primeiro elemento para o vetor */ptrNumero = vetNumeros;

Page 27: Linguagem de Programação C

/* Imprime: "O primeiro elemento é o 1" */printf("O primeiro elemento é o %d\n", *ptrNumero);

/* Incrementando o ponteiro */ptrNumero += 1;

/* Imprime : "O segundo elmento é o 2" */printf("O segundo elemento é o %d\n" *ptrNumero);

/* Imprime de 1 a 100 */for (i=0 ; i<100 ; i++) printf("O %do elemento é %d\n", i+1, *(ptrNumero+i));

Vamos analisar algumas das operações feitas:

O que preciamos aprender primeiro é que quando incrementamos ou decrementamos um ponteiro em um número inteiro, ou seja, somamos um número inteiro ao ponteiro, isto funciona porque o compilador C sabe de que tipo é o inteiro a partir do momento que você o declarou. Sabendo o tipo do ponteiro, quando você adiciona um número inteiro ao ponteiro o compilador sabe que na verdade o que você quer fazer é mover o ponteiro na memória. Então quando temos um ponteiro do tipo inteiro por exemplo e o incrementamos em um, na verdade estamos movendo o ponteiro um inteiro para a direita, ou x bits em que x é o espaço que um inteiro ocupa na memória.

Por que fizemos *(ptrNumero+1) ou invés de *ptrNumero+1? Simplesmente por se tivéssemos optado pela segunda opção, ele teria pego o inteiro identificado por *ptrNumero e depois teria somado um. Já com *(ptrNumero+1), o que acontece é que se utiliza um inteiro após *ptrNumero e não se soma um.

Casts

Vamos falar brevemente sobre os casts. Eles são bastante úteis na linguagem C já que freqüentemente nos depararmos com tipos incompatíveis, embora precisemos realizar operações com esses tipos. Além disso, os casts são utilizados com freqüência para determinar o tipo de um ponteiro, evitando que ele seja do tipo void. Usamos os casts para dizer ao compilador que tipo queremos que determine o comportamento de uma variável, é claro que respeitando os limites de conversão entre as variáveis. Vamos ver alguns exemplos:

int intNumero1=10;float floatNumero1=3.0;float floatReposta;

floatResposta = intNumero1 / floatNumero1;

Quando fazemos isso, podemos imaginar o que acontece na execução do programa. É feita uma divisão de inteiros entre o 10 e o 3, tendo como resultado 3. Esse valor é então convertido posteriormente em float para ser armazenado na variável floatResposta. Assim, o valor final é 3.0. No entanto, se fizermos da seguinte forma:

floatResposta = (float)intNumero1 / floatNumero1;

O compilador vai tratar tudo que está à direita do cast como sendo do tipo float. Assim, a operação vai retornar 3.3333 ao invés de 3.0, e é esse o valor que vai para a variável floatResposta.

Uma das utilidades dos casts está nos arredondamentos e outas operações semelhantes com inteiros e pontos flutuantes.

Código estruturado em C e ponteiros

Esta seção aborda conceitos de estruturação do código em linguagem C usando funções e estruturas simples de dados. Temas como alocação dinâmica de memória, subdivisão do programa e recursividade serão tratados aqui. Adicionalmente, uma introdução à entrada e saída em C será apresentada aqui.

Lição 5 - Manipulação de arquivos e entradas/saídas em C

Entrada e saída com o teclado e o monitor

Vamos estudar agora as formas mais utilizadas de entrada e saída padrões, ou seja, o teclado e o monitor, respectivamente. Futuramente vamos falar sobre as strings, mas nessa página já poderemos ter alguma noção de como elas são estruturadas. Vamos também relembrar alguns dos tipos de dados aprendidos anteriormente, já que agora vamos utilizar códigos que identificam o tipo da variável que vai ser lida ou escrita.

A função printf

Essa é com certeza uma das funções mais utilizadas na linguagem C, sem falar uma daquelas que todos que sabem alguma coisa sobre C conhecem. Com ela poderemos nos comunicar com o usuário, mostrando a ele tudo que queremos: insturções de uso do programa, instruções de entrada dos dados, notas e lembretes relacionados ao algoritmo adotado e os resultados do processamento do programa. Com ela podemos imprimir praticamente qualquer dado presente na execução do programa, impregando os diferentes códigos de impressão adotados pela

Page 28: Linguagem de Programação C

função, desde números inteiros, passando por strings até valores de endereços na memória RAM. Além disso, vamos ver o que é um dos aspectos mais interessantes dessa e de outras funções do C que é o fato de ela aceitar quantos parâmetros o programador desejar. Com ela, em apenas uma instrução, ou seja, chamada à função printf, podemos imprimir quanto texto quisermos, junto com qualquer quantidade de variáveis, inclusive de diferentes tipos. Vamos ver alguns exemplos:

/* Mensagem de abertura do programa e boas vindas ao usuário */printf("Olá usuário. Este programa foi desenvolvido por Fernando e tem como objetivo calcular o rendimento das suas ações no mercado de valores a partir de alguns dados fornecidos por você. Vamos guardar todas as informações fornecidas e prover relatórios de desempenho ao longo do tempo, acompanhando o desempenho dos seus negócios. Qualquer dúvida entre em contato com o desenvolvedor em [email protected].\n").

/* Imprimindo o número de dias que o usuário vai ter que deixar as ações aplicadas para ter o rendimento espero, considerando que o desempenho das ações mantenha o padrão atual */printf("Após %d dias, as ações aplicadas terão %.2f%% de rendimento, como esperado.\n").

/* Imprimindo uma tabela com as ações do usuário */printf("| AÇÃO APLICADA | VALOR DE COMPRA | VALOR ATUAL | OSCILAÇÃO |\n");printf("------------------------------------------------------------------------------------|");for (intContadorAcoes=0 ; intContadorAcoes < intNumeroAcoes ; intContadorAcoes++) {

printf("%s | R$%.2f | R$%.2f | %.2f%% |\n", vetAcoes[intContadorAcoes], vetCompra[intContadorAcoes], vetAtual[intContadorAcoes], vetOsc[intContadorAcoes])/

}

Não se preocupe se não tiver entendido alguns dos símbolos do exemplo. Esse é um exemplo levemente mais complexo em que o programa imprime o cabeçalho de uma tabela e então naturalmente, devido à estrutura de repetição envolvida, imprime toda a tabela. A posição dos separadores da coluna ainda precisa ser trabalhada de forma a imprimir estes separadores um em cima do outro. Pudemos ver também que nesse exemplo em apenas uma chamada à função printf, mostramos ao usuário o conteúdo de quatro variáveis, sendo que nem todas são do mesmo tipo. Essa é a versatilidade da função printf que pretendemos explorar de forma a implementar a melhor interface possível com o usuário.

Vamos ao código utilizados com a função printf:

%d : Inteiro;%f : Ponto flutuante (decimal);%e : Notação científica (i.e. 1,2e4);%E : Notação científica com 'e' maiúsculo (i.e. 1,2E4);%s : String;%p : Endereço de memória;%g : Escolhe automaticamente a melhor opção entre o %f e o %e;%G : Semelhante ao anterior, mas usando %f e %E;%u : Inteiro sem sinal;%x : Hexadecimal com letras minúsculas;%X : Hexadecimal com letras maiúsculoas.

Além desses símbolos, é importante aprender mais duas coisas: o símbolo %% e o controle de tamanho e casas decimais. O uso do %% é bem simples: como utilizamos o símbolo de porcentagem para denotar um código de variável, se quisermos mostrar na tela o símbolo % temos que usá-lo dessa forma %%. Com relação ao controle de casas decimais e tamanho, pode ser utilizado tanto com variáveis numéricas quanto com variáveis de texto.

%x.yd : Imprime um número inteiro com comprimento mínimo x e máximo y%x.yf : Imprime um número ponto flutuante com comprimento mínimo x e y casas decimais%x.ys : Imprime uma string com tamanho mínimo x e máximo y

Esses são usos já mesclando os dois tipos de controle, nos casos apresentados. Podemos normalmente fazer %.2f ou %4d. Experimente, veja o que consegue fazer.

Scanf

Essa função atua no programa como o oposto da função printf. Ela contempla a mesma idéia de um número qualquer de parâmetros, envolvendo inclusive variáveis de diferentes tipos. Seu objetivo é ler o que o usuário entra e armazenar esses dados nas variáveis escolhidas pelo programador de acordo com o padrão escolhido; é uma função com uma versatilidade enorme. Vamos ao protótipo:

int scanf ( const char * formato, ... );

Lembrando que todo os códigos com porcentagem aprendidos na função printf valem aqui também. Vamos ver alguns exemplos:

/* Lê dez números que o usuário escolhe */int vetNumeros[10];int intContador;

for (i=0 ; i<10 ; i++) scanf("%d",&ampvetNumeros[i]);

É importante perceber uma coisa nesse exemplo: a função scanf nunca recebe o nome da variável que vai receber o

Page 29: Linguagem de Programação C

conteúdo que o usuário entrou, e sim o endereço dessas variáveis na memória. Não se preocupe se não entender isso agora, vamos falar sobre ponteiros futuramente, mas o importante é que esse é o motivo de usarmos sempre o operador '&' ao usar a função scanf, a não ser que a variável que vai receber os dados seja um ponteiro, já tratando-se então de um endereço, não necessitando do operador.

Putchar

Essa é uma função simples que coloca um caractere na tela, na posição atual do cursor. Assim como a função printf utiliza o arquivo header stdio.h, mas ela aceita apenas um parâmetro, que é o inteiro que representa o caractere a ser impresso. Como na linguagem C todos os caracteres são traduzidos em inteiros que os representam de acordo com a tabela ASCII, as chamdas putchar(97) e putchar('a') fazem exatamente a mesma coisa, porque o inteiro que representa o caractere 'a' na tabela ASCII é o 97. Lembre-se disso porque essa idéia é utilizada em toda a linguagem C.

Gets

A função gets lê caracteres da saída padrão e escreve em uma string. É importante saber que ela faz isso até que o caractere de quebra de linha '\n' seja encontrado, ou seja, inclui na string espaços encontrados no meio do que foi digitado pelo usuário. A importância disso se dá no fato de que outras funções, como a própria scanf, pára de ler da entrada padrão quando encontra um espaço, o que impede o programador de receber, utilizando essas funções, frases com espaços como endereços, nomes completos etc. Por isso é importante, na hora de escolher a função que vai ser usada, levar isso em conta. Vamos ver o protótipo da função gets:

char * gets ( char * str );

A questão dos protótipos já foi mencionada na página que fala sobre funções, mas vamos dar uma recapitulada. O que vem antes do caractere '(' trata do tipo e do nome da função. Nesse caso, temos uma função do tipo char *, ou seja, que devolve um endereço de memória contendo a string. No caso dessa função, se ela for executada com sucesso, ela retorna o endereço da mesma string que foi apontada no seu parâmetro. O nome dela como já sabemos é gets e aí vem a parte interna da função, que mostra quais parâmetros e de quais tipos ela pode receber. Vale lembrar que algumas funções, como a printf, podem receber um número qualquer de parâmetros. No caso da função gets, ela recebe sempre um ponteiro para char, que é na verdade um endereço de memória do tipo char. Não se preocupe com relação aos ponteiros que vamos falar sobre eles mais tarde.

Puts

Essa função faz o que pode ser considerado o oposto da anterior. Ela escreve na tela a string passada pelo parâmetro e no final executa uma quebra de linha. Vamos ver seu protótipo:

int puts ( const char * str );

Podemos ver que ela retorna um inteiro. De acordo com a implementação, ela retorna um número não negativo caso seja executada com sucesso.

Introdução à manipulação de arquivos

Vamos falar hoje sobre uma parte muito importante de qualquer linguagem de programação. Trata-se de uma técnica chamada de persistência, utilizada, como o nome já diz, para manter dados em geral que estejam relacionados ao programa de alguma forma. Podemos realizar persistência tanto em dados manipulados pelo programa, como por exemplo registros de uma agenda de endereços, quanto em dados utilizados pelo programa para manipular outros dados - configurações como tamanho e posição da tela, configurações do programa, seções etc. Utilizando essas técnicas percebemos que elas aumentam bastante a eficiência dos nossos programas, à medida que os usuários não precisam reinserir dados e percebem que o progama mantém tudo que eles deixam aos cuidados do programa.

Podemos utilizar dois tipos de arquivos - um básico, manipulado na forma de texto - e um mais complexo, manipulado na sua forma binária, utilizado para gravar qualquer tipo de dado.

Arquivos texto e arquivos binários

Manipulamos os arquivos texto mais ou menos como se estivéssemos manipulando uma string. Podemos escrever e ler no arquivo, e também percebemos que existe uma espécie de cursor que aponta o local do arquivo que está sendo lido. Já nos arquivos podemos guardar qualquer coisa, ou seja, variáveis de qualquer tipo, incluíndo as structs que vamos estudar posteriormente. Podemos montar em arquivos binários estruturas que se parecem com vetores, com a diferença que os dados vão ficar guardados mesmo que o programa seja finalizado, o que é a maior vantagem da persistência.

Abrindo um arquivo

Primeiro temos que entender como o arquivo é manipulado dentro do programa escrito em C. Assim como para muitos tipos de dados em C utilizamos os ponteiros como forma de controlar os dados em trânsito, não é diferente com os arquivos - utilizamos um ponteiro do tipo FILE para controlar esse trânsito de informações, abrir e fechar o arquivo. Vamos ver um exemplo:

#include <stdio.h>

int main() {

Page 30: Linguagem de Programação C

FILE* arqInteiros1 = NULL;

int i;

/* Tenta abrir o arquivo */

if ((arqInteiros1 = fopen("Inteiros1.bin", "wb")) == NULL)

printf("Houve um erro na criação do arquivo - provavelmente permissão negada\n");

/* Escreve de 1 a 100 no arquivo */

for (i=0 ; i<100 ; i++) fwrite(&ampi, sizeof(int), 1, arqInteiros1);

fclose(arqInteiros1);

}

Vamos aprender algumas coisas que foram utilizadas no exemplo anterior:

fopen

Essa função é a responsável por criar e/ou abrir o arquivo que queremos utilizar no nosso programa. O importante na utilização dessa função é o segundo argumento dela, uma string que determina como o arquivo deve ser aberto, assim como qual vai ser a sua utilização. Seu protótipo é:

FILE * fopen ( const char * filename, const char * mode );

Vamos ver as diferentes formas de se acessar um arquivo:

"r" : Abre um arquivo texto para leitura;"w" : Abre um arquivo texto para gravação. Se ele não existir, é criado nesse momento;"a" : Abre um arquivo texto para gravação e posiciona o cursor do arquivo no final do mesmo. Cria o arquivo se ele não existir;"rb" : Abre um arquivo binário para leitura;"wb" : Abre um arquivo binário para leitura. Cria o arquivo se ele não existir;"ab" : Abre um arquivo binário para leitura e posiciona o cursor do arquivo no seu final. Cria o arquivo se ele não existir;"r+" : Abre um arquivo texto para leitura e gravação. O mesmo deve existir anteriormente;"w+" : Cria um arquivo texto para gravação. Se já existir um arquivo com esse nome, ele é destruído;"r+b": Abre um arquivo binário para leitura e gravação;"w+b": Cria um arquivo binário para leitura e gravação.

Basta utilizar a string desejada dentro da função fopen para acessar o arquivo como achar devido. No entanto o programador deve sempre lembrar de checar se o arquivo foi mesmo aberto. Para isto, basta que o ponteiro para arquivo seja criado com um valor nulo, como no exemplo anterior. Sendo assim, quando esse ponteiro for receber um endereço de memória da função fopen, se a abetura/criação do arquivo falhar, o ponteiro não vai receber nenhum endereço e vai continuar valendo NULL, permitindo que o programador saiba se a abertura/criação foi bem sucedida ou não. Uma forma elegante de fazer essa análise é a apresentada no exemplo.

fwrite

Com essa função podemos escrever praticamente qualquer coisa em arquivos binários, desde que saibamos aonde está o que queremos escrever e qual o tamanho total dos dados a serem escritos. Quando utilizamos a função passamos para ela algo parecido com: "escreve x bytes a partir do byte y na memória". Não importa exatamente o que estamos escrevendo e sim aonde está e qual o tamanho, por isso podemos escrever praticamente qualquer tipo de dado. Vamos ver o protótipo:

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

const void * ptr : Endereço de memória do primeiro byte que queremos escrever. Pode ser o endereço de uma variável ou uma string explícita.

size_t size: Tamanho da estrutura que estamos escrevendo. Esse é um bom momento para aprendermos a utilizar a macro sizeof(). Como o nome já diz, sizeof é uma macro e não uma função - isso quer dizer que quando a utilizamos, ela é processada não em tempo de execução, como as funções, mas em tempo de compilação. Isso quer dizer que essa análise é feita pelo pré-processador, que quando vai compilar o texto substitui a macro pelo valor correto da estrutura. Assim, podemos fazer sizeof(int), sizeof(char) e com qualquer outra estrutura conhecida pelo pré-processador do C. Dessa forma não precisamos nos preocupar com o tamanho de cada tipo de dado na arquitetura em questão, já que os valores variam de arquitetura para arquitetura.

size_t count: Número de vezes que a estrutura de tamanho sizeof(estrutura) deve ser escrita no arquivo, a partir do endereço de memória apontado por ptr. Utilizamos isso se quisermos escrever um vetor, por exemplo. Como os vetores sempre estão contíguos na memória, podemos passar o endereço do primeiro elemento e o número de elementos que queremos escrever, que a função faz o resto.

FILE * stream: Ponteiro para o arquivo já aberto pela função fopen.

Page 31: Linguagem de Programação C

Um detalhe importante com relação à essa função e á fread também é que as duas retornam o número de elementos realmente escritos/lidos. Esse retorno pode e deve ser utilizado como uma forma confiável de controlar a atuação da função, sabendo se o trabalho foi realmente feito.

fread

Vamos aproveitar o momento para aprender a função oposta à fwrite. Utilizamos ela exatamente da mesma forma que a fwrite, com a diferença que essa tem como objetivo ler dados do arquivo ao invés de escrever. O protótipo dela é:

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

void * ptr: Área da memória que vai receber os dados lidos do arquivo. É muito importante lembrar de preparar essa área da memória antes dessa leitura.

size_t size: Tamanho de cada elemento da estrutura a ser lida. Dica: utilizar sizeof().

size_t count: Número de elementos na estrutura a ser lida.

FILE * stream: Ponteiro para o arquivo já aberto contendo a estrutura.

fclose

Sempre que abrirmos um arquivo em C é muito importante que não esqueçamos de fechar o mesmo depois de manipular seus dados. O sistema operacional mantém uma lista dos arquivos que estão abertos e sendo acessados e não vai permitir que muitos fiquem nessa situação. Por esse motivo é importante que apenas os arquivos sendo manipulados estejam abertos. Essa é uma boa prática de programação. O seu protótipo é:

int fclose ( FILE * stream );

feof

Já vamos aprender a manipular também em um arquivo texto, mas antes é importante sabermos quando um arquivo acabou. Para isso utilizamos a função feof que tem o protótipo:

int feof ( FILE * stream );

Uma forma bem interessante de utilizar essa função é a seguinte:

n = 0;

while (!feof(arqArquivo1)) {

fgetc (pFile);

n++;

}

fclose (pFile);printf ("Total de bytes lidos: %d\n", n);

Dessa forma assim que o arquivo acabar, a função feof vai retornar 1 ao invés de 0, o que vai fazer com que a estrutura de repetição seja anulada.

fgetc

int fgetc ( FILE * stream );

Essa função, utilizada no exemplo anterior, retorna o caractere presente no cursor do arquivo texto. Ela pode ser utilizada para ler o arquivo texto de uma forma seqüencial, assim como podemos ler o que está na tela com a função getc. O que utilizamos, no caso, é o seu retorno, que deve ser atribuído a uma variável.

fprintf

Essa função é a printf dos arquivos. Com ela podemos imprimir em um arquivo texto praticamente qualquer coisa. Seu protótipo é:

int fprintf ( FILE * stream, const char * format, ... );

Vamos ver um exemplo:

fprintf(arqArquivo1, "%s - %d\n", strNome, intDiasTrabalhados);

Page 32: Linguagem de Programação C

Essa função é muito útil para atividades de log, por exemplo. Com ela podemos manter um registro do que foi feito no programa desde que a utilização dele foi iniciada. Ele vai imprimindo em arquivos textos, às vezes até separando um arquivo texto por dia, o que acontece com os dados manipulados pelo programa, permitindo assim um controle desses dados.

fputc

Com essa função podemos escrever apenas um caractere no arquivo. Seu protótipo é:

int fputc ( int character, FILE * stream );

Questão: Analise a seguinte afirmação, dizendo se a mesma é verdadeira ou falsa

Podemos escrever praticamente qualquer tipo de dado em arquivos;

Verdadeiro

Lição 6 - Recursividade

Recursividade

Provavelmente essa funcionalidade tenha sido incorporada às linguagens de programação por ser bastante comum na matemática. Inicialmente a idéia pode causar um pouco de confusão, mas assim que entendermos do que se trata vamos começar a criar funções recursivas realmente belas. Trata-se de uma forma de resolver problemas em que os problemas tem algo em comum. Juntos, formam uma natureza de problema, um tipo de problema. Normalmente problemas dessa natureza ficam realmente simples se encarados recursivamente, enquanto que tendem a se tornar bastante complicados se encarados como problemas não recursivos. Por isso é de extrema importância que saibamos como classificar um problema como sendo um problema recursivo ou não. Primeiro vamos entender o que é a recursividade.

A recursividade é basicamente uma metodologia. Nessa metodologia, temos um problema central que se reutiliza um número qualquer de vezes, até que atinge um ponto de parada. Nesse ponto, a recursividade faz o caminho inverso, voltando e voltando até que chegue na solução final. Um pouco confuso, né? Essa é apenas uma forma um pouco mais formal de ver a recursividade. Vamos analisar bem um exemplo:

De acordo com a matemática, para calcular o fatorial de um número, multiplicamos o fatorial do número anterior à esse (n-1) pelo próprio número. Assim:

Fatorial do número n : n!

5! = 5 * 4!

Opa, mas o quatro fatorial, 4!, pode ser definido como:

4 * 3!

Hum, nesse caso, chegamos à seguinte conclusão:

5! = 4 * 3 * 2 * 1!

Poderíamos continuar descendo os números inteiros? Não, e essa é a parte mais importante da recursividade, o critério de parada. Paramos no 1! porque sabemos que ele sempre vale 1. Percebeu a natureza recursiva do problema? Vamos transformar a resolução desse problema num pequeno algoritmo:

passo 1: quero o valor de 5!passo 2: o valor de 5! é 5 * 4!passo 3: quero o valor de 4!passo 4: o valor de 4! é 4 * 3!passo 5: quero o valor de 3!passo 6: o valor de 3! é 3 * 2!passo 7: quero o valor de 2!passo 8: o valor de 2! é 2 * 1!passo 9: quero o valor de 1!passso 10: o valor de 1! é 1passo 11: o valor de 2! é 2 * 1 = 2passo 12: o valor de 3! é 3 * 2 = 6passo 13: o valor de 4! é 4 * 6 = 24passo 14: o valor de 5! é 5 * 24 = 120

E aí terminamos a recursão. Percebeu que fazemos o caminho de ida na recursão e depois o de volta, devolvendo os valores para às diferentes instâncias de resolução do problema até que façamos a última devolução e assim tenhamos o resultado em mãos? Essa é a natureza da recursão, um conjunto de características presente em todos os problemas que seguem essa natureza. O que precisamos agora é só aprender a transferir esses pensamentos para as funções do C. Como implementar uma função recursiva em C? Vamos fazer a do fatorial?

Bem, sabemos que para calcular o fatorial de um número devemos fazer:

n! = n * (n-1)!

Page 33: Linguagem de Programação C

Vamos criar a função:

int calculaFatorial (int intEntrada) {

return(intEntrada * calculaFatorial(intEntrada-1));

}

Percebeu que dentro da execução da função, ela chama a ela mesma? Vamos ver agora que alguns problemas de natureza recursiva, como esse, podem ser revolvidos sem recursivdade. Nesse caso, temos um pró e um contra. Como já vimos, chamadas à funções gastam processamento e memória, já que o corpo da função tem que ser copiado para a memória e executado lá, além de as variáveis envolvidas serem copiadas (se tiverem sido passadas por parâmetros). Porém, às vezes um problema de natureza recursiva pode ficar bem mais complicado se abordado sem recursivdade. Cabe ao programador analisar essas duas situações. Vamos ver como resolver o problema do fatorial sem recursivdade:

int calculaFatorial (int intEntrada) {

int intSaida = 1;

int i;

for (i=2; i<=intEntrada; i++)

intSaida *= i;

return(intSaida);

}

Vamos ver mais um exemplo da matemática:

Seqüência de fibonacci

A seqüência de fibonacci começa da seguinte forma: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946... Você consegue perceber um padrão nessa seqüência? Tente analisar um elemento com base nos anteriores - esse é o primeiro passo na tentativa de resolver um problema de forma recursiva: tentar voltar aos elementos anteriores para descobrir o valor do atual, sem se esquecer do critério de parada. Podemos perceber que qualquer elemento na verdade é a soma dos dois anteriores. 0+1=1. 1+1=2 e 2+3=5. Aí está a nossa natureza recursiva. Qual o nosso critério de parada? Bem, podemos dizer arbritariamente que o primeiro elemento é zero e que o segundo elemento é um. Vamos criar uma função:

int calculaFibonacci (int intEntrada) {

/* Critério de parada. Não dependem de nenhum elemento anterior */

if (intEntrada == 0) return(0);

else if (intEntrada == 1) return(1);

/* Recursão

else return(calculaFibonacci(intEntrada-2) + calculaFibonacci(intEntrada-1));

}

Questão: Julgue a seguinte afirmação:

Qualquer problema pode ser resolvido sob uma abordagem recursiva assim como qualquer problema de natureza recursiva pode ser resolvido sem recursividade.

Falso

Lição 7 - Manipulação de ponteiros e tipos avançados de dados

Page 34: Linguagem de Programação C

As structs

As structs formam uma parte integrante do C bastante utilizada pelos programadores em geral. Muitas vezes, os programadores se deparam com problemas para os quais a solução utilizando os tipos de variáveis do C é muito complicada, gera código ilegível e conseqüentemente custoso de manter. Por isso existem as structs - são estruturas que o programador pode criar livremente com o objetivo de incrementar o espaço de programação. Com o uso de structs, o programador pode agrupar dados que possuem uma forte ligação de significado, criando um tipo novo de variável composto por uma ou mais variáveis, inclusive de tipos diferentes. Vamos ver um exemplo:

Imagine que você esteja codificando uma função e que essa função precisa de um conjunto de cinco dados. Não é tão difícil imaginar uma função que necessite dessa quantidade de dados - um exemplo é uma função que calcula a média de um aluno com base nas notas de cinco provas. Temos várias formas de resolver esse problema: primeiro, podemos fazer uma função que recebe um vetor com quantidade indefinida de elementos. Ou então podemos passar como parâmetro para a função o endereço de memória para o primeiro elemento do vetor de notas, e uma variável dizendo o número de provas às quais o aluno foi submetido - essa é uma boa idéia, mas vamos pensar em mandar as cinco notas separadamente. O protótipo da função seria o seguinte:

float calculaMedia (float floatNota1, float floatNota2, float floatNota3, floatNota4, floatNota5);

Se você não se importar com a quantidade de parâmetros, tudo bem, pode fazer a função assim. Mas esse não é um código de qualidade, não é legível e conseqüentemente é difícil de manter. Por isso, vamos criar uma estrutura composta por cinco variáveis do tipo inteiro, assim poderemos passar apenas ela como parâmetro:

struct notas {

float floatNota1;float floatNota2;float floatNota3;float floatNota4;float floatNota5;

};

Perceba que há um ';' logo após o final do bloco de código. Com a estrutura criada, podemos, dentro do programa, criar uma variável do tipo dela, simplesmente fazendo:

struct notas varNotas;

E podemos criar a função com o seguinte protótipo:

float calculaMedia (struct notas varNotas);

Vamos ver um exemplo completo:

#include <stdio.h>

struct notas {float floatNota1;float floatNota2;float floatNota3;float floatNota4;float floatNota5;};

float calculaMedia (struct notas varNotas) {

return((varNotas.floatNota1 + varNotas.floatNota2 + varNotas.floatNota3 + varNotas.floatNota4 + varNotas.floatNota5)/5);

}

int main() {

struct notas varNotas;float floatMedia;

printf("Digite a nota da prova 1: "); scanf("%f",&(varNotas.floatNota1));printf("Digite a nota da prova 2: "); scanf("%f",&(varNotas.floatNota2));printf("Digite a nota da prova 3: "); scanf("%f",&(varNotas.floatNota3));printf("Digite a nota da prova 4: "); scanf("%f",&(varNotas.floatNota4));printf("Digite a nota da prova 5: "); scanf("%f",&(varNotas.floatNota5));

floatMedia = calculaMedia(varNotas);printf("A media do aluno eh: %.2f\n", floatMedia);

return(0);

Page 35: Linguagem de Programação C

}

É importante percebermos que é apenas um exemplo do uso de structs - não deve ser visto com um exemplo de um programa bem escrito. Sempre que estiver programando se preocupe em escrever código legível, mantenível (de fácil manutenção) e acima de tudo eficiente. No caso de estarmos codificando um programa seguindo esses objetivos, nunca utilizaríamos cinco execuções seguidas da função scanf para ler as notas - utilizaríamos um vetor e leríamos da seguinte forma:

float vtrNotas[5];int i;

for (i=0 ; i<5 ; i++) {

printf("Digite a nota da %da prova: ", i+1);scanf("%f",&ampvtrNotas[i]);

}

Por isso esse não é um bom exemplo de um código bem escrito, mas serve como exemplo de um código que utiliza structs. Um outro bom exemplo de programa que utiliza structs é um programa responsável por manter uma agenda de endereços. Se não for a idéia fazer com que os registros sejam dinâmicos, ou seja, que o conteúdo de cada registro não varie, podemos utilizar uma struct parecida com a seguinte:

struct registro {char nome[20];char sobrenome[20];char telFixo[11];char telMovel[11];char endPessoal[40];char endProfissional[40];char email[30];};

Vetores e structs

Uma forma interessante de se trabalhar com as structs é utilizar vetores dentro delas, como uma forma de armazenar dados. O método é bem intuitivo - vamos criar uma struct:

struct aluno {char nome[40];float notas[5];float trabalhos[2];float conceito;};

Criando a struct dessa forma a leitura das nota do aluno seria feita com um código bem mais elegante, assim:

struct aluno varAluno;

for (i=0 ; i<5 ; i++) {

printf("Digite a %da nota do aluno: ", i+1);scanf("%f",&ampvarAluno.notas[i]);

}

Além disso, podemos criar um vetor de structs! Seria como por na memória uma stuct dessa depois da outra, contíguamente na memória, assim:

struct aluno varAluno[10];

E poderíamos acessar a nota do aluno de índice cinco da seguinte forma:

varNotas[5].notas[i]

que quer dizer, a i-ésima nota do aluno de índice cinco.

Ponteiros

Uma forma bastante elegante de se manipular as estruturas compostas é com a utilização de ponteiros. Anteriormente nós comentamos que a maior desavantagem da programação orientada às funções é o fato de todas as variáveis que serão utilizadas pela função serem copiadas para o espaço de execução da função, gerando um custo de tempo e memória nessa cópia. Se estivermos trabalhando com estruturas compostas extensas, esse efeito é mais prejudicial ainda. Por isso, se pudermos passar para as funções a área da memória em que a estrutura se encontra, ganharemos tempo e espaço na memória. Podemos fazer isso da seguinte forma:

Primeiro criamos a estrutura - afinal, ela tem que estar na memória propriamente dita:

Page 36: Linguagem de Programação C

struct aluno varAluno;

Depois, criamos a função que, ao invés de receber a estrutura, recebe o endereço dela:

float calculaMedia (struct aluno* varAluno);

Com a função criada dessa forma, podemos passar o endereço da estrutura como parâmetro, assim:

floatMedia = calculaMedia(&ampvarAluno);

Agora só precisamos nos preocupar com mais uma coisa: estamos trabalhando com endereço de memória e não o elemento em si. Por isso, precisamos manipular os elementos da estrutura de uma forma um pouco diferente. Dentro da função de calcular a média, acessaríamos as notas da seguinte forma:

for (i=0 ; i<5 ; i++) {

floatMedia += varAluno->notas[i];

}

Isto porque varAluno não é uma estrutura e sim um ponteiro para uma estrutura. Para fixar, vamos ver uma estrutura com mais um nível e como manipular os seus dados:

struct endereco {char cep[10];char rua[10];char bairro[10];};

struct aluno {char nome[40];float notas[5];float trabalhos[2];float conceito;struct endereco;

};

Como você pode ver, temos uma struct dento de uma struct. Isso não é um problema, e na verdade, pode ser uma boa solução. Se estivermos trabalhando com a estrutura em si, podemos manipular o cep da seguinte forma:

varAluno.endereco.cep

No entanto, se estivermos trabalhando com um ponteiro para a estrutura, faríamos assim:

varAluno->endereco.cep

Isto porque varAluno é um ponteiro, mas endereco é uma estrutura. Então utilizamos o operador '->' para o ponteiro varAluno e o operador '.' para a estrutura endereço.

Questão: Julgue a seguinte afirmação:

As structs formam uma área importante do C e de outras linguagens de programação e, por isso, todas as variáveis do programa sendo feito devem ser armazenadas em structs.

FalsoCorreto. Embora as estruturas compostas desempenhem um papel bastante importante dentro de qualquer linguagem de programação algumas variávels, especialmente os contadores e outras variáveis de controle, precisam ser manipuladas individualmente dentro de cada bloco do programa, já que elas são locais àquele bloco.

Alocação dinâmica

Hoje vamos falar sobre memória, e como manipular a memória que o sistema operacional permite que seja manipulada por nós.

Em C, assim como em outras linguagens, podemos armazenar as variáveis utilizadas pelo programa de duas formas distintas. A primeira consiste em declarar as variáveis estaticamente no início do bloco de código do programa, e a segunda em pedir durante a execução do programa memória ao sistema operacional, de uma forma chamada de dinâmica. Além de as variáveis criadas dessas duas formas não serem armazenadas em locais semelhantes na memória (as variáveis estáticas estão incorporadas ao código do programa, que tem um tamanho fixo), a grande diferença dessas duas abordagens está justamente no

Page 37: Linguagem de Programação C

poder que as variáveis dinâmicas dão ao programador. Muitas vezes, o programador não sabe exatamente quanto de memória precisa, por estar manipulando uma estrutura de tamanho variável (vamos aprender a criar algumas) ou então por estar trabalhando com uma área que pode exigir cada vez mais elementos ou registros. É justamente nesses casos que a alocação dinâmica confere grande poder e versatilidade ao programador, que tem como controlar cada byte utilizado pelo seu programa (além daqueles que ele não controla por estarem incorporados ao corpo do programa). Vamos aprender a utilizar as duas principais funções envolvidas:

malloc

Com essa função pedimos memória ao sistema operacional. Passamos para ele a quantidade de memória que queremos, em bytes, e é retornado para nós um ponteiro para a área de memória reservada. Podemos então fazer o que quisermos com essa área de memória - ler e escrever, contanto que tenhamos o cuidado de não estrapolar o limite dessa área de memória reservada para nós. Vamos ver o protótipo da função:

void * malloc ( size_t size );

O único detalhe para o qual temos que atentar é que a função vai retornar um ponteiro do tipo void, ou seja, um ponteiro sem tipo definido o qual não podemos manipular normalmente. Por esse motivo, temos sempre que utilizar a técnica de cast que aprendemos anteriormente, dizendo ao compilador de que tipo queremos que o ponteiro seja. Vamos ver um exemplo de utilização da função malloc:

Primeiro criamos a variável ponteiro, do tipo desejado, lembrando sempre de criar o ponteiro de tal forma que ele não aponte para lugar algum da memória:

int* ptrInteiro = NULL;

Depois associamos à esse ponteiro um endereço da memória, utilizando a função malloc:

ptrInteiro = (int *) malloc(sizeof(int));

Dessa forma, dizemos ao compilador que ele deve converter o ponteiro criado pela função malloc para o tipo inteiro. Poderemos, então, manipular essa variável como sendo uma variável inteiro normal, assim como faríamos se tivéssemos declarado a variável no início do programa. Podemos fazer por exemplo:

*ptrInteiro = 30;

free

Podemos dizer que a prática mais importante, ao manipular ponteiros, é tomar todo o cuidado necessário para que a rotina que você implementou não extaprole os limites da sua estrutura, lendo ou escrevendo em áreas da memória que não foram designadas ao seu programa. Já ao manipular estruturas alocadas dinâmicamente na memória, a prática mais importante a ser seguida é a de tomar todo o cuidado possível para que nenhum ponteiro se perca, gerando um grave problema comumente chamado de vazamento de memória. Vamos ver um exemplo:

int* ptrInteiro = (int *) malloc(sizeof(int));

*ptrInteiro = 20;

Temos um ponteiro que aponta para uma área da memória na qual temos como conteúdo o inteiro 20. Se fizermos o seguinte:

ptrInteiro = NULL;

Vamos fazer com que o ponteiro deixe de apontar para aquela área da memória e passe a apontar para lugar algum. Temos como conseqüência a perda do endereço de memória contendo o inteiro 20 - isto é, não temos mais como ler ou alterar aquele conteúdo, ou o que é pior, não podemos mais liberar aquele pedaço de memória para o sistema operacional. Eventualmente, após a finalização do nosso programa, o sistema operacional vai perceber o vazamento e vai liberar aquela porção de memória, mas até lá, aquela área de memória estará indisponível mesmo não sendo mais utilizada pelo seu programa. Por esse motivo é de extrema importância que sempre que uma área da memória for alocada dinâmicamente, ela seja, após a manipulação, liberada para o sistema operacional. Para tal operação utilizamos a função free. Vamos ver um exemplo:

int* ptrInteiro = (int *) malloc(sizeof(int));

*ptrInteiro = 20;

/* Vamos agora liberar a memória e fazer com que o ponteiro não aponte mais para aquela área */free(ptrInteiro);ptrInteiro = NULL;

Assim além de liberarmos a memória para o sistema operacional garantimos que aquela área de memória não seja indevidamente alterada pelo nosso programa, garantindo que não possamos mais acessá-la.

Vetores e matrizes

Vamos agora falar brevemente sobre como montar vetores e matrizes dinamicamente na memória. Primeiro, precisamos pedir para o sistema operacional memória suficiente para armazenar os dados que queremos. Podemos fazer isso da seguinte forma:

int* vtrInteiros = (int *) malloc(sizeof(int)*TAMANHO_VETOR);

Assim pedimos ao sistema operacional espaço para armazenar TAMANHO_VETOR elementos do tipo inteiro, lembrando que o

Page 38: Linguagem de Programação C

espaço para esses dados é sempre contíguo na memória, o que nos permite manipulá-lo da mesma forma que fazemos com os vetores alocados estaticamente. Podemos fazer assim :

vtrInteiros[i] = 20;

Isto porque vtrInteiros é um ponteiro, mas vtrInteiros[i] não, é uma variável do tipo inteiro. Podemos também acessar os dados assim:

*(vtrInteiros+i) = 20;

Esse comando diz que queremos que o espaço de memória i*sizeof(int) bytes após o endereço apontado por vtrInteiros receba o valor 20. Isto funciona porque, como já mencionamos, o compilador sabe que o ponteiro vtrInteiros é do tipo inteiro, e, por isto, quando adicionamos i ao endereço de memória apontado por vtrInteiros, o compilador desloca na verdade i*sizeof(int) bytes ao invés de simplesmente i.

Já para montar uma matriz de duas dimensões (linhas e colunas) dinamicamente precisamos fazer um sistema de controle um pouco mais incrementado, já que nesse caso o compilador não vai aceitar algo como mtrInteiros[i][j] por não achar que se trata de uma matriz, e sim de apenas uma estrutura contígua na memória. Para pedir a memória, temos primeiro que pensar quanto de espaço precisamos, e essa operação é simples: basta que multipliquemos o número de linhas pelo número de colunas que queremos alocar. Por exemplo, se quisermos alocar uma matriz de 3 linhas e 3 colunas, basta que o sistema operacional nos dê espaço para armazenar 9 elementos. Por isto, podemos fazer assim:

int* mtrInteiros = (int *) malloc(sizeof(int)*9);

Agora, vamos ver como controlar a nossa matriz. Podemos fazer isso no corpo do programa ou utilizando uma macro, que é uma forma um pouco mais elegante de resolver o problema. Vamos começar vendo como fazer isso no próprio programa.

Se pudéssemos ver a nossa matriz na memória, veríamos um espaço contíguo mais ou menos assim:

[0][0] [0][1] [0][2] [1][0] [1][1] [1][2] [2][0] [2][1] [2][2]

Isto quer dizer que se quisermos acessar o elemento [1][1], o elemento central na segunda coluna da segunda linha, estaremos na verdade acessando o elemento [4], que é o quinto elemento na estrutura contígua que temos na memória. É com esse pensamento que vamos transformar a nossa matriz em um vetor e vice-versa, com a seguinte fórmula:

índice = linha * número de elementos por linha + coluna

Assim, 1 + 3 * 1 = 4, que é o elemento que estávamos procurando. No corpo da função poderíamos manipular toda a matriz da seguinte forma:

for (i=0 ; i<NUM_LINHAS; i++) {

for (j=0 ; j<NUM_COLUNAS. j++) {

mtrInteiros[i*LARGURA_LINHA+j] = i*j;

}

}

Este é apenas um exemplo de manipulação, que ilustra como o nosso vetor dinâmico pode ser transformado em uma matriz. Podemos criar uma macro que faça a mesma coisa da seguinte forma:

#define I(i,j,LARGURA_LINHA) (i*LARGURA_LINHA+j)

Como já comentamos anteriormente, a macro pode ser utilizada pelo programador como se fosse uma função, mas difere das funções no sentido de que ela é avaliada em tempo de compilação, momento em que o pré-processador substitui todas as ocorrências da macro no corpo do programa pelo o que ela têm como argumento no define. Assim poderíamos fazer a mesma manipulação da seguinte forma:

for (i=0 ; i<NUM_LINHAS; i++) {for (j=0 ; j<NUM_COLUNAS. j++) {

mtrInteiros[I(i,j,LARGURA_LINHA)] = i*j;

}

}

Questão: Julgue a seguinte afirmação.

Page 39: Linguagem de Programação C

Depois que utilizamos a área da memória que pedimos ao sistema operacional não precisamos mais nos preocupar com essa área de memória - o próprio sistema operacional vai se preocupar em liberar essa memória.

Correto. O programador deve sempre se preocupar com a manipulação da memória alocada dinâmicamente e após a manipulação com a correta liberação daquela área de memória.

Abstração de dados

Esta seção aborda temas de estruturas de dados. Assuntos como listas, filas, pilhas, árvores e grafos serão tratados aqui. Lição 8 - Estruturas de dados, parte 1

Pilhas

Vamos começar a falar essa semana sobre uma parte bastante importante de qualquer linguagem de programação, as estruturas de dados. Talvez você tenha percebido que embora tenhamos, à nossa disposição na linguagem C, uma considerável quantidade de tipos de dados. No entanto, ainda há determinados tipos de aplicações e problemas para os quais o programador precisa de estruturas um pouco mais complexas e também dinâmicas, se adaptando às necessidades da aplicação conforme ela vai evoluindo. É por esses motivos que utilizamos as estruturas de dados, realizando operações que não conseguiríamos sem as mesmas.

Vamos começar o nosso estudo de estrutura de dados com as pilhas, que são a estrutura de dados mais simples de entender e implementar. Vamos trabalhar também, sempre que possível, com alocação dinâmica, proporcionando ao aluno um bom treinamento de manipulação de ponteiros.

Há diversos exemplos em que a utilização das pilhas é ótima. Na verdade, essa acaba se tornando a parte mais importante de um projeto que pretende utilizar estruturas de dados: a escolha das estruturas de dados que serão utilizadas. Isso parece ser verdade porque ao mesmo tempo que uma determinada estrutura de dados se encaixa perfeitamente no problema envolvido, facilitando a implementação e aumentando a eficiência da aplicação, uma outra estrutura de dados tende a dificultar em muitos níveis a implementação e posteriormente torna a aplicação ineficiente. Aos poucos vamos perceber essa questão e vamos aprender a escolher a estrutura de dados a ser utilizada - é apenas uma questão de prática.

Um dos exemplos mais práticos, na área da computação, em que a utilização das pilhas é ideal, é a transformação de números da base decimal para a base binária. Isso se dá pelo fato de que em algum momento dessa transformação temos que inverter a ordem de cálculo, e acredito que seja nesse ponto que a utilização da pilha é ideal. A principal característica da pilha pode ser descrita a partir da frase: "o primeiro elemento que entra é o último que sai". Ter essa frase em mente vai facilitar bastante a compreensão dos problemas que pedem a implementação das pilhas. Vamos então ver brevemente como transformar um número de uma base para outra:

Se tivermos que transformar o número 112 da base decimal para a base binária - começamos dividindo o número por dois com o objetivo de preencher as casas binárias do número.

112 / 2 = 56 e resto 0. Logo, temos 0

0

56 / 2 = 28 e resto 0. Temos 0

00

28 / 2 = 14 e resto 0. Temos 0

000

14 / 2 = 7 e resto 0. Temos 0

0000

7 / 2 = 3 e resto 1. Temos 1

00001

3 / 2 = 1 e resto 1. Temos 1

000011

1 / 2 = 0 e resto 1. Temos 1

0000111

Certo, aparentemente terminamos a rotina de transformação. No entanto, se calcularmos o número decimal a partir desse número binário que encontramos, não vamos encontrar 112:

1 * 2^0 + 1 * 2^1 + 1 * 2^2 + 0 * 2^3 + 0 * 2^4 + 0 * 2^5 + 0 * 2^6 = 7

Page 40: Linguagem de Programação C

Por isso precisamos inverter o número binário e fazer isso com uma pilha se torna bastante simples e eficiente. Basta que ao calcular o primeiro elemento da estrutura, ao invés de simplesmente o inserir em um vetor, ele seja inserido no fundo de uma pilha, tornando-se o seu primeiro elemento. Ao calcular o segundo elemento, o inserimos logo acima do primeiro e assim por diante até que, ao final da rotina, temos no topo da pilha o último elemento calculado. Então, só precisamos ir retirando os elementos da pilha e inserindo em um vetor comum. Não precisamos nos preocupar mais com a ordem e nem com o tamanho desse vetor, já que ele é dado pelo tamanho da pilha quando esta estava cheia. O único cuidado que devemos tomar é setar um tamanho máximo para a pilha tal que não falte espaço para a rotina de transformação.

Inicialmente, vamos aprender como criar a pilha. Como vamos precisar das funções de alocação dinâmica free e malloc, vamos ter que incluir o cabeçalho stdlib, responsável por manter essas e outras funções. Então podemos criar a estrutura que controla a pilha e depois manipulá-la. Vamos começar:

/* Cabeçalhos incluídos */#include <stdlib.h>#include <stdio.h>

/* Constantes */#define TOPO_VAZIO -1#define TAMANHO_PILHA 20

/* Estrutura básica da pilha */struct pilha {

int topo;int itens[TAMANHO_PILHA];};

/* Função que inicializa a pilha na memória */struct pilha* criaPilha() {

struct pilha* ptrPilha = (struct pilha*) malloc(sizeof(struct pilha));ptrPilha->topo = TOPO_VAZIO;return(ptrPilha);

}

Pronto. Com isso já podemos criar a pilha na memória. Agora precisamos aprender a inserir e retirar elementos do topo da pilha. Aqui na lição vamos ver apenas a função de inserir elementos, fica por conta do aluno criar a função de retirar elementos.

/* Função que insere um elemento na pilha */void adicionaElemento (int elemento , struct pilha* ptrPilha) {ptrPilha->itens[++ptrPilha->topo] = elemento;}

E vamos ver também a função de converter um número decimal em binário e a função que mostra o arquivo binário posteriormente:

int converteBinario (int valor, struct pilha* ptrPilha) {

if ((valor) && (!pilhaCheia(ptrPilha))) {

adicionaElemento((valor%2), ptrPilha);converteBinario(valor/2, ptrPilha);

}

else if ((!valor) && (pilhaVazia(ptrPilha))) adicionaElemento(0, ptrPilha);

else if (pilhaCheia(ptrPilha)) {

printf("Pilha cheia!\n");exit(1);

}

return(0);

}

void mostraBinario (struct pilha* ptrPilha) {

while(!pilhaVazia(ptrPilha)) {

printf("%d",ptrPilha->itens[ptrPilha->topo]);

Page 41: Linguagem de Programação C

removeElemento(ptrPilha);

}}

Vamos ver também a função que controla se a pilha está cheia ou não, dada a constante de tamanho máximo da pilha. Assim como a função de retirar elementos da pilha, a função que checa se a pilha não está vazia fica por conta do aluno.

/* Função que checa se a pilha já não está cheia */int pilhaCheia (struct pilha* p_Pilha) {

if (p_Pilha->topo == TAMANHO_PILHA-1) return (1);else return(0);

}

Com essas funções mais as que ficaram por conta do aluno já podemos criar um programa que converte números decimais em números binários, utiliando as pilhas como estruturas de dados. Pratique o que aprendeu e se aparecer qualquer dúvida, o tutor estará pronto para retirá-las.

Filas encadeadas

Prosseguindo o nosso estudo de estrutura de dados vamos aprender sobre o que tende a ser a estrutura de dados mais utilizada na computação. Ela segue um pensamento humano muito comum, que nós utilizamos bastante no nosso dia-a-dia. Enquanto que nas pilhas o primeiro elemento que entra é o último que sai, nas listas encadeadas o primeiro elemento que entra é o primeiro que sai, fazendo com que elas se comportem como filas.

A primeira diferença importante entre as pilhas e todas as outras estruturas de dados é que na pilha todos os dados são guardados em um vetor, controlado pelas rotinas da pilha, enquanto que nas outras estruturas de dados, com o objetivo de torná-las ainda mais dinâmicas, os dados ficam espalhados pela memória, referenciados apenas por ponteiros que vão interligando cada elemento da estrutura, de tal forma que nenhum se perca (vazamento de memória).

Sabendo disso, vamos planejar a implementação de um programa que controle a fila em um caixa de banco. Assim que o cliente entra na agência, ele vai até um terminal, digita o seu nome e retira uma senha (seqüencial). Então quando for a vez de ser atendido, ele será chamado pelo número da senha e pelo nome. Para implementar esse programa precisamos de uma lista ligada, precisamos inserir elementos no final da lista e retirar elementos do início, intuitivamente como é feito na agência. Vamos primeiro criar a estrutura básica da lista, lembrando que agora nós precisamos de duas estruturas diferentes: uma para a lista e outra para os elementos da lista.

#include <stdlib.h>#include <stdio.h>

#define LISTA_VAZIA 0#define TAMANHO_NOME 30

struct elemento {

char* nome;int senha;struct elemento* proximo;};

struct lista {struct elemento* primeiro;struct elemento* ultimo;int atual;};

Como podemos ver, precisamos de um instrumento de controle que nos permita não perder nenhum dos elementos na memória, evitando o vazamento de memória. Esse instrumento são os ponteiros anterior e proximo na estrutura elemento. Com eles vamos sempre saber onde estão os elementos na memória. Vamos agora implementar a função que cria a lista:

struct lista* criaLista() {

struct lista* ptrLista = (struct lista *) malloc(sizeof(struct lista));ptrLista->ultimo = ptrLista->primeiro = NULL;ptrLista->atual = LISTA_VAZIA;return(ptrLista);}

Ok. Criada a lista, podemos criar a função de inserir elementos. Ela será mostrada aqui, porém a função de retirar elementos, seguindo o que foi feito com as pilhas, ficará por conta do aluno. Vamos ver que agora a função de inserir elementos é um pouco mais complicada, tendo que lidar com mais de um caso. Vamos também precisar de uma função que cria um elemento antes de

Page 42: Linguagem de Programação C

poder inserí-lo, ou seja, como estamos trabalhando integralmente com alocação dinâmica precisamos alocar o espaço necessário para um elemento da lista antes de armazenar os dados.

Note também que na estrutura elemento o nome do cliente é uma variável ponteiro de char, ao invés de um simples vetor de char. Isso implica que, quando usarmos a função malloc para alocar o espaço para um elemento da lista, vamos receber espaço para armazenar um ponteiro de char, um inteiro e um ponteiro para outra estrutura elemento, ou seja, não temos inicialmente espaço para armazenar o nome do cliente - isso deve ser feito manualmente, como vamos ver:

struct elemento* criaElemento() {

struct elemento* ptrElemento = (struct elemento *) malloc(sizeof(struct elemento));ptrElemento->nome = (char *) malloc(sizeof(char)*TAMANHO_NOME);ptrElemento->senha = LISTA_VAZIA;ptrElemento->proximo = NULL;return(ptrElemento);

}

void insereElemento (struct elemento* ptrElemento, struct lista* ptrLista) {

/* Se estivermos inserindo o primeiro elemento */

if (!ptrLista->primeiro) {

ptrLista->primeiro = ptrElemento; ptrLista->ultimo = ptrElemento;

/* Senao */else {

ptrLista->ultimo->proximo = ptrElemento;ptrLista->ultimo = ptrElemento;

}

ptrElemento->senha = ++ptrLista->atual;

}

Lembrando que precisamos criar uma função que retire da lista apenas o primeiro elemento (o que está na frente), a não ser que a agência em questão exija que um cliente possa desistir do atendimento.

Além de retirar um elemento, precisamos saber o que fazer com a estrutura que está montada na memória quando não tivermos mais clientes na lista e quisermos finalizá-la. Afinal, a estrutura básica da lista, por si só, já ocupa espaço na memória como vimos com a função de criar a lista. Além disso, apenas retirar um elemento da lista não basta - precisamos liberar a memória utilizada por ele. Para tal, precisamos nos recordar que um dos componentes da estrutura que comanda cada elemento é um ponteiro para char que nós utilizamos para armazenar o nome do cliente. Sendo assim, se liberarmos a memória utilizada pelo ponteiro da estrutura do elemento vamos liberar o ponteiro próximo (sem liberar a memória utilizada pelo próximo cliente), o inteiro senha e o ponteiro nome (sem liberar o espaço ocupado pela string do nome). Por esse motivo, precisamos primeiro liberar a memória utilizada pela string para só então liberar a memória utilizada pela estrutura do elemento. Podemos fazer isso assim:

void destroiElemento (struct elemento* ptrElemento) {

free(ptrElemento->nome);free(ptrElemento);

}

Já para destruir o esqueleto da lista após termos certeza de que não há mais nenhum cliente incluído, podemos fazer assim:

int destroiLista (struct lista* ptrLista) {

/* Checando se ainda há elementos na lista */if (!ptrLista->primeiro) { free(ptrLista);return(0);}

/* Não podemos destruir a lista ainda */else return(1);

}

Lembrando de sempre utilizar essas duas funções ao retirar elementos da estrutura e ao finalizar a estrutura faz com que o

Page 43: Linguagem de Programação C

nosso programa utilize a memória fornecida pelo sistema operacional de forma consciente. Por enquanto, estamos trabalhando com pequenos pedaços da memória de 10 a 100 kb no máximo. Imagine uma lista de grandes proporções. Um vazamento crônico de memória pode resultar no travamento do sistema rodando o programa que implementa a lista.

Questão: Julgue a seguinte afirmação:

Quando utilizamos a função free para liberar a memória utiliza por um elemento da lista, liberamos toda a memória utilizada por aquele elemento, incluído a memória apontada por ponteiros que façam parte da estrutura, como um ponteiro para char que faz parte do elemento da lista.

Falso

Correto. Se temos uma estrutura básica e dento dessa estrutura básica temos variáveis estáticas e variáveis ponteiro, quando utilizamos a função free as variáveis estáticas têm a sua memória liberada, assim como as variáveis ponteiro, mas não as áreas de memória apontadas pelas variáveis ponteiro - por isso precisamos liberar essas áreas de memória antes de destruir os ponteiros

Filas duplamente encadeadas

O que vamos aprender agora é apenas uma extensão do assunto anterior. Vimos como criar, manipular e remover uma fila encadeada, em que para realizar as operações é necessário apenas que cada elemento conheça o endereço do próximo, além da estrutura principal conhecer o primeiro e o último elemento. A limitação que essa forma de estruturar a fila gera é que não podemos remover nenhum elemento além do primeiro. Dado um ponteiro cuja memória apontada faça parte da fila, conhecemos apenas o elemento seguinte, mas não o seu antecessor. Por esse motivo não podemos removê-lo da fila, já que não teríamos como fazer com que o seu antecessor apontasse agora para o seu sucessor. Sendo assim vamos abrir mão de um pouco mais de espaço em memória para agora mantermos um controle de ida e volta na estrutura. Além da estrutura base da fila conhecer o primeiro e o último elemento, cada elemento vai conhecer o seu sucessor e antecessor. Dessa forma temos como fazer qualquer tipo de manipulação com a fila, inclusive uma eventual ordenação. Vamos começar com as bibliotecas e as estruturas, lembrando que implementaremos utilizando a fila duplamente encadeada, o mesmo exemplo da fila encadeada - assim poderemos comparar as duas formas.

#include <stdio.h>#include <stdlib.h>

#define TAMANHO_NOME 30#define FILA_VAZIA -1

struct elemento {

char* nome;int senha;struct elemento* proximo;struct elemento* anterior;};

struct fila {struct elemento* primeiro;struct elemento* ultimo;int contador;};

Vamos ver também as funções que criam a estrutura base da fila, criam um elemento que pode ser inserido na fila e insere um elemento na fila. As funções que fazem o oposto ficam como exercício para o aluno:

/* Função que pede o SO memória para armazenar a estrutura base da fila */struct fila* criaFila() {

struct fila* ptrFila = (struct fila *) malloc(sizeof(struct fila));

ptrFila->primeiro = ptrFila->ultimo = NULL;ptrFila->contador=FILA_VAZIA;

return(ptrFila);

}

/* Função que pede ao SO memória para armazenar um elemento da fila */struct elemento* criaElemento() {

struct elemento* ptrElemento = (struct elemento *) malloc(sizeof(struct elemento));

ptrElemento->nome = (char *) malloc(sizeof(char)*TAMANHO_NOME);ptrElemento->anterior = ptrElemento->proximo = NULL;

Page 44: Linguagem de Programação C

return(ptrElemento);

}

/* Função que insere um elemento na fila */int insereElemento (struct elemento* ptrElemento, struct fila* ptrFila, int posicao) {

int i;struct elemento* ptrAux = NULL;

/* Checando se a posicão é valida */if ((posicao > ptrFila->contador+1) || (posicao < 0)) return(-1);

/* Iniciando a insercão */

/* Se for inserir na primeira posicão e já há elementos */else if ((!posicao) && (ptrFila->contador > FILA_VAZIA)) {

ptrElemento->proximo = ptrFila->primeiro;ptrFila->primeiro = ptrElemento;ptrElemento->proximo->anterior = ptrElemento;ptrFila->contador++;ptrElemento->senha = 1;ptrElemento = ptrElemento->proximo;for (; ptrElemento; ptrElemento = ptrElemento->proximo) ptrElemento->senha++;}/* Inserir na primeira posicão mas ainda não há elementos */else if ((!posicao) && (ptrFila->contador == FILA_VAZIA)) {ptrFila->primeiro = ptrFila->ultimo = ptrElemento;ptrFila->contador++;ptrElemento->senha = 1;

}

/* Inserir em uma posicão interna */else if (posicao < ptrFila->contador+1) {ptrAux = ptrFila->primeiro;for (i=0 ; i<posicao-1 ; i++) ptrAux = ptrAux->proximo;ptrElemento->proximo = ptrAux->proximo;ptrAux->proximo = ptrElemento;ptrElemento->anterior = ptrAux;ptrElemento->proximo->anterior = ptrElemento;ptrFila->contador++;ptrElemento->senha = posicao + 1;ptrElemento = ptrElemento->proximo;for (; ptrElemento; ptrElemento = ptrElemento->proximo) ptrElemento->senha++;

}

/* Inserindo na última posicão */else if (posicao == ptrFila->contador+1) {ptrFila->ultimo->proximo = ptrElemento;ptrElemento->anterior = ptrFila->ultimo;ptrFila->ultimo = ptrElemento;ptrFila->contador++;ptrElemento->senha = posicao + 1;

}

/* Ocorreu algum erro */else return(-2);

/* Não ocorrram erros. Finalizando */return(0);

}

Agora basta implementar as funções que fazem as operações contrárias as acima e a função principal do programa, que deve implementar um menu amigável e meios para que a fila seja criada, manipulada e posteriormente destruída.

Questão: Julgue a seguinte afirmativa:

Page 45: Linguagem de Programação C

Utilizando as filas ligadas, podemos fazer as mesmas operações que podemos fazer com as filas duplamente ligadas, com um custo maior de processamento, já que não temos como deslocar o ponteiro auxiliar nos dois sentidos da lista. Por isso, operações como remoção de um elemento anterior ao apontado pelo ponteiro auxiliar e ordenação da fila se tornam mais custosas, já que temos ou que guardar mais ponteiros auxiliares ou que deslocar o ponteiro auxiliar a partir do primeiro elemento seguidas vezes.

Verdadeiro

Filas circulares

Já vimos como implementar as filas encadeadas e as filas duplamente encadeadas. Vimos que, utilizando as filas encadeadas não temos como nos deslocar no sentido de nos aproximarmos do início da fila partindo de um elemento qualquer. Com isso, partindo de um elemento qualquer, não podemos removê-lo ou remover qualquer um dos seus antecessores porque não sabemos quem são esses antecessores. Vamos ver agora como ligar o primeiro elemento da fila duplamente encadeada ao último, formando uma espécie de anel. A vantagem dessa forma de montar a fila é que podemos agora nos deslocar nos dois sentidos da fila e à vontade, sem precisarmos nos preocupar com as bordas - a fila vai crescendo e diminuindo conforme suas necessidades e mantém todas essas características. Vamos começar, lembrando que estamos ainda trabalhando com o problema da fila na agência bancária:

Agora não precisamos mais de uma estrutura que age como cabeçalho da fila, o que a torna mais simples - precisamos manter apenas um ponteiro para um dos elementos, o qual podemos arbitrariamente chamar de primeiro ou último:

#include <stdlib.h>#include <stdio.h>

#define TAMANHO_NOME 20#define FILA_VAZIA -1

struct elemento {

char* nome;int senha;struct elemento* proximo;struct elemento* anterior;};

Vamos ver as funções que geram um elemento e o inserem na fila:

struct elemento* criaElemento() {

struct elemento* ptrElemento = (struct elemento *) malloc(sizeof(struct elemento));ptrElemento->nome = (char *) malloc(sizeof(char)*TAMANHO_NOME);

ptrElemento->proximo = ptrElemento->anterior = NULL;return(ptrElemento);

}

void insereFinal (struct elemento* ptrElemento, struct elemento* ptrUltimo) {

ptrUltimo->proximo->anterior = ptrElemento;ptrElemento->proximo = ptrUltimo->proximo;ptrUltimo->proximo = ptrElemento;ptrElemento->anterior = ptrUltimo;

}

As outras funções de remover e destruir um elemento, imprimir os clientes na fila e destruir a fila ficarão de exercício para o aluno. Lembre-se sempre que os ponteiros são perigosos. Quando estiver manipulando-os, faça um desenho que ilustre a operação sendo feita, pare ver se os ponteiros estão sendo manipulados da forma correta, isto é, se as atribuições estão corretas e se nenhum endereço de memória está sofrendo vazamento.

Questão: Julgue a seguinte afirmação:

A vantagem das filas circulares sobre as filas lineares, ambas duplamente encadeadas, é que com as filas circulares podemos deslocar o ponteiro auxiliar nas duas direções da fila, permitindo a realização de um número maior de operações.

Falso

Correto. Como as duas filas mencionadas são duplamente encadeadas, as duas permitem o deslocamento do ponteiro auxiliar nas duas direções. A vantagem das filas circulares sobre as lineares é que podemos fazer um deslocamento contínuo sem nos preocuparmos com as bordas da fila, além do fato das filas circulares serem mais simples, já que não precisam de uma estrutura atuando como cabeçalho, o que faz com que inserções e remoções sejam também mais simples.

Page 46: Linguagem de Programação C

Lição 9 - Estruturas de dados, parte 2

Árvores

Agora que já falamos sobre as pilhas e filas, vamos falar sobre uma estrutura de dados um pouco mais complicada, mas muito útil em muitas ocasiões.

Como o nome já diz, vamos trabalhar nessa lição com estruturas que se assemelham às árvores que conhecemos com algumas diferenças. A primeira é que vamos precisar de um elemento no topo chamado de raiz da árvore. Esse elemento terá ligado a ele elementos chamados de galhos filhos, sendo que este é chamado pai dos elementos a ele ligados. Então ligados aos seus galhos há outos elementos que são galhos destes, e assim por diante.

O número de galhos ou filhos que um nó pode ter caracteriza a ordem da árvore. Sendo assim, uma árvore em que os elementos podem ter zero, um ou dois filhos caracteriza uma árvore de ordem dois. Árvores assim são tão comuns que têm um nome especial - árvore binária. Aqui trabalharemos com essas árvores, ficando para o aluno a curiosidade de buscar, na literatura, informações adicionais sobre o assunto.

A grande utilidade das árvores está no fato de que, com elas, podemos expressar problemas do dia-a-dia de uma forma extremamente simples, embora a implementação da própria árvore não seja trivial. Nesse caso, a grande vantagem de se programar seguindo padrões é que, feita uma implementação de uma árvore, pode-se reutilizar o código inúmeras vezes para qualquer propósito envolvendo as árvores, com mínima modificação no código.

O exemplo que vamos ver aqui na lição trata de criar, utilizando árvores, um programa que seja capaz de avaliar uma equação simples envolvendo uma variável, dizendo quanto ela vale para diferentes valores dessa variável. Vamos começar entendendo como as árvores podem nos auxiliar nesse problema.

Para exemplificar, vamos escolher uma função simples: f(x) = 3 * x + 4. Além disso, vamos falar mais um pouco sobre precedência, força e aridade de um operador. Precedência e força tratam ambas da ordem com a qual processamos uma determinada expressão. Sendo assim, percebemos que, na linguagem matemática, antiga e já padronizada em todo o mundo, alguns operadores são processados antes de outros, quando aparecem na mesma expressão. Por exemplo: na função do exemplo, qual operador seria avaliado primeiro, o de multiplicação ou o de soma? Nesse caso, vemos que o operador de multiplicação é mais forte e por isso se agrega aos operadores próximos a ele com mais força, por isso avaliamos ele primeiro. Quanto maior a força do operador, mais prioridade ele tem na avaliação da expressão. Por isso, essa expressão é equivalente à expressão ( 3 * x ) + 4. No entanto, se fizermos 3 * (x + 4), utilizamos os parênteses para quebrar a regra de precedência e obrigar quem está lendo a expressão a avaliar primeiro o operador de soma, aplicando-o aos dois operadores próximos a ele, para só então avaliar o restante da expressão. Com relação à aridade, sabemos que trata da quantidade de operadores que um dado operador necessita para ser corretamente avaliado. Podemos usar como exemplo dois operadores: primeiro, o operador “-”, que deve ser aplicado à apenas um outro operador, tornando o negativo. Por isso, é um operador unário. O segundo operador, por exemplo, o de multiplicação é chamado binário porque necessita de dois operadores para ser corretamente avaliado. Com esses conceitos em mente, podemos ver o nosso primeiro exemplo de uma árvore montada:

Page 47: Linguagem de Programação C

Agora que já vimos como uma árvore seria se pudéssemos vê-la depois de montada pelo programa, podemos tentar entender como seria uma avaliação da árvore, ou seja, que algoritmo nos daria de volta a expressão de entrada.

Primeiro temos que perceber que se trata de uma operação recursiva. Como já tratamos antes da natureza recursiva de alguns problemas, deve ficar claro que estamos também tratando de um problema que, se tratado recursivamente, se torna bastante simples. Vamos perceber essa característica.

Para receber o valor de toda a expressão, temos que perguntar ao nó raiz da árvore o seu valor, porque ele representa toda a árvore. Este, então, para conseguir calcular o seu valor, precisa aplicar o operador que ele contém aos seus filhos. Por isso, ele precisa do valor dos dois nós filhos. Cada um dos filhos então têm que calcular o seu valor e devolvê-lo para o nó pai, de forma que o cálculo vai sendo feito de baixo para cima, embora o pedido seja feito de cima para baixo. Essa é a característica recursiva desse cálculo.

Vamos então começar a implementação, partindo da função que cria a estrutura, passando pela função que adiciona elementos para a função que avalia o conteúdo da árvore. O grande problema de implementar um programa assim, não é exatamente implementar e controlar a árvore, e sim manipular os dados da entrada de forma que eles fiquem em um formato com o qual seja possível trabalhar. Isto quer dizer que precisamos ensinar o computador que o caractere x é uma variável e que os números são números, etc. Para isso precisamos criar uma função que se comporta como um analisador léxico, lendo cada caractere e separando-os. Vamos lá?

Percebemos que estamos sempre trabalhando com operadores e é eles que vamos utilizar para separar a entrada em diferentes sub-expressões, até que tenhamos expressões ditas moleculares que podem ser então inseridas em nós da árvore. Por exemplo, a função 3 * x + 4 pode ser separada em duas sub-expressões, 3 * x e 4, tendo como operador responsável por unir essas duas sub-expressões o operador +. Agora, vamos recursivamente aplicar essa rotina às duas sub-expressões. Na sub-expressão 3 * x, podemos ter a sub-expressão 3 e a sub-expressão x, tendo o operador * atuando como conectivo dessas duas sub-expressões. Já na sub-expressão 4 não precisamos fazer nada, pois já se trata de uma expressão molecular. Agora, só precisamos montar a árvore com essas sub-expressões, lembrando que os operadores se tornam o nó pai das suas duas sub-expressões, sendo que não toda a sub-expressão, mas apenas o resultado da rotina recursiva nos seus nós filhos. Perceba que a montagem dessa árvore gera o que usamos como exemplo na primeira figura.

Vamos ver agora o código referente às funções que criam e montam a árvore e a função que diz qual o valor da expressão presente na árvore dado que o valor da variável:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <math.h>

/* Caracteres que serão utilizados */#define SOMA 43#define SUBTRACAO 45#define MULTIPLICACAO 42#define DIVISAO 47#define VARIAVEL 120#define NUMEROS_INICIO 48#define NUMEROS_FIM 57

Page 48: Linguagem de Programação C

/* Informacão contida em cada nó */struct info {int tipo;int valor;};

/* Estrutura do nó */struct arvore {struct info info;struct arvore* pai;struct arvore* esquerda;struct arvore* direita;};

/* Funcão que diz quem é o operador principal da expressão. Devolve a posicão do operador na string de entrada */int procuraOperador (const char* entrada) {

int i;

/* Subtracão */for (i=0 ; i<strlen(entrada) ; i++) if (entrada[i] == SUBTRACAO) return(i);

/* Soma */for (i=0 ; i<strlen(entrada) ; i++) if (entrada[i] == SOMA) return(i);

/* Divisão */for (i=0 ; i<strlen(entrada) ; i++) if (entrada[i] == DIVISAO) return(i);

/* Multiplicacão */for (i=0 ; i<strlen(entrada) ; i++) if (entrada[i] == MULTIPLICACAO) return(i);

/* Variável */for (i=0 ; i<strlen(entrada) ; i++) if (entrada[i] == VARIAVEL) return(i);

/* Número qualquer */for (i=0 ; i<strlen(entrada) ; i++) if ((entrada[i] >= NUMEROS_INICIO) && (entrada[i] <= NUMEROS_FIM)) return(i);

/* Ocorreu algum erro. Retornar um erro sinalizando falta do operador */return(-1);

}

/* Cria um nó da árvore */struct arvore* criaArvore() {

struct arvore* ptrArvore = (struct arvore *) malloc(sizeof(struct arvore));ptrArvore->pai = ptrArvore->esquerda = ptrArvore->direita = NULL;return(ptrArvore);

}

/* Funcão que divide uma string, colocando as duas sub-strings em duas outras strings */void divideString (const char* entrada, char* esquerda, char* direita, int centro) {

int i;

for (i=0 ; i<centro ; i++) esquerda[i] = entrada[i];for (i=centro+1 ; i<strlen(entrada) ; i++) direita[i-centro-1] = entrada[i];

}

/* Funcão que vai montar a árvore */struct arvore* montaArvore (char* entrada) {

int i, j;int operador;char* esquerda = NULL;char* direita = NULL;struct arvore* ptrArvore = (struct arvore *) malloc(sizeof(struct arvore));

operador = procuraOperador(entrada);/* Erro na formação da entrada */if (operador == -1) return(NULL);

if ((entrada[operador] == DIVISAO) || (entrada[operador] == MULTIPLICACAO) ||

Page 49: Linguagem de Programação C

(entrada[operador] == SUBTRACAO) || (entrada[operador] == SOMA)) {

/* Um operador binário não pode estar no início da expressão */if (operador == 0) return(NULL);

ptrArvore->info.tipo = entrada[operador];

esquerda = (char *) malloc(sizeof(char)*operador);direita = (char *) malloc(sizeof(char)*(strlen(entrada)-operador-1));divideString(entrada, esquerda, direita, operador);

/* Recursividade */ptrArvore->esquerda = montaArvore(esquerda);ptrArvore->direita = montaArvore(direita);ptrArvore->esquerda->pai = ptrArvore->direita->pai = ptrArvore;if ((ptrArvore->esquerda == NULL) || (ptrArvore->direita == NULL)) return(NULL);

}

else if (entrada[operador] == VARIAVEL) ptrArvore->info.tipo = VARIAVEL;else if ((entrada[operador] >= NUMEROS_INICIO) && (entrada[operador] <= NUMEROS_FIM)) {

ptrArvore->info.tipo = NUMEROS_INICIO;ptrArvore->info.valor = 0;for (i=operador; (((entrada[operador] >= NUMEROS_INICIO) && (entrada[operador] <= NUMEROS_FIM)) && (i<strlen(entrada))) ; i++);i--; for (j=operador; j<=i ; j++) {

ptrArvore->info.valor += (entrada[j]-NUMEROS_INICIO) * pow(10, (i-j));

}

}

return(ptrArvore);

}

int valorArvore (struct arvore* ptrArvore, int variavel) {

if ((ptrArvore->info.tipo == DIVISAO) || (ptrArvore->info.tipo == MULTIPLICACAO) || (ptrArvore->info.tipo == SUBTRACAO) || (ptrArvore->info.tipo == SOMA)) {

if (ptrArvore->info.tipo == SOMA) return(valorArvore(ptrArvore->esquerda, variavel) + valorArvore(ptrArvore->direita, variavel));

else if (ptrArvore->info.tipo == SUBTRACAO) return(valorArvore(ptrArvore->esquerda, variavel) - valorArvore(ptrArvore->direita, variavel));

else if (ptrArvore->info.tipo == MULTIPLICACAO) return(valorArvore(ptrArvore->esquerda, variavel) *valorArvore(ptrArvore->direita, variavel));

else if (ptrArvore->info.tipo == DIVISAO) return(valorArvore(ptrArvore->esquerda, variavel) / valorArvore(ptrArvore->direita, variavel));

}

else if (ptrArvore->info.tipo == VARIAVEL) return(variavel);else return(ptrArvore->info.valor);

}

Lembrando que para utilizar a função pow precisamos incluir o cabeçalho math.h e que para compilar o programa de modo que as funções dessa biblioteca sejam incluídas temos que passar para o gcc a opção -lm para que ele inclua a biblioteca. Assim, poderiamos compilar o arquivo arvore.c da seguinte forma:

$ gcc -lm arvore.c -o binario

Lembrando também que por motivos de simplificação do problema, não está implementada a possibilidade de haver um

Page 50: Linguagem de Programação C

operador unário na expressão matemática de entrada, como por exemplo 3 * x + -4. Para incluir este caso basta fazer um tratamento no sentido de separar os casos em que o operador '-' realiza uma subtração e os casos em que realiza uma inversão de sinal. Não foi incluída também a possibilidade de haver parênteses para alterar a precedência natural dos operadores. Para tal, é necessário que se crie uma função que analisa a expressão em níveis, ou escopos, de tal forma que procuramos o operador apenas no primeiro nível, como se o parênteses

Assim como tem sido nas lições anteriores, as funções que fazem o trabalho de limpeza das estruturas ficam como exercício para o aluno. Vamos rever alguns conceitos importantes para o entendimento do código acima:

- Caracteres podem ser tratados como números inteiros, mas não o oposto. Por isso, precisamos da tabela ASCII para converter caracteres em uma variável do tipo inteiro. Além disso, precisamos de um método que nos auxilie a analisar corretamente a ordem das casas de um número. Para isso, utilizamos a função potência para montar o número. Por exemplo: o número 1344 pode ser montado somando-se 1*1000 + 3*100 + 4*10 + 4. Sendo assim, podemos fazer 1 * 10^3 + 3 * 10^2 + 4 * 10 + 4, e o que precisamos para gerar o código que faz essa montagem é apenas entender como os índices da string variam, do primeiro até o último algarismo do número, de forma que o loop não saia dessa área da string.- Assim como a montagem da árvore segue uma rotina recursiva, a rotina de dar um valor à expressão presente na árvore também segue.

Questão: Julgue a seguinte afirmativa.

As árvores binárias são assim chamadas porque o processador sempre utiliza números binários para fazer cálculos no seu nível mais baixo, mesmo quando estamos realizando operações na base decimal.

Falso

Correto. As árvore binárias são chamadas assim porque cada um dos nós pode ter zero, um ou dois nós abaixo dele, ou seja, nós filhos.

Grafos

Até agora já falamos sobre três tipos básicos de estruturas de dados: as pilhas, as listas e as árvores. Nas pilhas, o último elemento inserido é o primeiro a ser removido. Nas listas, dependendo da implementação, o primeiro elemento a ser inserido é o primeiro também a ser removido, configurando uma fila como nós as conhecemos. Em outras implementações que nós vimos, podemos inserir e retirar elementos de qualquer parte da lista. Já nas árvores, cada elemento pode ter zero, um ou dois filhos, o que configura uma árvore binária. Vimos que elas podem ser utilizadas para analisar expressões matemáticas, montando e avaliando as diferentes sub-expressões. Vamos falar agora sobre uma estrutura de dados que, assim como todas as que vimos até agora, foi criada com o objetivo de nos auxiliar a resolver problemas do dia-a-dia. Assim como as listas ligadas modelam muito bem a fila de cliente em uma agência de banco, os grafos modelam muito bem uma rede de transporte rodoviário de uma empresa logística. Com um grafo bem montado, a empresa tem como saber, instantaneamente, quais caminhos ela pode utilizar para levar uma encomenda, além de ter informações privilegiadas sobre o custo de cada um dos caminhos. Vamos começar?

O primeiro conceito que temos que ver é o próprio conceito de grafo: Um grafo é, essencialmente, um conjunto de nós (ou vértices) e arcos (ou arestas). Dizendo a mesma coisa de uma forma mais didática, um grafo é uma estrutura composta por pontos e traços ligando esses pontos, sempre dois a dois. Apenas conhecendo o conjunto de traços, não é possível montar o grafo por completo - eventuais nós sem qualquer arco ligado à eles estariam de fora do conjunto. Por isso precisamos dos dois conjuntos para configurar um grafo corretamente. Vamos ver um grafo:

O segundo conceito que precisamos ver é o conceito de grafo direcionado. Um grafo direcionado é um grafo que

Page 51: Linguagem de Programação C

diferencia um arco entre um nó A e um outro nó B - para ele os dois nós não são equivalentes. Uma outra forma de dizer isso é que o nó A->B não implica que há o nó B->A. Uma terceira forma de ver esse conceito consiste em perceber que, para obtermos a possibilidade de ida e volta entre dois nós, precimos das duas arestas, de ida e volta. O grafo presente na nossa primeira figura não é direcionado, já que não define direcionamento nas arestas. Vamos ver agora um grafo direcionado:

O terceiro conceito que precisamos ver é o conceito de matriz de adjacência. Quando vamos montar um grafo, a única coisa que precisamos para criar todos os nós e todas as arestas é uma matriz adjacência - ela nos diz quem está ligado a quem, mas contém também informações que nos permitem conhecer todos os arcos, até os que não estão ligados a nenhum outro arco. Vamos ver um exemplo de uma matriz de adjacência e seu grafo correspondente:

Uma matriz de adjacência Anxn nos dá todas as informações que precisamos para montar corretamente um grafo com n nós. Podemos obter essas informações checando o conteúdo dos elementos dessa matriz, da seguinte forma: se o elemento Aixj contiver um valor especificado pela documentação como sendo caminho existente, há uma aresta entre os dois nós. Por outro lado, se o elemento contiver um valor especificado na documentação como sendo caminho inexistente, não há uma aresta entre os dois nós. Podemos também utilizar o conceito, que aprendemos anteriormente, de grafo direcionado. Se fizermos isso, o elemento Aixj vai nos dizer se há uma aresta indo de i para j, mas nada nos diz com relação ao caminho de volta.

Sendo assim, na figura do exemplo temos uma matriz de adjacência A3x3, que nos permite montar um grafo com três nós. Vemos que o elemento A1x1 contém "0". Nesse caso, isso significa que não há uma aresta indo do nó 1 para ele mesmo. Já o elemento A1x2 contém "1". Iso quer dizer que há uma aresta indo do nó 1 para o nó 2. Porém , como A2x1 contém "0", quer dizer que não há o caminho de volta, ou seja, uma aresta indo do nó 2 para o nó 1.

Vamos ver então o código que cria o grafo a partir da matriz de adjacência:

#include <stdio.h>#include <stdlib.h>

#define TAMANHO 12

int mtrAdjacencia[TAMANHO][TAMANHO];

int main() {

Page 52: Linguagem de Programação C

int i,j, peso;

/* Limpando a matriz. Peso -1 quer dizer sem aresta */for (i=0 ; i<TAMANHO ; i++) {for (j=0 ; j<TAMANHO ; j++) {

mtrAdjacencia[i][j] = 0;

}

}

/* Inserindo os pontos */printf("Entre com os nós da seguinte forma: ORIGEM DESTINO PESO\n");for (;;) {

printf("(==) "); scanf("%d %d %d", &i, &j, &peso);if ((peso > 0) && (i > 0) && (j > 0)) mtrAdjacencia[i][j] = peso;else if ((peso == 0) && (i == 0) && (j == 0)) break;else printf("Houve um erro. Digite números maiores ou iguais a zero para entrar com os dados ou os tres numeros iguais a 0 para finalizar.\n");

}

}

Não se preocupe se você não entender bem como a função scanf está agindo nesse caso, vamos enxergar isso a partir de um exemplo de entrada e saída:

Entre com os nós da seguinte forma: ORIGEM DESTINO PESO(==) 1 2 200(==) 2 3 200(==) 3 1 100(==) 0 0 0

Com isso criamos um grafo contendo três nós e três arestas. Vale lembrar que como a nossa matriz de adjacência possui tamanho 12x12, temos na verdade doze nós, sendo que apenas três deles fazem parte de arestas. Vamos ver a matriz de montada a partir da entrada do exemplo:

0 200 00 0 200100 0 0

Agora que já vimos todos os conceitos importantes, podemos falar mais um pouco sobre menor caminho:

Existem vários algoritmos que se propõem a calcular o menor caminho entre dois nós de um grafo. Alguns deles calcula o menor caminho entre todos os pares de nós do grafo, enquanto que outros calculam o menor caminho entre dois nós específicos. Alguns utilizam operações matemáticas diretamente na matriz de adjacência, enquanto que outros utilizam vetores de nós conhecidos, desconhecidos, etc. O algoritmo que vamos conhecer no curso é creditado à Dijkstra e foi escolhido por ser extremamente eficiente e por não ser muito complexo, permitindo que o aluno acompanhe a sua execução. Vamos começar explicando as estruturas que o algoritmo utiliza:

INFINITO: Quando há uma aresta indo do nó i para o nó j, temos um peso ou custo associado à essa aresta. Se por outro lado não há essa aresta, representamos essa inexistência por um custo infinito, ou seja, a impossibilidade de ir de i para j. Utilizamos para isso uma constante contendo o maior inteiro possível.

int custoAresta(int origem, int destino): Nos diz justamente o custo associado à aresta indo de origem para destino.

vtrDistancia[i]: Vetor que guarda, durante os cálculos, o menor custo ou a menor distância entre o nó de origem e o nó j. Aproveitamos esse momento para ver que utiliza-se um vetor ao invés de uma variável justamente porque o algoritmo calcula, por padrão, o menor caminho entre um determinado nó e todos os outros (podemos modificá-lo para que ele calcule apenas o menor caminho entre dois nós).

Inicialmente, vamos setar vtrDistancia[origem] como sendo zero (custo zero para ir de origem para origem) e vtrDistancia[i] como sendo INFINITO para todos os outros nós, indicando que não conhecemos nenhum caminho ainda.

vtrPermanente[i]: Vetor que indica se a distância conhecida entre o nó de origem e o nó i já é com certeza mínima, o que implica que ela não será mais calculada. Percebemos a partir desse vetor que quando vtrPermanente[destino] for setado como "membro", o conteúdo de vtrDistancia[destino] será considerado a menor distância entre os nós de origem e destino e terminaremos o algoritmo. Utilizaremos o valor "1" para denominar "membro" e o valor "0" para denominar "não membro".

vtrCaminho[i]: Indica qual é o nó que antecede o nó i no caminho entre o nó de origem e o nó de destino. Esse vetor, com tamanho máximo igual ao número de nós do grafo, vai nos dizer exatamente qual é o menor caminho entre os dois nós. Ele deve ser global (declarado fora da função que calcula o menor caminho) para que seja

Page 53: Linguagem de Programação C

mantido e possa ser então utilizado posteriormente.

intAtual: Representa o último nó incluído no vetor vtrPermanete[]. No início do altoritmo, intAtual contém o nó de origem. Podemos ver aqui que sempre que um nó for incluído em vtrPermanete[] recalcularemos a distância até todos os nós que compartilham uma aresta com esse novo nó, checando se ela é menor do que a já conhecida.

Questão: julgue a seguinte afirmativa:

A principal diferença entre um grafo e uma árvore binária é que a árvore binária precisa ter um nó raiz, enquanto que o grafo não.

Falso

Correto. Na verdade, uma árvore, binária ou não, não precisa ter um nó raiz bem definido. Só é necessário que se conheça um nó do qual a rotina se iniciará. Por outro lado, a principal diferença entre as duas estruturas de dados é que os grafos são comumente definidos a partir de matrizes de adjacência, enquanto que as árvore não. Já com relação às árvores binárias, cada um de seus nós tem necessariamente zero, um ou dois nós filhos, ao contrário dos grafos, em que não há essa limitação

Estruturação e manipulação avançada do código e dos arquivos

Esta seção aborda conceitos sobre técnicas de organização do programa e de arquivos. Assuntos como representação de estruturas de dados persistentes em arquivos e organização do código em diversos arquivos serão apresentados aqui.

Lição 10 - Dividindo seu programa em diversos arquivos fontes

Controle de Inclusão

Vamos hoje introduzir alguns conceitos que serão necessários posteriormente quando formos falar sobre a divisão de um programa em vários códigos fontes. Vamos falar um pouco, nessa lição, sobre o mecanismo das inclusões e como esse mecanismo possibilita a divisão do programa em vários arquivos. Vamos falar um pouco, também, sobre as vantagens de se dividir o programa da forma proposta. Vamos começar?

Para entender o mecanismo de inclusões temos que analisar como essa inclusão é feita pelo compilador, ou sendo mais preciso, pelo pré-processador. Como já vimos anteriormente, o pré-processador é responsável por preparar o código antes de ele ser compilado propriamente dito pelo compilador. Essa preparação consiste, basicamente, em substituir, no corpo do texto, as macros e constantes definidas para aquele arquivo (isso é importante), além de retirar do código os comentários. A tarefa do pré-processador é mais complexa do que isso, mas para nós basta essa descrição, no momento.

Agora que sabemos que o que o pré-processador faz é inserir e retirar texto do código-fonte, podemos tentar entender a inclusão de um segundo arquivo, por exemplo, um cabeçalho.

Grande parte dos programas que criamos neste curso utilizaram a biblioteca de funções stdio.h. Como já vimos, o ".h" significa que o arquivo carrega headers, ou mais precisamente cabeçalhos de funções. Isto quer dizer que não se trata do código-fonte das funções (na verdade elas já estão compiladas, prontas para serem utilizadas pelo programador), e sim apenas dos cabeçalhos, que são utilizados pelo compilador para saber exatamente o que ele precisa incluir no código-objeto final. Agora sim podemos entender que a inclusão é apenas um conjunto de duas tarefas: a cópia de todo o texto do arquivo stdio.h dentro do arquivo contendo o código-fonte que vai ser compilado e posteriormente, durante a compilação, a inclusão do código já compilado das funções presentes nesse arquivo de cabeçalho no código-objeto final do programa.

Por isso podemos utilizar as funções na biblioteca stdio.h mesmo sem saber onde está seu código, porque o compilador se encarrega de incluir todos os dados necessários para que o programa possa ser executado normalmente.

Já que vimos que o arquivo de cabeçalho stdio.h pode ser incluído e isto nos permite utilizar funções que não foram implementadas por nós (reaproveitamento de código), nada nos impede de criar os nossos próprios arquivos de cabeçalho, associados à funções previamente implementada por nós que vai ser então incluído no nosso projeto. Antes de ver como fazer isso, vamos falar sobre um cuidado muito importante na hora de trabalhar com inclusões.

Como já vimos, o pré-processador, ao processar uma inclusão, copia para o corpo do código sendo preparado os protótipos das funções daquele cabeçalho que foi incluído. Bem, isso se torna um problema se por um acaso alguns ou todos esses cabeçalhos já estiverem presentes no código sendo preparado. Isso quer dizer que incluir um arquivo de cabeçalho mais de uma vez é um erro que deve ser evitado ao máximo. Ao trabalharmos com muitos arquivos, esse controle se tornaria realmente complicado se não tivéssemos nenhuma forma de garantir que nenhum protótipo esteja duplicado no código. Felizmente temos e é isso que vamos aprender agora.

Vamos primeiro criar um arquivo contendo uma função simples, que calcula a média aritmética de dois números. Então, vamos preparar um arquivo de cabeçalho para suportar o arquivo contendo a função que criamos, de forma que poderemos incluir esse arquivo sempre que precisarmos dessa função. Vamos começar com o arquivo media.h:

Page 54: Linguagem de Programação C

/* Controle de inclusão do arquivo de cabeçalho */#ifndef _MEDIA_H_#define _MEDIA_H_

float calculaMedia(float, float);

#endif

Podemos aprender três coisas com esse exemplo. A primeira, é que devemos padronizar o nome das constantes que vamos utilizar para controlar as inclusões. A segunda, é que devemos atrelar a declaração da constante de controle de inclusão aos protótipos das funções no arquivo. Podemos fazer isso como foi feito no exemplo, colocando todos os protótipos dentro da diretiva #ifndef, que só vai incluir tudo abaixo dela se a constante logo após sua chamada ainda não existir. A terceira é que, como alguns já devem ter percebido em alguns dos protótipos que vimos durante os cursos, não é necessário que se especifique o nome das variáveis no protótipo da função, podemos simplesmente declarar os tipos.

Agora podemos implementar a função cujo protótipo está no arquivo de cabeçalho.

/* Inclusão dos cabeçalhos */#include "media.h"

/* Código-fonte da função */float calculaMedia (float numero1, float numero2) {

return((numero1+numero2)/2);

}

Ao contrário dos protótipos, no momento da criação das funções os nomes das variáveis locais que são passadas como parâmetros são obrigatórias.

Certo, toda a parte de modularização da parte de cálculo de média do nosso projeto já está feita. Agora temos que ver mais duas coisas: como incluir essa parte do projeto no nosso arquivo contendo a função main e como compilar todos os arquivos, gerando um arquivo binário que tem condições de calcular a média.

/* Arquivo principal do projeto. Contém a função main */

/* Includes de bibliotecas padrão */#include <stdio.h>

/* Includes internos do projeto */#include "media.h"

int main() {

float numero1, numero2

printf("Digite dois numeros: ");scanf("%f %f", &numero1, &numero2);

printf("A media dos dois números é: %f", calculaMedia(numero1, numero2));

return(0);

}

Agora só nos falta aprender a compilar cada arquivo contendo código-fonte separadamente para depois unir os dois com o objetivo de gerar um arquivo executável. Faremos isso utilizando a opção -c do gcc. A sua utilização faz com que o gcc compile o arquivo mesmo que ele não tenha todo o código. Se não incluirmos essa opção, o gcc vai se recusar a compilar o arquivo main.c, dizendo que a função calculaMedia foi utilizada mas em momento algum foi declarada/implementada. Utilizando a opção, criamos apenas uma pecinha do programa, para depois juntar todas as peças. Acompanhe o processo de compilação:

$ gcc -c main.c -o main.o$ gcc -c madia.c -o media.o$$ gcc main.c media.c -o binario.out$ ./binario.out

Digite dois numeros: 10 12A media dos dois números é: 11.000000

$

Para quem já está um pouco mais acostumado com o terminal do GNU/Linux, há uma forma mais rápida de fazer a mesma coisa, desde que certas condições sejam respeitadas:

$ gcc -c *.c

Page 55: Linguagem de Programação C

$ gcc *.o -o binario.out$ ./binario.out

Simples, não? Podemos fazer isso com quantos arquivos quisermos, contanto que não nos esqueçamos de controlar as inclusões da forma correta.

Questão: julgue a seguinte afirmativa:

Ao compilar um programa constituído de mais de um arquivo contendo código, precisamos sempre realizar a compilação de tal forma que todo o código escrito esteja incluído no arquivo final.

Falso

Correto. Ao compilar um programa constituído de vários arquivos fonte, o importante é que cada um desses arquivos tenha acesso ao código necessário para que ele execute, e apenas o que for necessário (tanto a falta quanto a sobra devem ser evitadas). Por isso devemos utilizar a inclusão de arquivos de cabeçalho (que estão atrelados aos seus respectivos códigos) na medida certa.

Makefile

Vamos falar agora um pouco sobre como criar e utilizar um arquivo Makefile simples, mas antes vamos entender um pouco melhor o que é um arquivo Makefile:

Quando vamos compilar um programa simples, com poucos arquivos de código-fonte e poucos arquivos de cabeçalho, até podemos fazer isso manualmente, comandando o processo todo, principalmente se todos os arquivos estiverem na mesma pasta. Porém, se o programa for um pouco maior, contendo vários arquivos de código-fonte e vários arquivos de cabeçalho, além de estar espalhado em um número grande de pastas, passar pelo processo de compilação manualmente se torna uma tarefa extremamente sujeita a erros - podemos esquecer de compilar algum arquivo ou compilar arquivos em uma ordem incorreta. Por isso, o arquivo Makefile é tão importante e tão utilizado em um ambiente em que quase tudo pode ser adquirido na forma de código-fonte. A função dele é coordenar o processo de compilação, definindo uma ordem, uma lista de arquivos e definindo como cada um desses arquivos deve ser compilado. Vamos começar a montar o arquivo, explicando o objetivo de cada parte dele.

A primeira coisa a fazer é definir algumas constantes que o Makefile vai utilizar para compilar os arquivos. Elas vão permitir que a forma como os arquivos são compilados mude de uma forma simples - precisaríamos modificar apenas as constantes. Vamos criar constantes para o compilador, para as opções do compilador e para a lista de arquivos. Tudo no arquivo Makefile pode virar constante:

# Constantes utilizadas na compilação

# Compilador que vai ser utilizadoCC = gcc

# Lista de arquivos código-objetoOBJ = main.o media.o

# Arquivo binárioBIN = binario.out

# Opções a serem passadas ao compiladorFLAGS = -g -Wall

# Comando utilizado para remover os arquivos (limpar o diretorio)RM = rm -f

Certo. Agora vamos definir modos de compilação, em que cada módulo é constituído de três partes: um nome, pré-requisitos e um conteúdo. A sintaxe utilizada seria a seguinte:

<nome do modo> : <arquivos que devem existir><conteúdo>

Vamos começar com o modo de compilação responsável por limpar o diretório. Seu nome normalmente é clean.

clean : $(RM) $(OBJ) $(BIN)

Utilizando as constantes dessa forma, o Makefile vai automaticamente substituir $(CONSTANTE) pela string com a qual a constante é definida. Vamos agora criar o modo de compilaçao all, que precisa de todos os arquivos código-objeto já criados

Page 56: Linguagem de Programação C

para que possa ser executada. Normalmente podemos compilar todo o projeto apenas executando:

$ make all

Vamos ver como fazer isso :

all : $(OBJ)$(CC) $(FLAGS) $(OBJ) -o $(BIN)

Caso o conteúdo desse modo de compilação não tenha ficado claro, vamos ver que ele é correspondente a:

$ gcc -g -Wall main.o media.o -o binario.out

Vamos agora criar os dois últimos modos de compilação: dos dois arquivos código-fonte, lembrando de um pequeno detalhe: Os modos de compilação que geram um código-objeto a partir de um único arquivo contendo código-fonte precisam ter como nome o próprio nome do arquivo código-objeto - assim o Makefile, quando recebe o comando make all vai saber quais arquivos ele precisa compilar baseando-se nos pré-requisitos que o modo all fornece.

main.o: main.c$(CC) $(FLAGS) -c main.c -o main.o

media.o: media.c$(CC) $(FLAGS) -c media.c -o media.o

Questão: Com o Makefile, podemos automatizar todo o processo de compilação, inclusive o controle de inclusão dos arquivos de cabeçalho.

Parcialmente correto

Correto. O Makefile não trabalha especificamente com o controle de inclusão. Porém, com ele podemos especificar o comando que vai compilar cada um dos arquivos contendo código-fonte do programa envolvido. Se as inclusões estiverem corretas nos arquivos, a compilação será feita de tal forma que será incluído em cada arquivo apenas o código externo que realmente precisa ser incluído

Lição 11 - Manipulação avançada de arquivos

Estrutura de dados e persistência

Vamos aproveitar essa última lição para aprender a integrar a técnica de criação de estruturas de dados dinâmicas com a persistência em arquivos. Isto é, vamos criar uma estrutura de dados que pode ser salva em um arquivo e recuperada posteriormente, caso o usuário deseje. Vamos continuar trabalhando com alocação dinâmica de memória, de modo que utilizaremos os ponteiros para controlar o que vai ser escrito nos arquivos. Estes, serão binários, o que nos permite guardar não apenas texto e números inteiros, mas também estruturas compostas.

Primeiro, vamos aprender um pouco sobre como controlar a posição do cursor no arquivo. Isto é, vamos aprender a controlar em que posição do arquivo estamos escrevendo. Este aprendizado nos permitirá controlar melhor como o arquivo está sendo escrito, caso o método escolhido no projeto seja modificar dinâmicamente o arquivo. Vamos falar mais um pouco sobre isso.

Quando estivermos trabalhando com um banco de dados extenso, a criação e total escrita do arquivo se tornará um operação demorada e desnecessária. Por este motivo, um método mais inteligente de escrita se torna necessário. Uma idéia é controlarmos as posições do arquivo que já estão ocupadas e as que estão livres, de forma que na hora de escrever o arquivo vamos apenas escrever aqueles elementos que estão sendo criados, modificados ou removidos. Além disso, podemos trabalhar com uma forma de desfragmentação, controlando o arquivo com o objetivo de impedir que surjam muitos espaços em brancos no seu conteúdo. Vamos ver as funções que podem nos ajudar a controlar a posição do arquivo:

A primeira vai nos ajudar a saber em que posição do arquivo está o cursor de leitura e escrita. Há outra função que faz a mesma coisa, mas esta retorna um inteiro que pode ser utilizado de um número de formas diferentes. Este inteiro representa a posição atual do cursor, em bytes. Por exemplo: se estamos escrevendo números inteiros no arquivo e já escrevemos 10 vezes um número inteiro, o cursor vai estar na décima primeira posição inteira, ou 11*TAMANHO_INTEIRO. Vamos ver o protótipo da função:

long int ftell ( FILE * stream );

A segunda função vai complementar a anterior: com ela, vamos poder setar a posição do cursor do arquivo. Assim , poderemos escolher em que ponto do arquivo vamos escrever. Isto é especialmente útil, como já comentamos, para evitar fragmentação e controlar o tamanho do arquivo. Vamos ver o protótipo:

int fseek ( FILE * stream, long int offset, int origin );

Se a função for bem sucedida, retornará zero. Se não, retornará um número diferente de zero. A variável long int offset diz justamente quantos bytes após origin queremos que o cursor fique, e int origin é uma variável que determina um início, a partir do qual offset bytes serão contados. Origin pode ter os seguintes valores:

Page 57: Linguagem de Programação C

SEEK_SET = Início do arquivoSEEK_CUR = Posição atual do arquivoSEEK_END = Final do arquivo

Utilizando essas duas funções, em conjunto com a implementação de uma estrutura de dados, é possível criar um programa que seja capaz de armazenar os dados inseridos pelos usuários e manter esses dados em disco quando o programa é fechado. Agora o que precisamos fazer é só criar uma função que utilize essas duas e entregue ao programador uma interface capaz de ler e escrever no arquivo em qualquer posição deste.

int escreveArquivo (const void* ptrDados, int tamanho, int posicao, FILE* arqDados) {

int i;

/* Caso a função não seja bem sucedida */if ((i = fseek(arqDados, posicao*tamanho, SEEK_SET) != 0) return(i);

fwrite(ptrDados, tamanho, 1, arqDados);

return(0);

}

A função de leitura, análoga à essa, fica como exercício para o aluno. Com essas duas funções, só precisamos criar uma função que, utilizando um arquivo texto ou binário, saiba quais posições do nosso arquivo principal estão livres e quais estão ocupadas. Veremos aqui uma das duas - a que seta uma posição como ocupada. A outra fica como exercício.

int setaOcupado(int posicao, FILE* arqPosicoes) {

int i;if ((i = fseek(arqPosicoes, posicao, SEEK_SET) != 0) return(i);

fwrite(ptrOcupado, sizeof(ptrOcupado), 1, arqPosicoes);

return(0);

}

Esta função não vai funcionar, da forma como está escrita. Primeiro porque ela utiliza a função fwrite, que é utilizada apenas com arquivos binários, sendo que o ideal é que o aluno decida se vai utilizar um arquivo binário ou texto para organizar as posições. Segundo porque o ponteiro utilizado ptrOcupado não foi definido. Caso o aluno decida pelo arquivo binário, ele deve especificar de que tipo será esse ponteiro e que valores ele assumirá para denominar "ocupado"e "livre".

Questão: julgue a seguinte afirmativa:

É muito importante, ao se trabalhar com estrutura de dados, que alguma técnica de persistência esteja integrada ao projeto.

Verdadeiro

Correto. Como vimos anteriormente, as técnicas de estrutura de dados se integram muito bem com as técnicas de persistência porque enquanto que a primeira é responsável por gerar e manipular dados (utilizando o usuário como fonte), a segunda se responsabiliza por manter esses dados em disco, de modo que o usuário tem todos os dados à mao toda vez que iniciar o programa.

Faça 2 programas que permitam respectivamente a codificação e a decodificação de arquivos de e para a representação em base64.

Seja, um dos programas deve ser responsável pela codificação e o outro deve ser responsável pela respectiva decodificação.

A codificação de um arquivo qualquer (origem) para uma representação base64 (destino) é feita da seguinte forma:

1 - Leia 3 bytes do arquivo origem.

Ex: 3 bytes => (11110000) (10101010) (00001111)

2 - Separe esses 3 bytes em 4 blocos contendo 6 bits cada.

Ex:

(11110000) (10101010) (00001111 ) =>=> (111100 | 00 ) ( 1010 | 1010 ) ( 00 | 001111 ) =>=> (111100) (001010) (101000 ) (001111 ).

OBS. Os bits em negrito pertenciam ao 1o Byte, os em itálico pertenciam ao 2o Byte e os sublinhados pertenciam ao

Page 58: Linguagem de Programação C

3o Byte.

3 - Represente esses 4 blocos usando bytes. Para isso, concatene esses 4 blocos com dois zeros (0) nos bits mais significantes (bits da esquerda).

Ex:

(111100) (001010) (101000) (001111) => => (00111100) (00001010) (00101000) (00001111)

OBS. Os bits em negrito foram os zeros (0) inseridos.

Converta os 4 Bytes resultantes para caracteres (letras) de acordo com a lista decimal-caractere abaixo:

0 = 'A',1 = 'B',2 = 'C',3 = 'D',4 = 'E',5 = 'F',6 = 'G',7 = 'H',8 = 'I',9 = 'J', 10 = 'K',11 = 'L',12 = 'M',13 = 'N',14 = 'O',15 = 'P',16 = 'Q',17 = 'R',18 = 'S',19 = 'T', 20 = 'U','21 = V',22 = 'W',23 = 'X',24 = 'Y',25 = 'Z',26 = 'a',27 = 'b',28 = 'c',29 = 'd', 30 = 'e',31 = 'f',32 = 'g',33 = 'h',34 = 'i',35 = 'j',36 = 'k',37 = 'l',38 = 'm',39 = 'n', 40 = 'o','p','q','r','s','t','u','v','w','x', 'y','z','0','1','2','3','4','5','6','7','8','9','+','/'

Page 59: Linguagem de Programação C

Ex: Binário: (00111100) (00001010) (00101000) (00001111) = = Decimal: (60) (10) (40) (15) = = Caractere: ('8') ('K') ('o') ('P')

OBS. Se o arquivo de entrada não tiver quantidade de bytes em múltiplo de 3, preencha o arquivo destino com '=' para indicar que aquele ponto não tem representação no arquivo original.