Linguagem C
Versão 1.0.0
Sumário
I Sobre essa Apostila 2
II Informações Básicas 4
III GNU Free Documentation License 9
IV A linguagem C 18
1 O que é a linguagem C? 19
2 Plano de ensino 20
2.1 Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2 Público Alvo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3 Pré-requisitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4 Descrição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.5 Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6 Cronograma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.7 Programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.8 Avaliação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.9 Bibliografia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3 Introdução à linguagem C 23
3.1 Introdução teórica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.1 O que é a linguagem de programação C? . . . . . . . . . . . . . . . . . . . . 23
3.1.2 Quem realmente controla os programas? . . . . . . . . . . . . . . . . . . . . 24
3.1.3 Histórico da linguagem C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.1.4 Características . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.1.5 O compilador GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.1.6 Tipos de Erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2 Elementos léxicos e sintáticos da linguagem C, parte 1 . . . . . . . . . . . . . . . . 32
3.2.1 Palavras Reservadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.2.2 Variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.2.3 Operadores Aritméticos e de Atribuição . . . . . . . . . . . . . . . . . . . . . 35
3.2.4 Tipos de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2.5 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.3 Elementos léxicos e sintáticos da linguagem C, parte 2 . . . . . . . . . . . . . . . . 41
1
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
3.3.1 Introdução às funções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.3.2 Bibliotecas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.3.3 Operadores Lógicos e de Comparação . . . . . . . . . . . . . . . . . . . . . 52
3.3.4 Controle de Fluxo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.4 Elementos léxicos e sintáticos da linguagem C, parte 3 . . . . . . . . . . . . . . . . 60
3.4.1 Vetores e Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
3.4.2 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.4.3 Introdução ao uso de ponteiros . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.4.4 Casts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4 Código estruturado em C e ponteiros 69
4.1 Manipulação de arquivos e entradas/saídas em C . . . . . . . . . . . . . . . . . . . 69
4.1.1 Entrada e saída com o teclado e o monitor . . . . . . . . . . . . . . . . . . . 69
4.1.2 Introdução à manipulação de arquivos . . . . . . . . . . . . . . . . . . . . . . 72
4.2 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.2.1 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.3 Manipulação de ponteiros e tipos avançados de dados . . . . . . . . . . . . . . . . . 79
4.3.1 Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.3.2 Alocação dinâmica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5 Abstração de dados 88
5.1 Estruturas de dados, parte 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.1.1 Pilhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.1.2 Filas encadeadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.1.3 Filas duplamente encadeadas . . . . . . . . . . . . . . . . . . . . . . . . . . 94
5.1.4 Filas circulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.2 Estruturas de dados, parte 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.2.1 Árvores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.2.2 Grafos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
6 Estruturação e manipulação avançada do código e dos arquivos 109
6.1 Dividindo seu programa em diversos arquivos fontes . . . . . . . . . . . . . . . . . . 109
6.1.1 Contole de inclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
6.1.2 Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2 Manipulação avançada de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.2.1 Estrutura de dados e persistência . . . . . . . . . . . . . . . . . . . . . . . . 113
2
Parte I
Sobre essa Apostila
3
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Conteúdo
O conteúdo dessa apostila é fruto da compilação de diversos materiais livres publicados na in-
ternet, disponíveis em diversos sites ou originalmente produzido no CDTC (http://www.cdtc.org.br.)
O formato original deste material bem como sua atualização está disponível dentro da licença
GNU Free Documentation License, cujo teor integral encontra-se aqui reproduzido na seção de
mesmo nome, tendo inclusive uma versão traduzida (não oficial).
A revisão e alteração vem sendo realizada pelo CDTC (suporte@cdtc.org.br) desde outubro
de 2006. Críticas e sugestões construtivas serão bem-vindas a qualquer hora.
Autores
A autoria deste é de responsabilidade de Fernando Machado Mendonça (fmachado@cdtc.org.br).
O texto original faz parte do projeto Centro de Difusão de Tecnologia e Conhecimento que
vêm sendo realizado pelo ITI (Instituto Nacional de Tecnologia da Informação) em conjunto com
outros parceiros institucionais, e com as universidades federais brasileiras que tem produzido e
utilizado Software Livre apoiando inclusive a comunidade Free Software junto a outras entidades
no país.
Informações adicionais podem ser obtidas através do email ouvidoria@cdtc.org.br, ou da
home page da entidade, através da URL http://www.cdtc.org.br.
Garantias
O material contido nesta apostila é isento de garantias e o seu uso é de inteira responsabi-
lidade do usuário/leitor. Os autores, bem como o ITI e seus parceiros, não se responsabilizam
direta ou indiretamente por qualquer prejuízo oriundo da utilização do material aqui contido.
Licença
Copyright ©2006, Instituto Nacional de Tecnologia da Informação (cdtc@iti.gov.br) .
Permission is granted to copy, distribute and/or modify this document under the terms
of the GNU Free Documentation License, Version 1.1 or any later version published by
the Free Software Foundation; with the Invariant Chapter being SOBRE ESSA APOS-
TILA. A copy of the license is included in the section entitled GNU Free Documentation
License.
4
Parte II
Informações Básicas
5
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Sobre o CDTC
Objetivo Geral
O Projeto CDTC visa a promoção e o desenvolvimento de ações que incentivem a dissemina-
ção de soluções que utilizem padrões abertos e não proprietários de tecnologia, em proveito do
desenvolvimento social, cultural, político, tecnológico e econômico da sociedade brasileira.
Objetivo Específico
Auxiliar o Governo Federal na implantação do plano nacional de software não-proprietário e
de código fonte aberto, identificando e mobilizando grupos de formadores de opinião dentre os
servidores públicos e agentes políticos da União Federal, estimulando e incentivando o mercado
nacional a adotar novos modelos de negócio da tecnologia da informação e de novos negócios
de comunicação com base em software não-proprietário e de código fonte aberto, oferecendo
treinamento específico para técnicos, profissionais de suporte e funcionários públicos usuários,
criando grupos de funcionários públicos que irão treinar outros funcionários públicos e atuar como
incentivadores e defensores dos produtos de software não proprietários e código fonte aberto, ofe-
recendo conteúdo técnico on-line para serviços de suporte, ferramentas para desenvolvimento de
produtos de software não proprietários e do seu código fonte livre, articulando redes de terceiros
(dentro e fora do governo) fornecedoras de educação, pesquisa, desenvolvimento e teste de pro-
dutos de software livre.
Guia do aluno
Neste guia, você terá reunidas uma série de informações importantes para que você comece
seu curso. São elas:
• Licenças para cópia de material disponível;
• Os 10 mandamentos do aluno de Educação a Distância;
• Como participar dos foruns e da wikipédia;
• Primeiros passos.
É muito importante que você entre em contato com TODAS estas informações, seguindo o
roteiro acima.
Licença
Copyright ©2006, Instituto Nacional de Tecnologia da Informação (cdtc@iti.gov.br).
6
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
É dada permissão para copiar, distribuir e/ou modificar este documento sob os termos
da Licença de Documentação Livre GNU, Versão 1.1 ou qualquer versão posterior
públicada pela Free Software Foundation; com o Capitulo Invariante SOBRE ESSA
APOSTILA. Uma cópia da licença está inclusa na seção entitulada "Licença de Docu-
mentação Livre GNU".
Os 10 mandamentos do aluno de educação online
• 1. Acesso à Internet: ter endereço eletrônico, um provedor e um equipamento adequado é
pré-requisito para a participação nos cursos a distância;
• 2. Habilidade e disposição para operar programas: ter conhecimentos básicos de Informá-
tica é necessário para poder executar as tarefas;
• 3. Vontade para aprender colaborativamente: interagir, ser participativo no ensino a distân-
cia conta muitos pontos, pois irá colaborar para o processo ensino-aprendizagem pessoal,
dos colegas e dos professores;
• 4. Comportamentos compatíveis com a etiqueta: mostrar-se interessado em conhecer seus
colegas de turma respeitando-os e se fazendo ser respeitado pelos mesmos;
• 5. Organização pessoal: planejar e organizar tudo é fundamental para facilitar a sua revisão
e a sua recuperação de materiais;
• 6. Vontade para realizar as atividades no tempo correto: anotar todas as suas obrigações e
realizá-las em tempo real;
• 7. Curiosidade e abertura para inovações: aceitar novas idéias e inovar sempre;
• 8. Flexibilidade e adaptação: requisitos necessário à mudança tecnológica, aprendizagens
e descobertas;
• 9. Objetividade em sua comunicação: comunicar-se de forma clara, breve e transparente é
ponto - chave na comunicação pela Internet;
• 10. Responsabilidade: ser responsável por seu próprio aprendizado. O ambiente virtual não
controla a sua dedicação, mas reflete os resultados do seu esforço e da sua colaboração.
Como participar dos fóruns e Wikipédia
Você tem um problema e precisa de ajuda?
Podemos te ajudar de 2 formas:
A primeira é o uso dos fóruns de notícias e de dúvidas gerais que se distinguem pelo uso:
. O fórum de notícias tem por objetivo disponibilizar um meio de acesso rápido a informações
que sejam pertinentes ao curso (avisos, notícias). As mensagens postadas nele são enviadas a
7
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
todos participantes. Assim, se o monitor ou algum outro participante tiver uma informação que
interesse ao grupo, favor postá-la aqui.
Porém, se o que você deseja é resolver alguma dúvida ou discutir algum tópico específico do
curso. É recomendado que você faça uso do Fórum de dúvidas gerais que lhe dá recursos mais
efetivos para esta prática.
. O fórum de dúvidas gerais tem por objetivo disponibilizar um meio fácil, rápido e interativo
para solucionar suas dúvidas e trocar experiências. As mensagens postadas nele são enviadas
a todos participantes do curso. Assim, fica muito mais fácil obter respostas, já que todos podem
ajudar.
Se você receber uma mensagem com algum tópico que saiba responder, não se preocupe com a
formalização ou a gramática. Responda! E não se esqueça de que antes de abrir um novo tópico
é recomendável ver se a sua pergunta já foi feita por outro participante.
A segunda forma se dá pelas Wikis:
. Uma wiki é uma página web que pode ser editada colaborativamente, ou seja, qualquer par-
ticipante pode inserir, editar, apagar textos. As versões antigas vão sendo arquivadas e podem
ser recuperadas a qualquer momento que um dos participantes o desejar. Assim, ela oferece um
ótimo suporte a processos de aprendizagem colaborativa. A maior wiki na web é o site "Wikipé-
dia", uma experiência grandiosa de construção de uma enciclopédia de forma colaborativa, por
pessoas de todas as partes do mundo. Acesse-a em português pelos links:
• Página principal da Wiki - http://pt.wikipedia.org/wiki/
Agradecemos antecipadamente a sua colaboração com a aprendizagem do grupo!
Primeiros Passos
Para uma melhor aprendizagem é recomendável que você siga os seguintes passos:
• Ler o Plano de Ensino e entender a que seu curso se dispõe a ensinar;
• Ler a Ambientação do Moodle para aprender a navegar neste ambiente e se utilizar das
ferramentas básicas do mesmo;
• Entrar nas lições seguindo a seqüência descrita no Plano de Ensino;
• Qualquer dúvida, reporte ao Fórum de Dúvidas Gerais.
Perfil do Tutor
Segue-se uma descrição do tutor ideal, baseada no feedback de alunos e de tutores.
O tutor ideal é um modelo de excelência: é consistente, justo e profissional nos respectivos
valores e atitudes, incentiva mas é honesto, imparcial, amável, positivo, respeitador, aceita as
idéias dos estudantes, é paciente, pessoal, tolerante, apreciativo, compreensivo e pronto a ajudar.
8
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
A classificação por um tutor desta natureza proporciona o melhor feedback possível, é crucial, e,
para a maior parte dos alunos, constitui o ponto central do processo de aprendizagem.’ Este tutor
ou instrutor:
• fornece explicações claras acerca do que ele espera e do estilo de classificação que irá
utilizar;
• gosta que lhe façam perguntas adicionais;
• identifica as nossas falhas, mas corrige-as amavelmente’, diz um estudante, ’e explica por-
que motivo a classificação foi ou não foi atribuída’;
• tece comentários completos e construtivos, mas de forma agradável (em contraste com um
reparo de um estudante: ’os comentários deixam-nos com uma sensação de crítica, de
ameaça e de nervossismo’)
• dá uma ajuda complementar para encorajar um estudante em dificuldade;
• esclarece pontos que não foram entendidos, ou corretamente aprendidos anteriormente;
• ajuda o estudante a alcançar os seus objetivos;
• é flexível quando necessário;
• mostra um interesse genuíno em motivar os alunos (mesmo os principiantes e, por isso,
talvez numa fase menos interessante para o tutor);
• escreve todas as correções de forma legível e com um nível de pormenorização adequado;
• acima de tudo, devolve os trabalhos rapidamente;
9
Parte III
GNU Free Documentation License
10
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
(Traduzido pelo João S. O. Bueno através do CIPSGA em 2001)
Esta é uma tradução não oficial da Licença de Documentação Livre GNU em Português Brasi-
leiro. Ela não é publicada pela Free Software Foundation, e não se aplica legalmente a distribuição
de textos que usem a GFDL - apenas o texto original em Inglês da GNU FDL faz isso. Entretanto,
nós esperamos que esta tradução ajude falantes de português a entenderem melhor a GFDL.
This is an unofficial translation of the GNU General Documentation License into Brazilian Por-
tuguese. It was not published by the Free Software Foundation, and does not legally state the
distribution terms for software that uses the GFDL–only the original English text of the GFDL does
that. However, we hope that this translation will help Portuguese speakers understand the GFDL
better.
Licença de Documentação Livre GNU Versão 1.1, Março de 2000
Copyright (C) 2000 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
É permitido a qualquer um copiar e distribuir cópias exatas deste documento de licença, mas
não é permitido alterá-lo.
INTRODUÇÃO
O propósito desta Licença é deixar um manual, livro-texto ou outro documento escrito "livre"no
sentido de liberdade: assegurar a qualquer um a efetiva liberdade de copiá-lo ou redistribui-lo,
com ou sem modificações, comercialmente ou não. Secundariamente, esta Licença mantém
para o autor e editor uma forma de ter crédito por seu trabalho, sem ser considerado responsável
pelas modificações feitas por terceiros.
Esta Licença é um tipo de "copyleft"("direitos revertidos"), o que significa que derivações do
documento precisam ser livres no mesmo sentido. Ela complementa a GNU Licença Pública Ge-
ral (GNU GPL), que é um copyleft para software livre.
Nós fizemos esta Licença para que seja usada em manuais de software livre, por que software
livre precisa de documentação livre: um programa livre deve ser acompanhado de manuais que
provenham as mesmas liberdades que o software possui. Mas esta Licença não está restrita a
manuais de software; ela pode ser usada para qualquer trabalho em texto, independentemente
do assunto ou se ele é publicado como um livro impresso. Nós recomendamos esta Licença prin-
cipalmente para trabalhos cujo propósito seja de introdução ou referência.
APLICABILIDADE E DEFINIÇÕES
Esta Licença se aplica a qualquer manual ou outro texto que contenha uma nota colocada pelo
detentor dos direitos autorais dizendo que ele pode ser distribuído sob os termos desta Licença.
O "Documento"abaixo se refere a qualquer manual ou texto. Qualquer pessoa do público é um
11
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
licenciado e é referida como "você".
Uma "Versão Modificada"do Documento se refere a qualquer trabalho contendo o documento
ou uma parte dele, quer copiada exatamente, quer com modificações e/ou traduzida em outra
língua.
Uma "Seção Secundária"é um apêndice ou uma seção inicial do Documento que trata ex-
clusivamente da relação dos editores ou dos autores do Documento com o assunto geral do
Documento (ou assuntos relacionados) e não contém nada que poderia ser incluído diretamente
nesse assunto geral (Por exemplo, se o Documento é em parte um livro texto de matemática, a
Seção Secundária pode não explicar nada de matemática).
Essa relação poderia ser uma questão de ligação histórica com o assunto, ou matérias relaci-
onadas, ou de posições legais, comerciais, filosóficas, éticas ou políticas relacionadas ao mesmo.
As "Seções Invariantes"são certas Seções Secundárias cujos títulos são designados, como
sendo de Seções Invariantes, na nota que diz que o Documento é publicado sob esta Licença.
Os "Textos de Capa"são certos trechos curtos de texto que são listados, como Textos de Capa
Frontal ou Textos da Quarta Capa, na nota que diz que o texto é publicado sob esta Licença.
Uma cópia "Transparente"do Documento significa uma cópia que pode ser lida automatica-
mente, representada num formato cuja especificação esteja disponível ao público geral, cujos
conteúdos possam ser vistos e editados diretamente e sem mecanismos especiais com editores
de texto genéricos ou (para imagens compostas de pixels) programas de pintura genéricos ou
(para desenhos) por algum editor de desenhos grandemente difundido, e que seja passível de
servir como entrada a formatadores de texto ou para tradução automática para uma variedade
de formatos que sirvam de entrada para formatadores de texto. Uma cópia feita em um formato
de arquivo outrossim Transparente cuja constituição tenha sido projetada para atrapalhar ou de-
sencorajar modificações subsequentes pelos leitores não é Transparente. Uma cópia que não é
"Transparente"é chamada de "Opaca".
Exemplos de formatos que podem ser usados para cópias Transparentes incluem ASCII sim-
ples sem marcações, formato de entrada do Texinfo, formato de entrada do LaTex, SGML ou XML
usando uma DTD disponibilizada publicamente, e HTML simples, compatível com os padrões, e
projetado para ser modificado por pessoas. Formatos opacos incluem PostScript, PDF, formatos
proprietários que podem ser lidos e editados apenas com processadores de texto proprietários,
SGML ou XML para os quais a DTD e/ou ferramentas de processamento e edição não estejam
disponíveis para o público, e HTML gerado automaticamente por alguns editores de texto com
finalidade apenas de saída.
A "Página do Título"significa, para um livro impresso, a página do título propriamente dita,
mais quaisquer páginas subsequentes quantas forem necessárias para conter, de forma legível,
o material que esta Licença requer que apareça na página do título. Para trabalhos que não
tenham uma página do título, "Página do Título"significa o texto próximo da aparição mais proe-
minente do título do trabalho, precedendo o início do corpo do texto.
12
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
FAZENDO CÓPIAS EXATAS
Você pode copiar e distribuir o Documento em qualquer meio, de forma comercial ou não
comercial, desde que esta Licença, as notas de copyright, e a nota de licença dizendo que esta
Licença se aplica ao documento estejam reproduzidas em todas as cópias, e que você não acres-
cente nenhuma outra condição, quaisquer que sejam, às desta Licença.
Você não pode usar medidas técnicas para obstruir ou controlar a leitura ou confecção de
cópias subsequentes das cópias que você fizer ou distribuir. Entretanto, você pode aceitar com-
pensação em troca de cópias. Se você distribuir uma quantidade grande o suficiente de cópias,
você também precisa respeitar as condições da seção 3.
Você também pode emprestar cópias, sob as mesmas condições colocadas acima, e também
pode exibir cópias publicamente.
FAZENDO CÓPIAS EM QUANTIDADE
Se você publicar cópias do Documento em número maior que 100, e a nota de licença do
Documento obrigar Textos de Capa, você precisará incluir as cópias em capas que tragam, clara
e legivelmente, todos esses Textos de Capa: Textos de Capa da Frente na capa da frente, e
Textos da Quarta Capa na capa de trás. Ambas as capas também precisam identificar clara e
legivelmente você como o editor dessas cópias. A capa da frente precisa apresentar o título com-
pleto com todas as palavras do título igualmente proeminentes e visíveis. Você pode adicionar
outros materiais às capas. Fazer cópias com modificações limitadas às capas, tanto quanto estas
preservem o título do documento e satisfaçam a essas condições, pode ser tratado como cópia
exata em outros aspectos.
Se os textos requeridos em qualquer das capas for muito volumoso para caber de forma
legível, você deve colocar os primeiros (tantos quantos couberem de forma razoável) na capa
verdadeira, e continuar os outros nas páginas adjacentes.
Se você publicar ou distribuir cópias Opacas do Documento em número maior que 100, você
precisa ou incluir uma cópia Transparente que possa ser lida automaticamente com cada cópia
Opaca, ou informar, em ou com, cada cópia Opaca a localização de uma cópia Transparente
completa do Documento acessível publicamente em uma rede de computadores, à qual o público
usuário de redes tenha acesso a download gratuito e anônimo utilizando padrões públicos de
protocolos de rede. Se você utilizar o segundo método, você precisará tomar cuidados razoavel-
mente prudentes, quando iniciar a distribuição de cópias Opacas em quantidade, para assegurar
que esta cópia Transparente vai permanecer acessível desta forma na localização especificada
por pelo menos um ano depois da última vez em que você distribuir uma cópia Opaca (direta-
mente ou através de seus agentes ou distribuidores) daquela edição para o público.
É pedido, mas não é obrigatório, que você contate os autores do Documento bem antes de
redistribuir qualquer grande número de cópias, para lhes dar uma oportunidade de prover você
com uma versão atualizada do Documento.
13
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
MODIFICAÇÕES
Você pode copiar e distribuir uma Versão Modificada do Documento sob as condições das se-
ções 2 e 3 acima, desde que você publique a Versão Modificada estritamente sob esta Licença,
com a Versão Modificada tomando o papel do Documento, de forma a licenciar a distribuição
e modificação da Versão Modificada para quem quer que possua uma cópia da mesma. Além
disso, você precisa fazer o seguinte na versão modificada:
A. Usar na Página de Título (e nas capas, se houver alguma) um título distinto daquele do Do-
cumento, e daqueles de versões anteriores (que deveriam, se houvesse algum, estarem listados
na seção "Histórico do Documento"). Você pode usar o mesmo título de uma versão anterior se
o editor original daquela versão lhe der permissão;
B. Listar na Página de Título, como autores, uma ou mais das pessoas ou entidades responsá-
veis pela autoria das modificações na Versão Modificada, conjuntamente com pelo menos cinco
dos autores principais do Documento (todos os seus autores principais, se ele tiver menos que
cinco);
C. Colocar na Página de Título o nome do editor da Versão Modificada, como o editor;
D. Preservar todas as notas de copyright do Documento;
E. Adicionar uma nota de copyright apropriada para suas próprias modificações adjacente às
outras notas de copyright;
F. Incluir, imediatamente depois das notas de copyright, uma nota de licença dando ao público
o direito de usar a Versão Modificada sob os termos desta Licença, na forma mostrada no tópico
abaixo;
G. Preservar nessa nota de licença as listas completas das Seções Invariantes e os Textos de
Capa requeridos dados na nota de licença do Documento;
H. Incluir uma cópia inalterada desta Licença;
I. Preservar a seção entitulada "Histórico", e seu título, e adicionar à mesma um item dizendo
pelo menos o título, ano, novos autores e editor da Versão Modificada como dados na Página de
Título. Se não houver uma sessão denominada "Histórico"no Documento, criar uma dizendo o
título, ano, autores, e editor do Documento como dados em sua Página de Título, então adicionar
um item descrevendo a Versão Modificada, tal como descrito na sentença anterior;
J. Preservar o endereço de rede, se algum, dado no Documento para acesso público a uma
cópia Transparente do Documento, e da mesma forma, as localizações de rede dadas no Docu-
mento para as versões anteriores em que ele foi baseado. Elas podem ser colocadas na seção
"Histórico". Você pode omitir uma localização na rede para um trabalho que tenha sido publicado
pelo menos quatro anos antes do Documento, ou se o editor original da versão a que ela se refira
der sua permissão;
K. Em qualquer seção entitulada "Agradecimentos"ou "Dedicatórias", preservar o título da
14
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
seção e preservar a seção em toda substância e fim de cada um dos agradecimentos de contri-
buidores e/ou dedicatórias dados;
L. Preservar todas as Seções Invariantes do Documento, inalteradas em seus textos ou em
seus títulos. Números de seção ou equivalentes não são considerados parte dos títulos da seção;
M. Apagar qualquer seção entitulada "Endossos". Tal sessão não pode ser incluída na Versão
Modificada;
N. Não reentitular qualquer seção existente com o título "Endossos"ou com qualquer outro
título dado a uma Seção Invariante.
Se a Versão Modificada incluir novas seções iniciais ou apêndices que se qualifiquem como
Seções Secundárias e não contenham nenhum material copiado do Documento, você pode optar
por designar alguma ou todas aquelas seções como invariantes. Para fazer isso, adicione seus
títulos à lista de Seções Invariantes na nota de licença da Versão Modificada. Esses títulos preci-
sam ser diferentes de qualquer outro título de seção.
Você pode adicionar uma seção entitulada "Endossos", desde que ela não contenha qual-
quer coisa além de endossos da sua Versão Modificada por várias pessoas ou entidades - por
exemplo, declarações de revisores ou de que o texto foi aprovado por uma organização como a
definição oficial de um padrão.
Você pode adicionar uma passagem de até cinco palavras como um Texto de Capa da Frente
, e uma passagem de até 25 palavras como um Texto de Quarta Capa, ao final da lista de Textos
de Capa na Versão Modificada. Somente uma passagem de Texto da Capa da Frente e uma de
Texto da Quarta Capa podem ser adicionados por (ou por acordos feitos por) qualquer entidade.
Se o Documento já incluir um texto de capa para a mesma capa, adicionado previamente por
você ou por acordo feito com alguma entidade para a qual você esteja agindo, você não pode
adicionar um outro; mas você pode trocar o antigo, com permissão explícita do editor anterior que
adicionou a passagem antiga.
O(s) autor(es) e editor(es) do Documento não dão permissão por esta Licença para que seus
nomes sejam usados para publicidade ou para assegurar ou implicar endossamento de qualquer
Versão Modificada.
COMBINANDO DOCUMENTOS
Você pode combinar o Documento com outros documentos publicados sob esta Licença, sob
os termos definidos na seção 4 acima para versões modificadas, desde que você inclua na com-
binação todas as Seções Invariantes de todos os documentos originais, sem modificações, e liste
todas elas como Seções Invariantes de seu trabalho combinado em sua nota de licença.
O trabalho combinado precisa conter apenas uma cópia desta Licença, e Seções Invariantes
Idênticas com multiplas ocorrências podem ser substituídas por apenas uma cópia. Se houver
múltiplas Seções Invariantes com o mesmo nome mas com conteúdos distintos, faça o título de
15
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
cada seção único adicionando ao final do mesmo, em parênteses, o nome do autor ou editor
origianl daquela seção, se for conhecido, ou um número que seja único. Faça o mesmo ajuste
nos títulos de seção na lista de Seções Invariantes nota de licença do trabalho combinado.
Na combinação, você precisa combinar quaisquer seções entituladas "Histórico"dos diver-
sos documentos originais, formando uma seção entitulada "Histórico"; da mesma forma combine
quaisquer seções entituladas "Agradecimentos", ou "Dedicatórias". Você precisa apagar todas as
seções entituladas como "Endosso".
COLETÂNEAS DE DOCUMENTOS
Você pode fazer uma coletânea consitindo do Documento e outros documentos publicados
sob esta Licença, e substituir as cópias individuais desta Licença nos vários documentos com
uma única cópia incluida na coletânea, desde que você siga as regras desta Licença para cópia
exata de cada um dos Documentos em todos os outros aspectos.
Você pode extrair um único documento de tal coletânea, e distribuí-lo individualmente sob
esta Licença, desde que você insira uma cópia desta Licença no documento extraído, e siga esta
Licença em todos os outros aspectos relacionados à cópia exata daquele documento.
AGREGAÇÃO COM TRABALHOS INDEPENDENTES
Uma compilação do Documento ou derivados dele com outros trabalhos ou documentos se-
parados e independentes, em um volume ou mídia de distribuição, não conta como uma Ver-
são Modificada do Documento, desde que nenhum copyright de compilação seja reclamado pela
compilação. Tal compilação é chamada um "agregado", e esta Licença não se aplica aos outros
trabalhos auto-contidos compilados junto com o Documento, só por conta de terem sido assim
compilados, e eles não são trabalhos derivados do Documento.
Se o requerido para o Texto de Capa na seção 3 for aplicável a essas cópias do Documento,
então, se o Documento constituir menos de um quarto de todo o agregado, os Textos de Capa
do Documento podem ser colocados em capas adjacentes ao Documento dentro do agregado.
Senão eles precisarão aparecer nas capas de todo o agregado.
TRADUÇÃO
Tradução é considerada como um tipo de modificação, então você pode distribuir traduções
do Documento sob os termos da seção 4. A substituição de Seções Invariantes por traduções
requer uma permissão especial dos detentores do copyright das mesmas, mas você pode incluir
traduções de algumas ou de todas as Seções Invariantes em adição às versões orignais dessas
Seções Invariantes. Você pode incluir uma tradução desta Licença desde que você também in-
clua a versão original em Inglês desta Licença. No caso de discordância entre a tradução e a
16
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
versão original em Inglês desta Licença, a versão original em Inglês prevalecerá.
TÉRMINO
Você não pode copiar, modificar, sublicenciar, ou distribuir o Documento exceto como expres-
samente especificado sob esta Licença. Qualquer outra tentativa de copiar, modificar, sublicen-
ciar, ou distribuir o Documento é nula, e resultará automaticamente no término de seus direitos
sob esta Licença. Entretanto, terceiros que tenham recebido cópias, ou direitos de você sob esta
Licença não terão suas licenças terminadas, tanto quanto esses terceiros permaneçam em total
acordo com esta Licença.
REVISÕES FUTURAS DESTA LICENÇA
A Free Software Foundation pode publicar novas versões revisadas da Licença de Documen-
tação Livre GNU de tempos em tempos. Tais novas versões serão similares em espirito à versão
presente, mas podem diferir em detalhes ao abordarem novos porblemas e preocupações. Veja
http://www.gnu.org/copyleft/.
A cada versão da Licença é dado um número de versão distinto. Se o Documento especificar
que uma versão particular desta Licença "ou qualquer versão posterior"se aplica ao mesmo, você
tem a opção de seguir os termos e condições daquela versão específica, ou de qualquer versão
posterior que tenha sido publicada (não como rascunho) pela Free Software Foundation. Se o
Documento não especificar um número de Versão desta Licença, você pode escolher qualquer
versão já publicada (não como rascunho) pela Free Software Foundation.
ADENDO: Como usar esta Licença para seus documentos
Para usar esta Licença num documento que você escreveu, inclua uma cópia desta Licença
no documento e ponha as seguintes notas de copyright e licenças logo após a página de título:
Copyright (c) ANO SEU NOME.
É dada permissão para copiar, distribuir e/ou modificar este documento sob os termos da Licença
de Documentação Livre GNU, Versão 1.1 ou qualquer versão posterior publicada pela Free Soft-
ware Foundation; com as Seções Invariantes sendo LISTE SEUS TÍTULOS, com os Textos da
Capa da Frente sendo LISTE, e com os Textos da Quarta-Capa sendo LISTE. Uma cópia da li-
cença está inclusa na seção entitulada "Licença de Documentação Livre GNU".
Se você não tiver nenhuma Seção Invariante, escreva "sem Seções Invariantes"ao invés de
dizer quais são invariantes. Se você não tiver Textos de Capa da Frente, escreva "sem Textos de
Capa da Frente"ao invés de "com os Textos de Capa da Frente sendo LISTE"; o mesmo para os
Textos da Quarta Capa.
Se o seu documento contiver exemplos não triviais de código de programas, nós recomenda-
mos a publicação desses exemplos em paralelo sob a sua escolha de licença de software livre,
17
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
tal como a GNU General Public License, para permitir o seu uso em software livre.
18
Parte IV
A linguagem C
19
Capítulo 1
O que é a linguagem 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 conside-
rada 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.
20
Capítulo 2
Plano de ensino
2.1 Objetivo
Introduzir aos técnicos e programadores a linguagem de programação C padrão ANSI.
2.2 Público Alvo
Técnicos e Programadores que desejam trabalhar com a linguagem C.
2.3 Pré-requisitos
Os usuários deverão ter conhecimento básico acerca da lógica de programação.
2.4 Descrição
O curso de Linguagem de Programação C será realizado na modalidade EAD e utilizará a
plataforma Moodle como ferramenta de aprendizagem. Ele é composto por módulos de apren-
dizado que serão dados no início de cada semana e um módulo de avaliação que será dado na
última. O material didático estará disponível on-line de acordo com as datas pré-estabelecidas
no calendário. A versão utilizada para o compilador (o GCC) será a 4.0.4
2.5 Metodologia
O curso está dividido da seguinte maneira:
2.6 Cronograma
• Semana 1 - Introdução à linguagem C;
• Semana 2 - Código estruturado em C e ponteiros;
• Semana 3 - Abstração de dados;
21
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• Semana 4 - Estruturação e manipulação avançada do código e dos arquivos;
• Avaliação - Avaliação de aprendizagem e avaliação final.
As lições contêm o conteúdo principal. Elas poderão ser acessadas quantas vezes forem ne-
cessárias, desde que esteja dentro da semana programada. Ao final de uma lição, você receberá
uma nota de acordo com o seu desempenho. Responda com atenção às perguntas de cada li-
ção, pois elas serão consideradas na sua nota final. Caso sua nota numa determinada lição seja
menor que 6.0, sugerimos que você faça novamente esta lição.
Ao final do curso será disponibilizada a avaliação referente ao curso. Tanto as notas das lições
quanto a da avaliação serão consideradas para a nota final. Todos os módulos ficarão visíveis
para que possam ser consultados durante a avaliação final.
Aconselhamos a leitura da "Ambientação do Moodle"para que você conheça a plataforma de En-
sino a Distância, evitando dificuldades advindas do "desconhecimento"sobre a mesma.
Os instrutores estarão à sua disposição ao longo de todo curso. Qualquer dúvida deverá ser
enviada ao fórum. Diariamente os monitores darão respostas e esclarecimentos.
2.7 Programa
O curso da linguagem C oferecerá o seguinte conteúdo:
• Lição 1 - Introdução teórica;
• Lição 2 - Elementos léxicos e sintáticos da linguagem C, parte 1;
• Lição 3 - Elementos léxicos e sintáticos da linguagem C, parte 2;
• Lição 4 - Elementos léxicos e sintáticos da linguagem C, parte 3;
• Lição 5 - Manipulação de arquivos e entradas/saídas em C;
• Lição 6 - Recursividade;
• Lição 7 - Manipulação de ponteiros e tipos avançados de dados;
• Lição 8 - Estruturas de dados, parte 1;
• Lição 9 - Estruturas de dados, parte 2;
• Lição 10 - Dividindo seu programa em diversos arquivos fontes;
• Lição 11 - Manipulação avançada de arquivos.
2.8 Avaliação
Em cache - Páginas Semelhantes
Toda a avaliação será feita on-line. Aspectos a serem considerados na avaliação:
• Iniciativa e autonomia no processo de aprendizagem e de produção de conhecimento;
22
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• Capacidade de pesquisa e abordagem criativa na solução dos problemas apresentados.
Instrumentos de avaliação:
• Participação ativa nas atividades programadas;
• Avaliação ao final do curso;
• O participante fará várias avaliações referente ao conteúdo do curso. Para a aprovação e
obtenção do certificado o participante deverá obter nota final maior ou igual a 6.0 de acordo
com a fórmula abaixo:
• Nota Final = ((ML x 7) + (AF x 3)) / 10 = Média aritmética das lições
• AF = Avaliações .
2.9 Bibliografia
• Curso O Linux : http://www.coltec.ufmg.br/ info/cursoC_olinux/curso_c_htm.htm
• cplusplus.com : http://www.cplusplus.com/
23
Capítulo 3
Introdução à linguagem C
3.1 Introdução teórica
3.1.1 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,
24
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 é im-
posto 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 Byte-
code). Na concepção da linguagem é que se decide se ela vai ser 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 objeti-
vos. 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 pro-
grama 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 lingua-
gem C. Eles são para fins ilustrativos. Os interessados podem tentar compilar o código ou analisar
o que eles fazem.
3.1.2 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) en-
tre 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
25
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
sistema e que ele possa trabalhar com maior grau de abstração (se preocupar menos com pro-
blemas 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 esca-
lonadores 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 es-
perando 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 sistemas 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 me-
ramente os "bits".
O interpretador de comandos 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 interpre-
tador de comandos é conhecido no mundo Linux como SHELL (pode variar para outros sistemas).
3.1.3 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 {
26
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
p2 = p - 1;
}
p = p1 + ((p2 - p1)/2);
}
return NULL;
}
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 (des-
crito 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 pa-
dronizaçã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. Atual-
mente, 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 por-
tabilidade) é 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.
3.1.4 Características
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 hard-
ware;
2. Foi projetado para ser usado por programadores especializados interessados em geração
de códigos compactos e eficientes;
27
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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;
12. 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.
3.1.5 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);
28
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 com-
pilaçã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.
1. O compilador realiza a "pré-compilação", ou seja, processa comandos especiais direciona-
dos 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ân-
ticos 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 compila-
dores 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
29
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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), ge-
rando 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.
$ gcc -o destino fonte.c
O seu programa se chamará destino e será o derivado do fonte chamado 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.
3.1.6 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[] ) {
30
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 basica-
mente 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 mate-
má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.
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.
31
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 pro-
duto 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
32
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 co-
mando. 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 Pas-
cal), o último comando não precisa desse elemento, pois o último comando não tem algo a seguir
para precisar separar.
3.2 Elementos léxicos e sintáticos da linguagem C, parte 1
3.2.1 Palavras Reservadas
Um dos elementos léxicos mais importantes de uma linguagem de programação são as pa-
lavras reservadas. As palavras 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 ele-
mentos 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 lingua-
gem. 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 determi-
nadas 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 desig-
nados (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
33
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• 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
34
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Todo conjunto de palavras reservadas acima são o conjunto das instruções básicas do C. Apa-
rentemente, 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 encon-
trará nenhuma versão do C sem nenhuma delas. Algumas são até tratadas como parte da lin-
guagem por serem padronizadas.
3.2.2 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ús-
culas 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 é uti-
lizado 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á armaze-
nado. 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ên-
cias, 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 asso-
ciados 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.
35
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Declaração de uma variável
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 inici-
alizado 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;
3.2.3 Operadores Aritméticos e de Atribuição
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.
36
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 correta-
mente 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, para corretamente
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. 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=%fn",intNumero1, floatNumero1);
intNumero2 = intNumero1 + floatNumero1;
printf("inteiro = inteiro + flutuante: %dnn",intNumero2);
intNumero1 = 1;
floatNumero1 = 2.5;
37
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
printf("intNumero1=%d e floatNumero1=%fn",intNumero1, floatNumero1);
intNumero2 = intNumero1 * floatNumero1;
printf("inteiro = inteiro * flutuante: %dnn",intNumero2);
intNumero1 = 10;
floatNumero1 = 3;
printf("intNumero1=%d e floatNumero1=%fn",intNumero1, floatNumero1);
intNumero2 = intNumero1 / floatNumero1;
printf("inteiro = inteiro / futuante: %dnn",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 funci-
onalidade 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 */
38
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Métodos interessantes de atribuição
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 + varNumero2
varNumero1 -= varNumero2 equivale à varNumero1 = varNumero1 - varNumero2
varNumero1 *= varNumero2 equivale à varNumero1 = varNumero1 * varNumero2
varNumero1 /= 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 equi-
vale à 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.
3.2.4 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:
39
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• 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 univer-
sal.
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 desconsidera-
dos. 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;
• 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;
40
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• 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 armazena-
mento 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 ex-
terno 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.
3.2.5 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 das variá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() {
41
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
comando1;
comando2;
...
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 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.
3.3 Elementos léxicos e sintáticos da linguagem C, parte 2
3.3.1 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... perplexo
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;
42
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• 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 mate-
má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 reapro-
veitamento 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
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 lingua-
gem (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:
43
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 sis-
tema 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 compilado-
res 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 portabi-
lidade (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.
44
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 detalha-
damente), 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
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;
45
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
3.3.2 Bibliotecas
Já que mostramos o que é uma função, aproveitamos para apresentarmos o que é uma bibli-
oteca.
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 conveni-
entes.
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
46
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• termio.h
• aliases.h
• expat_external.h
• jmorecfg.h
• pthread.h
• termios.h
• alloca.h
• expat.h
• jpegint.h
• pty.h
• tgmath.h
• a.out.h
• fcntl.h
• jpeglib.h
• pwd.h
• thread_db.h
• argp.h
• features.h
• langinfo.h
• re_comp.h
• tiffconf.h
• argz.h
• fenv.h
• lastlog.h
• regex.h
• tiff.h
• ar.h
• FlexLexer.h
• libgen.h
47
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• regexp.h
• tiffio.h
• assert.h
• fmtmsg.h
• libintl.h
• resolv.h
• tiffvers.h
• autosprintf.h
• fnmatch.h
• libio.h
• sched.h
• time.h
• byteswap.h
• fpu_control.h
• limits.h
• search.h
• tls.h
• complex.h
• fstab.h
• link.h
• semaphore.h
• ttyent.h
• cpio.h
• ft2build.h
• locale.h
• setjmp.h
• ucontext.h
• crypt.h
• fts.h
48
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• malloc.h
• sgtty.h
• ulimit.h
• ctype.h
• ftw.h
• math.h
• shadow.h
• unistd.h
• dialog.h
• _G_config.h
• mcheck.h
• signal.h
• ustat.h
• dirent.h
• gconv.h
• memory.h
• spawn.h
• utime.h
• dlfcn.h
• getopt.h
• mntent.h
• stab.h
• utmp.h
• dlg_colors.h
• gettext-po.h
• monetary.h
• stdint.h
• utmpx.h
• dlg_config.h
49
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• glob.h
• mqueue.h
• stdio_ext.h
• values.h
• dlg_keys.h
• gnu-versions.h
• netdb.h
• stdio.h
• wait.h
• dts.h
• grp.h
• nl_types.h
• stdlib.h
• wchar.h
• elf.h
• gsm.h
• nss.h
• string.h
• wctype.h
• endian.h
• iconv.h
• obstack.h
• strings.h
• wordexp.h
• envz.h
• ieee754.h
• oss-redir.h
• stropts.h
• xlocale.h
50
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• err.h
• ifaddrs.h
• paths.h
• syscall.h
• zconf.h
• errno.h
• initreq.h
• pngconf.h
• sysexits.h
• zlib.h
• error.h
• inttypes.h
• png.h
• syslog.h
• execinfo.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
51
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 (ex-
plicaçõ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/):
#include <NOME_DA_BIBLIOTECA.h>
Para incluir uma biblioteca pessoal:
#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 so-
bre 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. 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:
52
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
/*************************************************************/
/* 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 é o exemplo número %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...");
}
Exemplo de um programa
3.3.3 Operadores Lógicos e de Comparação
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 preci-
samos 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:
53
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• > : 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. Intuiti-
vamente, podemos ver que:
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 sabe-
mos 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
54
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 aplica-
dos 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, traba-
lhamos 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á, es-
tã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ía-
mos 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
55
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• «: 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 "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.
56
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
3.3.4 Controle de Fluxo
Vamos agora introduzir à nossa lista de ferramentas do C algumas das que são mais utiliza-
das 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
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.
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
57
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 facil-
mente 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.
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:
58
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
int main() {
/* Atribui os valores às variáveis */
intNumero1 = 5;
intNumero2 = 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 condi-
cional 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:
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 bi-
ná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
59
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
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 comandos */
if ( ( (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!
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 pro-
grama 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;
60
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
while (intNUmero <= 100) {
printf("%d ",intNumero);
ntNumero = 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 incremen-
tada, 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.
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. É im-
portante 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}
3.4 Elementos léxicos e sintáticos da linguagem C, parte 3
3.4.1 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
61
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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
#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]);
62
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
}
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: %.2fn",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
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, precisa-
mos 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 exata-
mente à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.
63
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
3.4.2 Strings
Vamos ver agora uma parte bastante importante do C. Já comentamos antes que introduzi-
mos 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 es-
pecí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 peri-
gosa. 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 conseguir-
mos, 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 me-
mó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, es-
tamos 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 con-
forme 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.
64
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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á 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 carac-
teres 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.
65
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
3.4.3 Introdução ao uso de ponteiros
Vamos ver agora o que é uma das partes mais úteis da linguagem C. Os ponteiros são res-
ponsá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 utili-
zar 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.
66
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
float calculaMedia3 (float * vetNumeros);
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;
67
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 opera-
dor ’*’. 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;
/* Imprime: "O primeiro elemento é o 1" */
printf("O primeiro elemento é o %dn", *ptrNumero);
/* Incrementando o ponteiro */
ptrNumero += 1;
/* Imprime : "O segundo elmento é o 2" */
printf("O segundo elemento é o %dn" *ptrNumero);
/* Imprime de 1 a 100 */
for (i=0 ; i<100 ; i++) printf("O %do elemento é %dn", i+1, *(ptrNumero+i));
Vamos analisar algumas das operações feitas:
68
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
3.4.4 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 ope-
raçã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.
69
Capítulo 4
Código estruturado em C e ponteiros
4.1 Manipulação de arquivos e entradas/saídas em C
4.1.1 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 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
fmachado@cdtc.org.br.textbackslash{}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 */
70
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 exem-
plo 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
• %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.
71
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
• %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 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. As-
sim 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 carac-
teres 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.
72
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
4.1.2 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 da-
dos em geral que estejam relacionados ao programa de alguma forma. Podemos realizar persis-
tê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 - confi-
guraçõ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.
73
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 pos-
teriormente. 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() {
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 negadan");
/* 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
74
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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,
75
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
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 exata-
mente 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 saber-
mos 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:
76
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
n = 0;
while (!feof(arqArquivo1)) {
fgetc (pFile);
n++;
}
fclose (pFile);
printf ("Total de bytes lidos: %dn", 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 pratica-
mente qualquer coisa. Seu protótipo é:
int fprintf ( FILE * stream, const char * format, ... );
Vamos ver um exemplo:
fprintf(arqArquivo1, "%s - %dtextbackslash{}n", strNome, intDiasTrabalhados);
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 );
77
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
4.2 Recursividade
4.2.1 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 tor-
nar 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! é 1 passo 11: o valor de 2! é 2 * 1 = 2 passo 12: o valor de 3! é 3 * 2 = 6 passo 13: o valor
de 4! é 4 * 6 = 24 passo 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 recur-
78
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
sã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)!
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 processa-
mento 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 abor-
dado 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
79
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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));
}
4.3 Manipulação de ponteiros e tipos avançados de dados
4.3.1 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 uti-
lizando 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 {
80
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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: %.2fn", floatMedia);
81
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
return(0);
}
É 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;
};
82
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
Criando a struct dessa forma a leitura das nota do aluno seria feita com um código bem mais
elegante, assim:
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 estiver-
mos 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:
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:
83
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 ope-
rador ’->’ para o ponteiro varAluno e o operador ’.’ para a estrutura endereço.
4.3.2 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 pro-
grama 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 va-
riá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 poder que as variáveis dinâmicas dão ao
84
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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:
85
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 me-
mó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 elemen-
tos do tipo inteiro, lembrando que o 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 estatica-
mente. 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:
86
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
*(vtrInteiros+i) = 20;
Esse comando diz que queremos que o espaço de memória i*sizeof(int) bytes após o ende-
reç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. Va-
mos 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 co-
luna 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:
87
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
#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 com-
pilaçã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;
}
}
88
Capítulo 5
Abstração de dados
5.1 Estruturas de dados, parte 1
5.1.1 Pilhas
Vamos começar a falar essa semana sobre uma parte bastante importante de qualquer lin-
guagem 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 necessi-
dades 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 pon-
teiros.
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 es-
colha 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 envol-
vido, 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
89
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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
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 simples-
mente 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ó preci-
samos 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 alo-
cação dinâmica free e malloc, vamos ter que incluir o cabeçalho stdlib, responsável por manter
90
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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);
}
91
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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]);
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.
5.1.2 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.
92
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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
93
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
que lidar com mais de um caso. Vamos também precisar de uma função que cria um elemento
antes de 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 estru-
tura 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
94
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 mo-
tivo, 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 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.
5.1.3 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
95
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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;
96
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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;
97
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
5.1.4 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 aproximar-
mos 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
98
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
99
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
5.2 Estruturas de dados, parte 2
5.2.1 Á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 árvo-
res 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 as-
sim, 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, va-
mos 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
100
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
de uma árvore montada:
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 an-
tes 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 va-
lor, 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 ár-
vore, 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 mole-
culares 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 es-
sas 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
101
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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
/* 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);
102
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
/* 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 */
103
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
if (operador == -1) return(NULL);
if ((entrada[operador] == DIVISAO) ||
(entrada[operador] == MULTIPLICACAO) ||
(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) {
104
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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, poderíamos 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 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, precisa-
mos da tabela ASCII para converter caracteres em uma variável do tipo inteiro. Além disso,
105
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
5.2.2 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, depen-
dendo 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, po-
demos 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 utiliza-
das 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. Va-
mos começar?
O primeiro conceito que temos que ver é o próprio conceito de grafo: Um grafo é, essencial-
mente, 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 pon-
tos, 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 direcio-
nado é um grafo que diferencia um arco entre um nó A e um outro nó B - para ele os dois nós não
106
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 possibili-
dade 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 ou-
tro 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 apren-
demos 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 signi-
fica 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:
107
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
#include <stdio.h>
#include <stdlib.h>
#define TAMANHO 12
int mtrAdjacencia[TAMANHO][TAMANHO];
int main() {
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 PESOn");
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:
108
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
0 200 0
0 0 200
100 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 associ-
ado à 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 ori-
gem para origem) e vtrDistancia[i] como sendo INFINITO para todos os outros nós, indi-
cando 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 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.
109
Capítulo 6
Estruturação e manipulação avançada
do código e dos arquivos
6.1 Dividindo seu programa em diversos arquivos fontes
6.1.1 Contole 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 st-
dio.h. Como já vimos, o ".h"significa que o arquivo carrega headers, ou mais precisamente ca-
beç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 pro-
110
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
grama 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 implemen-
tada 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 prepa-
rado. 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 real-
mente 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 preci-
sarmos dessa função. Vamos começar com o arquivo media.h:
/* 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);
}
111
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
o 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 utili-
zando 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
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
$ gcc *.o -o binario.out
$ ./binario.out
112
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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.
6.1.2 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 pou-
cos 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 manual-
mente 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 im-
portante 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 utilizado
CC = gcc
# Lista de arquivos código-objeto
OBJ = main.o media.o
# Arquivo binário
BIN = binario.out
# Opções a serem passadas ao compilador
FLAGS = -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>
113
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 $(CONS-
TANTE) 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 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
6.2 Manipulação avançada de arquivos
6.2.1 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 pontei-
ros 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.
114
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
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 in-
teligente 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:
SEEK_SET = Início do arquivo
SEEK_CUR = Posição atual do arquivo
SEEK_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;
115
CDTC Centro de Difusão de Tecnologia e Conhecimento Brasil/DF
/* 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".
116

Linguagem c

  • 1.
  • 2.
    Sumário I Sobre essaApostila 2 II Informações Básicas 4 III GNU Free Documentation License 9 IV A linguagem C 18 1 O que é a linguagem C? 19 2 Plano de ensino 20 2.1 Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.2 Público Alvo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.3 Pré-requisitos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.4 Descrição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.5 Metodologia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.6 Cronograma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.7 Programa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2.8 Avaliação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2.9 Bibliografia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 3 Introdução à linguagem C 23 3.1 Introdução teórica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.1.1 O que é a linguagem de programação C? . . . . . . . . . . . . . . . . . . . . 23 3.1.2 Quem realmente controla os programas? . . . . . . . . . . . . . . . . . . . . 24 3.1.3 Histórico da linguagem C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.1.4 Características . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.1.5 O compilador GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.1.6 Tipos de Erros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.2 Elementos léxicos e sintáticos da linguagem C, parte 1 . . . . . . . . . . . . . . . . 32 3.2.1 Palavras Reservadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.2.2 Variáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.2.3 Operadores Aritméticos e de Atribuição . . . . . . . . . . . . . . . . . . . . . 35 3.2.4 Tipos de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.2.5 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.3 Elementos léxicos e sintáticos da linguagem C, parte 2 . . . . . . . . . . . . . . . . 41 1
  • 3.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 3.3.1 Introdução às funções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.3.2 Bibliotecas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 3.3.3 Operadores Lógicos e de Comparação . . . . . . . . . . . . . . . . . . . . . 52 3.3.4 Controle de Fluxo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.4 Elementos léxicos e sintáticos da linguagem C, parte 3 . . . . . . . . . . . . . . . . 60 3.4.1 Vetores e Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.4.2 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.4.3 Introdução ao uso de ponteiros . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.4.4 Casts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4 Código estruturado em C e ponteiros 69 4.1 Manipulação de arquivos e entradas/saídas em C . . . . . . . . . . . . . . . . . . . 69 4.1.1 Entrada e saída com o teclado e o monitor . . . . . . . . . . . . . . . . . . . 69 4.1.2 Introdução à manipulação de arquivos . . . . . . . . . . . . . . . . . . . . . . 72 4.2 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 4.2.1 Recursividade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 4.3 Manipulação de ponteiros e tipos avançados de dados . . . . . . . . . . . . . . . . . 79 4.3.1 Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 4.3.2 Alocação dinâmica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 5 Abstração de dados 88 5.1 Estruturas de dados, parte 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 5.1.1 Pilhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 5.1.2 Filas encadeadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 5.1.3 Filas duplamente encadeadas . . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.1.4 Filas circulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 5.2 Estruturas de dados, parte 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 5.2.1 Árvores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 5.2.2 Grafos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 6 Estruturação e manipulação avançada do código e dos arquivos 109 6.1 Dividindo seu programa em diversos arquivos fontes . . . . . . . . . . . . . . . . . . 109 6.1.1 Contole de inclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 6.1.2 Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 6.2 Manipulação avançada de arquivos . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 6.2.1 Estrutura de dados e persistência . . . . . . . . . . . . . . . . . . . . . . . . 113 2
  • 4.
  • 5.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Conteúdo O conteúdo dessa apostila é fruto da compilação de diversos materiais livres publicados na in- ternet, disponíveis em diversos sites ou originalmente produzido no CDTC (http://www.cdtc.org.br.) O formato original deste material bem como sua atualização está disponível dentro da licença GNU Free Documentation License, cujo teor integral encontra-se aqui reproduzido na seção de mesmo nome, tendo inclusive uma versão traduzida (não oficial). A revisão e alteração vem sendo realizada pelo CDTC (suporte@cdtc.org.br) desde outubro de 2006. Críticas e sugestões construtivas serão bem-vindas a qualquer hora. Autores A autoria deste é de responsabilidade de Fernando Machado Mendonça (fmachado@cdtc.org.br). O texto original faz parte do projeto Centro de Difusão de Tecnologia e Conhecimento que vêm sendo realizado pelo ITI (Instituto Nacional de Tecnologia da Informação) em conjunto com outros parceiros institucionais, e com as universidades federais brasileiras que tem produzido e utilizado Software Livre apoiando inclusive a comunidade Free Software junto a outras entidades no país. Informações adicionais podem ser obtidas através do email ouvidoria@cdtc.org.br, ou da home page da entidade, através da URL http://www.cdtc.org.br. Garantias O material contido nesta apostila é isento de garantias e o seu uso é de inteira responsabi- lidade do usuário/leitor. Os autores, bem como o ITI e seus parceiros, não se responsabilizam direta ou indiretamente por qualquer prejuízo oriundo da utilização do material aqui contido. Licença Copyright ©2006, Instituto Nacional de Tecnologia da Informação (cdtc@iti.gov.br) . Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with the Invariant Chapter being SOBRE ESSA APOS- TILA. A copy of the license is included in the section entitled GNU Free Documentation License. 4
  • 6.
  • 7.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Sobre o CDTC Objetivo Geral O Projeto CDTC visa a promoção e o desenvolvimento de ações que incentivem a dissemina- ção de soluções que utilizem padrões abertos e não proprietários de tecnologia, em proveito do desenvolvimento social, cultural, político, tecnológico e econômico da sociedade brasileira. Objetivo Específico Auxiliar o Governo Federal na implantação do plano nacional de software não-proprietário e de código fonte aberto, identificando e mobilizando grupos de formadores de opinião dentre os servidores públicos e agentes políticos da União Federal, estimulando e incentivando o mercado nacional a adotar novos modelos de negócio da tecnologia da informação e de novos negócios de comunicação com base em software não-proprietário e de código fonte aberto, oferecendo treinamento específico para técnicos, profissionais de suporte e funcionários públicos usuários, criando grupos de funcionários públicos que irão treinar outros funcionários públicos e atuar como incentivadores e defensores dos produtos de software não proprietários e código fonte aberto, ofe- recendo conteúdo técnico on-line para serviços de suporte, ferramentas para desenvolvimento de produtos de software não proprietários e do seu código fonte livre, articulando redes de terceiros (dentro e fora do governo) fornecedoras de educação, pesquisa, desenvolvimento e teste de pro- dutos de software livre. Guia do aluno Neste guia, você terá reunidas uma série de informações importantes para que você comece seu curso. São elas: • Licenças para cópia de material disponível; • Os 10 mandamentos do aluno de Educação a Distância; • Como participar dos foruns e da wikipédia; • Primeiros passos. É muito importante que você entre em contato com TODAS estas informações, seguindo o roteiro acima. Licença Copyright ©2006, Instituto Nacional de Tecnologia da Informação (cdtc@iti.gov.br). 6
  • 8.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF É dada permissão para copiar, distribuir e/ou modificar este documento sob os termos da Licença de Documentação Livre GNU, Versão 1.1 ou qualquer versão posterior públicada pela Free Software Foundation; com o Capitulo Invariante SOBRE ESSA APOSTILA. Uma cópia da licença está inclusa na seção entitulada "Licença de Docu- mentação Livre GNU". Os 10 mandamentos do aluno de educação online • 1. Acesso à Internet: ter endereço eletrônico, um provedor e um equipamento adequado é pré-requisito para a participação nos cursos a distância; • 2. Habilidade e disposição para operar programas: ter conhecimentos básicos de Informá- tica é necessário para poder executar as tarefas; • 3. Vontade para aprender colaborativamente: interagir, ser participativo no ensino a distân- cia conta muitos pontos, pois irá colaborar para o processo ensino-aprendizagem pessoal, dos colegas e dos professores; • 4. Comportamentos compatíveis com a etiqueta: mostrar-se interessado em conhecer seus colegas de turma respeitando-os e se fazendo ser respeitado pelos mesmos; • 5. Organização pessoal: planejar e organizar tudo é fundamental para facilitar a sua revisão e a sua recuperação de materiais; • 6. Vontade para realizar as atividades no tempo correto: anotar todas as suas obrigações e realizá-las em tempo real; • 7. Curiosidade e abertura para inovações: aceitar novas idéias e inovar sempre; • 8. Flexibilidade e adaptação: requisitos necessário à mudança tecnológica, aprendizagens e descobertas; • 9. Objetividade em sua comunicação: comunicar-se de forma clara, breve e transparente é ponto - chave na comunicação pela Internet; • 10. Responsabilidade: ser responsável por seu próprio aprendizado. O ambiente virtual não controla a sua dedicação, mas reflete os resultados do seu esforço e da sua colaboração. Como participar dos fóruns e Wikipédia Você tem um problema e precisa de ajuda? Podemos te ajudar de 2 formas: A primeira é o uso dos fóruns de notícias e de dúvidas gerais que se distinguem pelo uso: . O fórum de notícias tem por objetivo disponibilizar um meio de acesso rápido a informações que sejam pertinentes ao curso (avisos, notícias). As mensagens postadas nele são enviadas a 7
  • 9.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF todos participantes. Assim, se o monitor ou algum outro participante tiver uma informação que interesse ao grupo, favor postá-la aqui. Porém, se o que você deseja é resolver alguma dúvida ou discutir algum tópico específico do curso. É recomendado que você faça uso do Fórum de dúvidas gerais que lhe dá recursos mais efetivos para esta prática. . O fórum de dúvidas gerais tem por objetivo disponibilizar um meio fácil, rápido e interativo para solucionar suas dúvidas e trocar experiências. As mensagens postadas nele são enviadas a todos participantes do curso. Assim, fica muito mais fácil obter respostas, já que todos podem ajudar. Se você receber uma mensagem com algum tópico que saiba responder, não se preocupe com a formalização ou a gramática. Responda! E não se esqueça de que antes de abrir um novo tópico é recomendável ver se a sua pergunta já foi feita por outro participante. A segunda forma se dá pelas Wikis: . Uma wiki é uma página web que pode ser editada colaborativamente, ou seja, qualquer par- ticipante pode inserir, editar, apagar textos. As versões antigas vão sendo arquivadas e podem ser recuperadas a qualquer momento que um dos participantes o desejar. Assim, ela oferece um ótimo suporte a processos de aprendizagem colaborativa. A maior wiki na web é o site "Wikipé- dia", uma experiência grandiosa de construção de uma enciclopédia de forma colaborativa, por pessoas de todas as partes do mundo. Acesse-a em português pelos links: • Página principal da Wiki - http://pt.wikipedia.org/wiki/ Agradecemos antecipadamente a sua colaboração com a aprendizagem do grupo! Primeiros Passos Para uma melhor aprendizagem é recomendável que você siga os seguintes passos: • Ler o Plano de Ensino e entender a que seu curso se dispõe a ensinar; • Ler a Ambientação do Moodle para aprender a navegar neste ambiente e se utilizar das ferramentas básicas do mesmo; • Entrar nas lições seguindo a seqüência descrita no Plano de Ensino; • Qualquer dúvida, reporte ao Fórum de Dúvidas Gerais. Perfil do Tutor Segue-se uma descrição do tutor ideal, baseada no feedback de alunos e de tutores. O tutor ideal é um modelo de excelência: é consistente, justo e profissional nos respectivos valores e atitudes, incentiva mas é honesto, imparcial, amável, positivo, respeitador, aceita as idéias dos estudantes, é paciente, pessoal, tolerante, apreciativo, compreensivo e pronto a ajudar. 8
  • 10.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF A classificação por um tutor desta natureza proporciona o melhor feedback possível, é crucial, e, para a maior parte dos alunos, constitui o ponto central do processo de aprendizagem.’ Este tutor ou instrutor: • fornece explicações claras acerca do que ele espera e do estilo de classificação que irá utilizar; • gosta que lhe façam perguntas adicionais; • identifica as nossas falhas, mas corrige-as amavelmente’, diz um estudante, ’e explica por- que motivo a classificação foi ou não foi atribuída’; • tece comentários completos e construtivos, mas de forma agradável (em contraste com um reparo de um estudante: ’os comentários deixam-nos com uma sensação de crítica, de ameaça e de nervossismo’) • dá uma ajuda complementar para encorajar um estudante em dificuldade; • esclarece pontos que não foram entendidos, ou corretamente aprendidos anteriormente; • ajuda o estudante a alcançar os seus objetivos; • é flexível quando necessário; • mostra um interesse genuíno em motivar os alunos (mesmo os principiantes e, por isso, talvez numa fase menos interessante para o tutor); • escreve todas as correções de forma legível e com um nível de pormenorização adequado; • acima de tudo, devolve os trabalhos rapidamente; 9
  • 11.
    Parte III GNU FreeDocumentation License 10
  • 12.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF (Traduzido pelo João S. O. Bueno através do CIPSGA em 2001) Esta é uma tradução não oficial da Licença de Documentação Livre GNU em Português Brasi- leiro. Ela não é publicada pela Free Software Foundation, e não se aplica legalmente a distribuição de textos que usem a GFDL - apenas o texto original em Inglês da GNU FDL faz isso. Entretanto, nós esperamos que esta tradução ajude falantes de português a entenderem melhor a GFDL. This is an unofficial translation of the GNU General Documentation License into Brazilian Por- tuguese. It was not published by the Free Software Foundation, and does not legally state the distribution terms for software that uses the GFDL–only the original English text of the GFDL does that. However, we hope that this translation will help Portuguese speakers understand the GFDL better. Licença de Documentação Livre GNU Versão 1.1, Março de 2000 Copyright (C) 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA É permitido a qualquer um copiar e distribuir cópias exatas deste documento de licença, mas não é permitido alterá-lo. INTRODUÇÃO O propósito desta Licença é deixar um manual, livro-texto ou outro documento escrito "livre"no sentido de liberdade: assegurar a qualquer um a efetiva liberdade de copiá-lo ou redistribui-lo, com ou sem modificações, comercialmente ou não. Secundariamente, esta Licença mantém para o autor e editor uma forma de ter crédito por seu trabalho, sem ser considerado responsável pelas modificações feitas por terceiros. Esta Licença é um tipo de "copyleft"("direitos revertidos"), o que significa que derivações do documento precisam ser livres no mesmo sentido. Ela complementa a GNU Licença Pública Ge- ral (GNU GPL), que é um copyleft para software livre. Nós fizemos esta Licença para que seja usada em manuais de software livre, por que software livre precisa de documentação livre: um programa livre deve ser acompanhado de manuais que provenham as mesmas liberdades que o software possui. Mas esta Licença não está restrita a manuais de software; ela pode ser usada para qualquer trabalho em texto, independentemente do assunto ou se ele é publicado como um livro impresso. Nós recomendamos esta Licença prin- cipalmente para trabalhos cujo propósito seja de introdução ou referência. APLICABILIDADE E DEFINIÇÕES Esta Licença se aplica a qualquer manual ou outro texto que contenha uma nota colocada pelo detentor dos direitos autorais dizendo que ele pode ser distribuído sob os termos desta Licença. O "Documento"abaixo se refere a qualquer manual ou texto. Qualquer pessoa do público é um 11
  • 13.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF licenciado e é referida como "você". Uma "Versão Modificada"do Documento se refere a qualquer trabalho contendo o documento ou uma parte dele, quer copiada exatamente, quer com modificações e/ou traduzida em outra língua. Uma "Seção Secundária"é um apêndice ou uma seção inicial do Documento que trata ex- clusivamente da relação dos editores ou dos autores do Documento com o assunto geral do Documento (ou assuntos relacionados) e não contém nada que poderia ser incluído diretamente nesse assunto geral (Por exemplo, se o Documento é em parte um livro texto de matemática, a Seção Secundária pode não explicar nada de matemática). Essa relação poderia ser uma questão de ligação histórica com o assunto, ou matérias relaci- onadas, ou de posições legais, comerciais, filosóficas, éticas ou políticas relacionadas ao mesmo. As "Seções Invariantes"são certas Seções Secundárias cujos títulos são designados, como sendo de Seções Invariantes, na nota que diz que o Documento é publicado sob esta Licença. Os "Textos de Capa"são certos trechos curtos de texto que são listados, como Textos de Capa Frontal ou Textos da Quarta Capa, na nota que diz que o texto é publicado sob esta Licença. Uma cópia "Transparente"do Documento significa uma cópia que pode ser lida automatica- mente, representada num formato cuja especificação esteja disponível ao público geral, cujos conteúdos possam ser vistos e editados diretamente e sem mecanismos especiais com editores de texto genéricos ou (para imagens compostas de pixels) programas de pintura genéricos ou (para desenhos) por algum editor de desenhos grandemente difundido, e que seja passível de servir como entrada a formatadores de texto ou para tradução automática para uma variedade de formatos que sirvam de entrada para formatadores de texto. Uma cópia feita em um formato de arquivo outrossim Transparente cuja constituição tenha sido projetada para atrapalhar ou de- sencorajar modificações subsequentes pelos leitores não é Transparente. Uma cópia que não é "Transparente"é chamada de "Opaca". Exemplos de formatos que podem ser usados para cópias Transparentes incluem ASCII sim- ples sem marcações, formato de entrada do Texinfo, formato de entrada do LaTex, SGML ou XML usando uma DTD disponibilizada publicamente, e HTML simples, compatível com os padrões, e projetado para ser modificado por pessoas. Formatos opacos incluem PostScript, PDF, formatos proprietários que podem ser lidos e editados apenas com processadores de texto proprietários, SGML ou XML para os quais a DTD e/ou ferramentas de processamento e edição não estejam disponíveis para o público, e HTML gerado automaticamente por alguns editores de texto com finalidade apenas de saída. A "Página do Título"significa, para um livro impresso, a página do título propriamente dita, mais quaisquer páginas subsequentes quantas forem necessárias para conter, de forma legível, o material que esta Licença requer que apareça na página do título. Para trabalhos que não tenham uma página do título, "Página do Título"significa o texto próximo da aparição mais proe- minente do título do trabalho, precedendo o início do corpo do texto. 12
  • 14.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF FAZENDO CÓPIAS EXATAS Você pode copiar e distribuir o Documento em qualquer meio, de forma comercial ou não comercial, desde que esta Licença, as notas de copyright, e a nota de licença dizendo que esta Licença se aplica ao documento estejam reproduzidas em todas as cópias, e que você não acres- cente nenhuma outra condição, quaisquer que sejam, às desta Licença. Você não pode usar medidas técnicas para obstruir ou controlar a leitura ou confecção de cópias subsequentes das cópias que você fizer ou distribuir. Entretanto, você pode aceitar com- pensação em troca de cópias. Se você distribuir uma quantidade grande o suficiente de cópias, você também precisa respeitar as condições da seção 3. Você também pode emprestar cópias, sob as mesmas condições colocadas acima, e também pode exibir cópias publicamente. FAZENDO CÓPIAS EM QUANTIDADE Se você publicar cópias do Documento em número maior que 100, e a nota de licença do Documento obrigar Textos de Capa, você precisará incluir as cópias em capas que tragam, clara e legivelmente, todos esses Textos de Capa: Textos de Capa da Frente na capa da frente, e Textos da Quarta Capa na capa de trás. Ambas as capas também precisam identificar clara e legivelmente você como o editor dessas cópias. A capa da frente precisa apresentar o título com- pleto com todas as palavras do título igualmente proeminentes e visíveis. Você pode adicionar outros materiais às capas. Fazer cópias com modificações limitadas às capas, tanto quanto estas preservem o título do documento e satisfaçam a essas condições, pode ser tratado como cópia exata em outros aspectos. Se os textos requeridos em qualquer das capas for muito volumoso para caber de forma legível, você deve colocar os primeiros (tantos quantos couberem de forma razoável) na capa verdadeira, e continuar os outros nas páginas adjacentes. Se você publicar ou distribuir cópias Opacas do Documento em número maior que 100, você precisa ou incluir uma cópia Transparente que possa ser lida automaticamente com cada cópia Opaca, ou informar, em ou com, cada cópia Opaca a localização de uma cópia Transparente completa do Documento acessível publicamente em uma rede de computadores, à qual o público usuário de redes tenha acesso a download gratuito e anônimo utilizando padrões públicos de protocolos de rede. Se você utilizar o segundo método, você precisará tomar cuidados razoavel- mente prudentes, quando iniciar a distribuição de cópias Opacas em quantidade, para assegurar que esta cópia Transparente vai permanecer acessível desta forma na localização especificada por pelo menos um ano depois da última vez em que você distribuir uma cópia Opaca (direta- mente ou através de seus agentes ou distribuidores) daquela edição para o público. É pedido, mas não é obrigatório, que você contate os autores do Documento bem antes de redistribuir qualquer grande número de cópias, para lhes dar uma oportunidade de prover você com uma versão atualizada do Documento. 13
  • 15.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF MODIFICAÇÕES Você pode copiar e distribuir uma Versão Modificada do Documento sob as condições das se- ções 2 e 3 acima, desde que você publique a Versão Modificada estritamente sob esta Licença, com a Versão Modificada tomando o papel do Documento, de forma a licenciar a distribuição e modificação da Versão Modificada para quem quer que possua uma cópia da mesma. Além disso, você precisa fazer o seguinte na versão modificada: A. Usar na Página de Título (e nas capas, se houver alguma) um título distinto daquele do Do- cumento, e daqueles de versões anteriores (que deveriam, se houvesse algum, estarem listados na seção "Histórico do Documento"). Você pode usar o mesmo título de uma versão anterior se o editor original daquela versão lhe der permissão; B. Listar na Página de Título, como autores, uma ou mais das pessoas ou entidades responsá- veis pela autoria das modificações na Versão Modificada, conjuntamente com pelo menos cinco dos autores principais do Documento (todos os seus autores principais, se ele tiver menos que cinco); C. Colocar na Página de Título o nome do editor da Versão Modificada, como o editor; D. Preservar todas as notas de copyright do Documento; E. Adicionar uma nota de copyright apropriada para suas próprias modificações adjacente às outras notas de copyright; F. Incluir, imediatamente depois das notas de copyright, uma nota de licença dando ao público o direito de usar a Versão Modificada sob os termos desta Licença, na forma mostrada no tópico abaixo; G. Preservar nessa nota de licença as listas completas das Seções Invariantes e os Textos de Capa requeridos dados na nota de licença do Documento; H. Incluir uma cópia inalterada desta Licença; I. Preservar a seção entitulada "Histórico", e seu título, e adicionar à mesma um item dizendo pelo menos o título, ano, novos autores e editor da Versão Modificada como dados na Página de Título. Se não houver uma sessão denominada "Histórico"no Documento, criar uma dizendo o título, ano, autores, e editor do Documento como dados em sua Página de Título, então adicionar um item descrevendo a Versão Modificada, tal como descrito na sentença anterior; J. Preservar o endereço de rede, se algum, dado no Documento para acesso público a uma cópia Transparente do Documento, e da mesma forma, as localizações de rede dadas no Docu- mento para as versões anteriores em que ele foi baseado. Elas podem ser colocadas na seção "Histórico". Você pode omitir uma localização na rede para um trabalho que tenha sido publicado pelo menos quatro anos antes do Documento, ou se o editor original da versão a que ela se refira der sua permissão; K. Em qualquer seção entitulada "Agradecimentos"ou "Dedicatórias", preservar o título da 14
  • 16.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF seção e preservar a seção em toda substância e fim de cada um dos agradecimentos de contri- buidores e/ou dedicatórias dados; L. Preservar todas as Seções Invariantes do Documento, inalteradas em seus textos ou em seus títulos. Números de seção ou equivalentes não são considerados parte dos títulos da seção; M. Apagar qualquer seção entitulada "Endossos". Tal sessão não pode ser incluída na Versão Modificada; N. Não reentitular qualquer seção existente com o título "Endossos"ou com qualquer outro título dado a uma Seção Invariante. Se a Versão Modificada incluir novas seções iniciais ou apêndices que se qualifiquem como Seções Secundárias e não contenham nenhum material copiado do Documento, você pode optar por designar alguma ou todas aquelas seções como invariantes. Para fazer isso, adicione seus títulos à lista de Seções Invariantes na nota de licença da Versão Modificada. Esses títulos preci- sam ser diferentes de qualquer outro título de seção. Você pode adicionar uma seção entitulada "Endossos", desde que ela não contenha qual- quer coisa além de endossos da sua Versão Modificada por várias pessoas ou entidades - por exemplo, declarações de revisores ou de que o texto foi aprovado por uma organização como a definição oficial de um padrão. Você pode adicionar uma passagem de até cinco palavras como um Texto de Capa da Frente , e uma passagem de até 25 palavras como um Texto de Quarta Capa, ao final da lista de Textos de Capa na Versão Modificada. Somente uma passagem de Texto da Capa da Frente e uma de Texto da Quarta Capa podem ser adicionados por (ou por acordos feitos por) qualquer entidade. Se o Documento já incluir um texto de capa para a mesma capa, adicionado previamente por você ou por acordo feito com alguma entidade para a qual você esteja agindo, você não pode adicionar um outro; mas você pode trocar o antigo, com permissão explícita do editor anterior que adicionou a passagem antiga. O(s) autor(es) e editor(es) do Documento não dão permissão por esta Licença para que seus nomes sejam usados para publicidade ou para assegurar ou implicar endossamento de qualquer Versão Modificada. COMBINANDO DOCUMENTOS Você pode combinar o Documento com outros documentos publicados sob esta Licença, sob os termos definidos na seção 4 acima para versões modificadas, desde que você inclua na com- binação todas as Seções Invariantes de todos os documentos originais, sem modificações, e liste todas elas como Seções Invariantes de seu trabalho combinado em sua nota de licença. O trabalho combinado precisa conter apenas uma cópia desta Licença, e Seções Invariantes Idênticas com multiplas ocorrências podem ser substituídas por apenas uma cópia. Se houver múltiplas Seções Invariantes com o mesmo nome mas com conteúdos distintos, faça o título de 15
  • 17.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF cada seção único adicionando ao final do mesmo, em parênteses, o nome do autor ou editor origianl daquela seção, se for conhecido, ou um número que seja único. Faça o mesmo ajuste nos títulos de seção na lista de Seções Invariantes nota de licença do trabalho combinado. Na combinação, você precisa combinar quaisquer seções entituladas "Histórico"dos diver- sos documentos originais, formando uma seção entitulada "Histórico"; da mesma forma combine quaisquer seções entituladas "Agradecimentos", ou "Dedicatórias". Você precisa apagar todas as seções entituladas como "Endosso". COLETÂNEAS DE DOCUMENTOS Você pode fazer uma coletânea consitindo do Documento e outros documentos publicados sob esta Licença, e substituir as cópias individuais desta Licença nos vários documentos com uma única cópia incluida na coletânea, desde que você siga as regras desta Licença para cópia exata de cada um dos Documentos em todos os outros aspectos. Você pode extrair um único documento de tal coletânea, e distribuí-lo individualmente sob esta Licença, desde que você insira uma cópia desta Licença no documento extraído, e siga esta Licença em todos os outros aspectos relacionados à cópia exata daquele documento. AGREGAÇÃO COM TRABALHOS INDEPENDENTES Uma compilação do Documento ou derivados dele com outros trabalhos ou documentos se- parados e independentes, em um volume ou mídia de distribuição, não conta como uma Ver- são Modificada do Documento, desde que nenhum copyright de compilação seja reclamado pela compilação. Tal compilação é chamada um "agregado", e esta Licença não se aplica aos outros trabalhos auto-contidos compilados junto com o Documento, só por conta de terem sido assim compilados, e eles não são trabalhos derivados do Documento. Se o requerido para o Texto de Capa na seção 3 for aplicável a essas cópias do Documento, então, se o Documento constituir menos de um quarto de todo o agregado, os Textos de Capa do Documento podem ser colocados em capas adjacentes ao Documento dentro do agregado. Senão eles precisarão aparecer nas capas de todo o agregado. TRADUÇÃO Tradução é considerada como um tipo de modificação, então você pode distribuir traduções do Documento sob os termos da seção 4. A substituição de Seções Invariantes por traduções requer uma permissão especial dos detentores do copyright das mesmas, mas você pode incluir traduções de algumas ou de todas as Seções Invariantes em adição às versões orignais dessas Seções Invariantes. Você pode incluir uma tradução desta Licença desde que você também in- clua a versão original em Inglês desta Licença. No caso de discordância entre a tradução e a 16
  • 18.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF versão original em Inglês desta Licença, a versão original em Inglês prevalecerá. TÉRMINO Você não pode copiar, modificar, sublicenciar, ou distribuir o Documento exceto como expres- samente especificado sob esta Licença. Qualquer outra tentativa de copiar, modificar, sublicen- ciar, ou distribuir o Documento é nula, e resultará automaticamente no término de seus direitos sob esta Licença. Entretanto, terceiros que tenham recebido cópias, ou direitos de você sob esta Licença não terão suas licenças terminadas, tanto quanto esses terceiros permaneçam em total acordo com esta Licença. REVISÕES FUTURAS DESTA LICENÇA A Free Software Foundation pode publicar novas versões revisadas da Licença de Documen- tação Livre GNU de tempos em tempos. Tais novas versões serão similares em espirito à versão presente, mas podem diferir em detalhes ao abordarem novos porblemas e preocupações. Veja http://www.gnu.org/copyleft/. A cada versão da Licença é dado um número de versão distinto. Se o Documento especificar que uma versão particular desta Licença "ou qualquer versão posterior"se aplica ao mesmo, você tem a opção de seguir os termos e condições daquela versão específica, ou de qualquer versão posterior que tenha sido publicada (não como rascunho) pela Free Software Foundation. Se o Documento não especificar um número de Versão desta Licença, você pode escolher qualquer versão já publicada (não como rascunho) pela Free Software Foundation. ADENDO: Como usar esta Licença para seus documentos Para usar esta Licença num documento que você escreveu, inclua uma cópia desta Licença no documento e ponha as seguintes notas de copyright e licenças logo após a página de título: Copyright (c) ANO SEU NOME. É dada permissão para copiar, distribuir e/ou modificar este documento sob os termos da Licença de Documentação Livre GNU, Versão 1.1 ou qualquer versão posterior publicada pela Free Soft- ware Foundation; com as Seções Invariantes sendo LISTE SEUS TÍTULOS, com os Textos da Capa da Frente sendo LISTE, e com os Textos da Quarta-Capa sendo LISTE. Uma cópia da li- cença está inclusa na seção entitulada "Licença de Documentação Livre GNU". Se você não tiver nenhuma Seção Invariante, escreva "sem Seções Invariantes"ao invés de dizer quais são invariantes. Se você não tiver Textos de Capa da Frente, escreva "sem Textos de Capa da Frente"ao invés de "com os Textos de Capa da Frente sendo LISTE"; o mesmo para os Textos da Quarta Capa. Se o seu documento contiver exemplos não triviais de código de programas, nós recomenda- mos a publicação desses exemplos em paralelo sob a sua escolha de licença de software livre, 17
  • 19.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF tal como a GNU General Public License, para permitir o seu uso em software livre. 18
  • 20.
  • 21.
    Capítulo 1 O queé a linguagem 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 conside- rada 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. 20
  • 22.
    Capítulo 2 Plano deensino 2.1 Objetivo Introduzir aos técnicos e programadores a linguagem de programação C padrão ANSI. 2.2 Público Alvo Técnicos e Programadores que desejam trabalhar com a linguagem C. 2.3 Pré-requisitos Os usuários deverão ter conhecimento básico acerca da lógica de programação. 2.4 Descrição O curso de Linguagem de Programação C será realizado na modalidade EAD e utilizará a plataforma Moodle como ferramenta de aprendizagem. Ele é composto por módulos de apren- dizado que serão dados no início de cada semana e um módulo de avaliação que será dado na última. O material didático estará disponível on-line de acordo com as datas pré-estabelecidas no calendário. A versão utilizada para o compilador (o GCC) será a 4.0.4 2.5 Metodologia O curso está dividido da seguinte maneira: 2.6 Cronograma • Semana 1 - Introdução à linguagem C; • Semana 2 - Código estruturado em C e ponteiros; • Semana 3 - Abstração de dados; 21
  • 23.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • Semana 4 - Estruturação e manipulação avançada do código e dos arquivos; • Avaliação - Avaliação de aprendizagem e avaliação final. As lições contêm o conteúdo principal. Elas poderão ser acessadas quantas vezes forem ne- cessárias, desde que esteja dentro da semana programada. Ao final de uma lição, você receberá uma nota de acordo com o seu desempenho. Responda com atenção às perguntas de cada li- ção, pois elas serão consideradas na sua nota final. Caso sua nota numa determinada lição seja menor que 6.0, sugerimos que você faça novamente esta lição. Ao final do curso será disponibilizada a avaliação referente ao curso. Tanto as notas das lições quanto a da avaliação serão consideradas para a nota final. Todos os módulos ficarão visíveis para que possam ser consultados durante a avaliação final. Aconselhamos a leitura da "Ambientação do Moodle"para que você conheça a plataforma de En- sino a Distância, evitando dificuldades advindas do "desconhecimento"sobre a mesma. Os instrutores estarão à sua disposição ao longo de todo curso. Qualquer dúvida deverá ser enviada ao fórum. Diariamente os monitores darão respostas e esclarecimentos. 2.7 Programa O curso da linguagem C oferecerá o seguinte conteúdo: • Lição 1 - Introdução teórica; • Lição 2 - Elementos léxicos e sintáticos da linguagem C, parte 1; • Lição 3 - Elementos léxicos e sintáticos da linguagem C, parte 2; • Lição 4 - Elementos léxicos e sintáticos da linguagem C, parte 3; • Lição 5 - Manipulação de arquivos e entradas/saídas em C; • Lição 6 - Recursividade; • Lição 7 - Manipulação de ponteiros e tipos avançados de dados; • Lição 8 - Estruturas de dados, parte 1; • Lição 9 - Estruturas de dados, parte 2; • Lição 10 - Dividindo seu programa em diversos arquivos fontes; • Lição 11 - Manipulação avançada de arquivos. 2.8 Avaliação Em cache - Páginas Semelhantes Toda a avaliação será feita on-line. Aspectos a serem considerados na avaliação: • Iniciativa e autonomia no processo de aprendizagem e de produção de conhecimento; 22
  • 24.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • Capacidade de pesquisa e abordagem criativa na solução dos problemas apresentados. Instrumentos de avaliação: • Participação ativa nas atividades programadas; • Avaliação ao final do curso; • O participante fará várias avaliações referente ao conteúdo do curso. Para a aprovação e obtenção do certificado o participante deverá obter nota final maior ou igual a 6.0 de acordo com a fórmula abaixo: • Nota Final = ((ML x 7) + (AF x 3)) / 10 = Média aritmética das lições • AF = Avaliações . 2.9 Bibliografia • Curso O Linux : http://www.coltec.ufmg.br/ info/cursoC_olinux/curso_c_htm.htm • cplusplus.com : http://www.cplusplus.com/ 23
  • 25.
    Capítulo 3 Introdução àlinguagem C 3.1 Introdução teórica 3.1.1 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, 24
  • 26.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 é im- posto 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 Byte- code). Na concepção da linguagem é que se decide se ela vai ser 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 objeti- vos. 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 pro- grama 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 lingua- gem C. Eles são para fins ilustrativos. Os interessados podem tentar compilar o código ou analisar o que eles fazem. 3.1.2 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) en- tre 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 25
  • 27.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF sistema e que ele possa trabalhar com maior grau de abstração (se preocupar menos com pro- blemas 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 esca- lonadores 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 es- perando 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 sistemas 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 me- ramente os "bits". O interpretador de comandos 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 interpre- tador de comandos é conhecido no mundo Linux como SHELL (pode variar para outros sistemas). 3.1.3 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 { 26
  • 28.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF p2 = p - 1; } p = p1 + ((p2 - p1)/2); } return NULL; } 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 (des- crito 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 pa- dronizaçã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. Atual- mente, 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 por- tabilidade) é 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. 3.1.4 Características 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 hard- ware; 2. Foi projetado para ser usado por programadores especializados interessados em geração de códigos compactos e eficientes; 27
  • 29.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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; 12. 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. 3.1.5 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); 28
  • 30.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 com- pilaçã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. 1. O compilador realiza a "pré-compilação", ou seja, processa comandos especiais direciona- dos 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ân- ticos 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 compila- dores 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 29
  • 31.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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), ge- rando 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. $ gcc -o destino fonte.c O seu programa se chamará destino e será o derivado do fonte chamado 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. 3.1.6 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[] ) { 30
  • 32.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 basica- mente 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 mate- má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. 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. 31
  • 33.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 pro- duto 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 32
  • 34.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 co- mando. 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 Pas- cal), o último comando não precisa desse elemento, pois o último comando não tem algo a seguir para precisar separar. 3.2 Elementos léxicos e sintáticos da linguagem C, parte 1 3.2.1 Palavras Reservadas Um dos elementos léxicos mais importantes de uma linguagem de programação são as pa- lavras reservadas. As palavras 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 ele- mentos 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 lingua- gem. 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 determi- nadas 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 desig- nados (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 33
  • 35.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • 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 34
  • 36.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Todo conjunto de palavras reservadas acima são o conjunto das instruções básicas do C. Apa- rentemente, 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 encon- trará nenhuma versão do C sem nenhuma delas. Algumas são até tratadas como parte da lin- guagem por serem padronizadas. 3.2.2 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ús- culas 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 é uti- lizado 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á armaze- nado. 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ên- cias, 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 asso- ciados 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. 35
  • 37.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Declaração de uma variável 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 inici- alizado 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; 3.2.3 Operadores Aritméticos e de Atribuição 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. 36
  • 38.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 correta- mente 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, para corretamente 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. 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=%fn",intNumero1, floatNumero1); intNumero2 = intNumero1 + floatNumero1; printf("inteiro = inteiro + flutuante: %dnn",intNumero2); intNumero1 = 1; floatNumero1 = 2.5; 37
  • 39.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF printf("intNumero1=%d e floatNumero1=%fn",intNumero1, floatNumero1); intNumero2 = intNumero1 * floatNumero1; printf("inteiro = inteiro * flutuante: %dnn",intNumero2); intNumero1 = 10; floatNumero1 = 3; printf("intNumero1=%d e floatNumero1=%fn",intNumero1, floatNumero1); intNumero2 = intNumero1 / floatNumero1; printf("inteiro = inteiro / futuante: %dnn",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 funci- onalidade 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 */ 38
  • 40.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Métodos interessantes de atribuição 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 + varNumero2 varNumero1 -= varNumero2 equivale à varNumero1 = varNumero1 - varNumero2 varNumero1 *= varNumero2 equivale à varNumero1 = varNumero1 * varNumero2 varNumero1 /= 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 equi- vale à 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. 3.2.4 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: 39
  • 41.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • 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 univer- sal. 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 desconsidera- dos. 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; • 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; 40
  • 42.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • 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 armazena- mento 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 ex- terno 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. 3.2.5 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 das variá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() { 41
  • 43.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF comando1; comando2; ... 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 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. 3.3 Elementos léxicos e sintáticos da linguagem C, parte 2 3.3.1 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... perplexo 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; 42
  • 44.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • 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 mate- má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 reapro- veitamento 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 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 lingua- gem (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: 43
  • 45.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 sis- tema 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 compilado- res 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 portabi- lidade (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. 44
  • 46.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 detalha- damente), 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 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; 45
  • 47.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 3.3.2 Bibliotecas Já que mostramos o que é uma função, aproveitamos para apresentarmos o que é uma bibli- oteca. 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 conveni- entes. 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 46
  • 48.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • termio.h • aliases.h • expat_external.h • jmorecfg.h • pthread.h • termios.h • alloca.h • expat.h • jpegint.h • pty.h • tgmath.h • a.out.h • fcntl.h • jpeglib.h • pwd.h • thread_db.h • argp.h • features.h • langinfo.h • re_comp.h • tiffconf.h • argz.h • fenv.h • lastlog.h • regex.h • tiff.h • ar.h • FlexLexer.h • libgen.h 47
  • 49.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • regexp.h • tiffio.h • assert.h • fmtmsg.h • libintl.h • resolv.h • tiffvers.h • autosprintf.h • fnmatch.h • libio.h • sched.h • time.h • byteswap.h • fpu_control.h • limits.h • search.h • tls.h • complex.h • fstab.h • link.h • semaphore.h • ttyent.h • cpio.h • ft2build.h • locale.h • setjmp.h • ucontext.h • crypt.h • fts.h 48
  • 50.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • malloc.h • sgtty.h • ulimit.h • ctype.h • ftw.h • math.h • shadow.h • unistd.h • dialog.h • _G_config.h • mcheck.h • signal.h • ustat.h • dirent.h • gconv.h • memory.h • spawn.h • utime.h • dlfcn.h • getopt.h • mntent.h • stab.h • utmp.h • dlg_colors.h • gettext-po.h • monetary.h • stdint.h • utmpx.h • dlg_config.h 49
  • 51.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • glob.h • mqueue.h • stdio_ext.h • values.h • dlg_keys.h • gnu-versions.h • netdb.h • stdio.h • wait.h • dts.h • grp.h • nl_types.h • stdlib.h • wchar.h • elf.h • gsm.h • nss.h • string.h • wctype.h • endian.h • iconv.h • obstack.h • strings.h • wordexp.h • envz.h • ieee754.h • oss-redir.h • stropts.h • xlocale.h 50
  • 52.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • err.h • ifaddrs.h • paths.h • syscall.h • zconf.h • errno.h • initreq.h • pngconf.h • sysexits.h • zlib.h • error.h • inttypes.h • png.h • syslog.h • execinfo.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 51
  • 53.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 (ex- plicaçõ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/): #include <NOME_DA_BIBLIOTECA.h> Para incluir uma biblioteca pessoal: #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 so- bre 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. 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: 52
  • 54.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF /*************************************************************/ /* 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 é o exemplo número %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..."); } Exemplo de um programa 3.3.3 Operadores Lógicos e de Comparação 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 preci- samos 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: 53
  • 55.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • > : 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. Intuiti- vamente, podemos ver que: 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 sabe- mos 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 54
  • 56.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 aplica- dos 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, traba- lhamos 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á, es- tã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ía- mos 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 55
  • 57.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • «: 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 "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. 56
  • 58.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 3.3.4 Controle de Fluxo Vamos agora introduzir à nossa lista de ferramentas do C algumas das que são mais utiliza- das 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 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. 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 57
  • 59.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 facil- mente 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. 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: 58
  • 60.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF int main() { /* Atribui os valores às variáveis */ intNumero1 = 5; intNumero2 = 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 condi- cional 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: 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 bi- ná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 59
  • 61.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 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 comandos */ if ( ( (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! 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 pro- grama 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; 60
  • 62.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF while (intNUmero <= 100) { printf("%d ",intNumero); ntNumero = 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 incremen- tada, 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. 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. É im- portante 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} 3.4 Elementos léxicos e sintáticos da linguagem C, parte 3 3.4.1 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 61
  • 63.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 #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]); 62
  • 64.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF } 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: %.2fn",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 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, precisa- mos 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 exata- mente à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. 63
  • 65.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 3.4.2 Strings Vamos ver agora uma parte bastante importante do C. Já comentamos antes que introduzi- mos 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 es- pecí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 peri- gosa. 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 conseguir- mos, 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 me- mó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, es- tamos 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 con- forme 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. 64
  • 66.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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á 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 carac- teres 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. 65
  • 67.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 3.4.3 Introdução ao uso de ponteiros Vamos ver agora o que é uma das partes mais úteis da linguagem C. Os ponteiros são res- ponsá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 utili- zar 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. 66
  • 68.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF float calculaMedia3 (float * vetNumeros); 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; 67
  • 69.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 opera- dor ’*’. 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; /* Imprime: "O primeiro elemento é o 1" */ printf("O primeiro elemento é o %dn", *ptrNumero); /* Incrementando o ponteiro */ ptrNumero += 1; /* Imprime : "O segundo elmento é o 2" */ printf("O segundo elemento é o %dn" *ptrNumero); /* Imprime de 1 a 100 */ for (i=0 ; i<100 ; i++) printf("O %do elemento é %dn", i+1, *(ptrNumero+i)); Vamos analisar algumas das operações feitas: 68
  • 70.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 3.4.4 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 ope- raçã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. 69
  • 71.
    Capítulo 4 Código estruturadoem C e ponteiros 4.1 Manipulação de arquivos e entradas/saídas em C 4.1.1 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 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 fmachado@cdtc.org.br.textbackslash{}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 */ 70
  • 72.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 exem- plo 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 • %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. 71
  • 73.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF • %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 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. As- sim 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 carac- teres 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. 72
  • 74.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 4.1.2 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 da- dos em geral que estejam relacionados ao programa de alguma forma. Podemos realizar persis- tê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 - confi- guraçõ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. 73
  • 75.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 pos- teriormente. 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() { 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 negadan"); /* 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 74
  • 76.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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, 75
  • 77.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 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 exata- mente 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 saber- mos 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: 76
  • 78.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF n = 0; while (!feof(arqArquivo1)) { fgetc (pFile); n++; } fclose (pFile); printf ("Total de bytes lidos: %dn", 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 pratica- mente qualquer coisa. Seu protótipo é: int fprintf ( FILE * stream, const char * format, ... ); Vamos ver um exemplo: fprintf(arqArquivo1, "%s - %dtextbackslash{}n", strNome, intDiasTrabalhados); 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 ); 77
  • 79.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 4.2 Recursividade 4.2.1 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 tor- nar 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! é 1 passo 11: o valor de 2! é 2 * 1 = 2 passo 12: o valor de 3! é 3 * 2 = 6 passo 13: o valor de 4! é 4 * 6 = 24 passo 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 recur- 78
  • 80.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF sã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)! 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 processa- mento 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 abor- dado 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 79
  • 81.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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)); } 4.3 Manipulação de ponteiros e tipos avançados de dados 4.3.1 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 uti- lizando 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 { 80
  • 82.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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: %.2fn", floatMedia); 81
  • 83.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF return(0); } É 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; }; 82
  • 84.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF Criando a struct dessa forma a leitura das nota do aluno seria feita com um código bem mais elegante, assim: 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 estiver- mos 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: 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: 83
  • 85.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 ope- rador ’->’ para o ponteiro varAluno e o operador ’.’ para a estrutura endereço. 4.3.2 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 pro- grama 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 va- riá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 poder que as variáveis dinâmicas dão ao 84
  • 86.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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: 85
  • 87.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 me- mó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 elemen- tos do tipo inteiro, lembrando que o 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 estatica- mente. 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: 86
  • 88.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF *(vtrInteiros+i) = 20; Esse comando diz que queremos que o espaço de memória i*sizeof(int) bytes após o ende- reç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. Va- mos 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 co- luna 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: 87
  • 89.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF #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 com- pilaçã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; } } 88
  • 90.
    Capítulo 5 Abstração dedados 5.1 Estruturas de dados, parte 1 5.1.1 Pilhas Vamos começar a falar essa semana sobre uma parte bastante importante de qualquer lin- guagem 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 necessi- dades 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 pon- teiros. 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 es- colha 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 envol- vido, 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 89
  • 91.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 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 simples- mente 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ó preci- samos 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 alo- cação dinâmica free e malloc, vamos ter que incluir o cabeçalho stdlib, responsável por manter 90
  • 92.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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); } 91
  • 93.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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]); 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. 5.1.2 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. 92
  • 94.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 93
  • 95.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF que lidar com mais de um caso. Vamos também precisar de uma função que cria um elemento antes de 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 estru- tura 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 94
  • 96.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 mo- tivo, 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 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. 5.1.3 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 95
  • 97.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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; 96
  • 98.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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; 97
  • 99.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 5.1.4 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 aproximar- mos 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 98
  • 100.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 99
  • 101.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 5.2 Estruturas de dados, parte 2 5.2.1 Á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 árvo- res 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 as- sim, 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, va- mos 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 100
  • 102.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF de uma árvore montada: 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 an- tes 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 va- lor, 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 ár- vore, 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 mole- culares 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 es- sas 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 101
  • 103.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 /* 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); 102
  • 104.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF /* 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 */ 103
  • 105.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF if (operador == -1) return(NULL); if ((entrada[operador] == DIVISAO) || (entrada[operador] == MULTIPLICACAO) || (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) { 104
  • 106.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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, poderíamos 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 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, precisa- mos da tabela ASCII para converter caracteres em uma variável do tipo inteiro. Além disso, 105
  • 107.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 5.2.2 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, depen- dendo 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, po- demos 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 utiliza- das 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. Va- mos começar? O primeiro conceito que temos que ver é o próprio conceito de grafo: Um grafo é, essencial- mente, 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 pon- tos, 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 direcio- nado é um grafo que diferencia um arco entre um nó A e um outro nó B - para ele os dois nós não 106
  • 108.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 possibili- dade 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 ou- tro 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 apren- demos 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 signi- fica 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: 107
  • 109.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF #include <stdio.h> #include <stdlib.h> #define TAMANHO 12 int mtrAdjacencia[TAMANHO][TAMANHO]; int main() { 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 PESOn"); 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: 108
  • 110.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 0 200 0 0 0 200 100 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 associ- ado à 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 ori- gem para origem) e vtrDistancia[i] como sendo INFINITO para todos os outros nós, indi- cando 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 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. 109
  • 111.
    Capítulo 6 Estruturação emanipulação avançada do código e dos arquivos 6.1 Dividindo seu programa em diversos arquivos fontes 6.1.1 Contole 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 st- dio.h. Como já vimos, o ".h"significa que o arquivo carrega headers, ou mais precisamente ca- beç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 pro- 110
  • 112.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF grama 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 implemen- tada 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 prepa- rado. 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 real- mente 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 preci- sarmos dessa função. Vamos começar com o arquivo media.h: /* 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); } 111
  • 113.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF o 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 utili- zando 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 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 $ gcc *.o -o binario.out $ ./binario.out 112
  • 114.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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. 6.1.2 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 pou- cos 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 manual- mente 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 im- portante 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 utilizado CC = gcc # Lista de arquivos código-objeto OBJ = main.o media.o # Arquivo binário BIN = binario.out # Opções a serem passadas ao compilador FLAGS = -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> 113
  • 115.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 $(CONS- TANTE) 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 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 6.2 Manipulação avançada de arquivos 6.2.1 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 pontei- ros 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. 114
  • 116.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF 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 in- teligente 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: SEEK_SET = Início do arquivo SEEK_CUR = Posição atual do arquivo SEEK_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; 115
  • 117.
    CDTC Centro deDifusão de Tecnologia e Conhecimento Brasil/DF /* 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". 116