Prévia do material em texto
Manual da Linguagem:
C
Luis Fernando Espinosa Cocian
Engenheiro Eletricista
Mestre em Engenharia Elétrica
Professor de Engenharia Elétrica
Universidade Luterana do Brasil
2
No interesse da difusão do conhecimento e da cultura, o autor envidou o máximo esforço para localizar
os detentores dos direitos autorais de todo o material utilizado, dispondo-se a possíveis acertos
posteriores caso, inadvertidamente, tenha sido omitida a identificação de algum deles.
Copyright © 2004 by Luis Fernando Espinosa Cocian
1ª Edição 2004
Capa: Luis Fernando Espinosa Cocian
Revisão Técnica: Dr. Eng. Eletricista Valner João Brusamarello
Revisão da Língua Portuguesa: Licenciada em Letras Leonice Lesina Espinosa
Projeto Gráfico e Editoração: Roseli Menzen
Luis Fernando Espinosa Cocian nasceu em Montevidéu, Uruguai, no ano de 1970. Formou-se em
Engenharia Elétrica na Universidade Federal do Rio Grande do Sul (UFRGS) em 1992. Recebeu o
grau de Mestre em Engenharia na UFRGS em 1995 na área de Instrumentação Eletroeletrônica.
Desenvolve as suas atividades profissionais como diretor do curso de Engenharia Elétrica da Ulbra. na
cidade de Canoas, e como sócio-diretor da empresa Mater Matris Technae Ltda.. em Porto Alegre.
Ministra as disciplinas de Instrumentação e Introdução a Engenharia Elétrica no curso de Engenharia
Elétrica da Ulbra. Nos seus momentos livres, gosta de ler sobre os mais variados assuntos,
especialmente astronomia, biologia, filosofia e medicina.
Dados Internacionais de Catalogação na Publicação (CIP)
Bibliotecária Responsável: Ana Lígia Trindade CRB/10-1235
Dados técnicos do livro
Fontes: Carmina, Lucida Casual
Papel: Offset 75 (miolo) e Supremo 240g (capa)
Medidas: 16 cm x 23 cm
Impresso na Gráfica da ULBRA
Julho /2004
3
Dedicatória
A minha querida mãe Belén.
4
Agradecimentos
Espero ter tempo suficiente na minha vida para poder agradecer:
A minha esposa Leonice pela ajuda constante e pelos dias de trabalho que
levou para revisar este texto.
Aos meus queridos professores pelo conhecimento transmitido.
Aos meus queridos alunos pela chance de aprender com eles.
Ao colega Valner João Brusamarello pela grande ajuda na revisão técnica dos
manuscritos originais.
Ao nosso pequeno “pero” grande time de professores do curso de Engenharia
Elétrica da Ulbra: Adriane Parraga, Alexandre Balbinot, Dalton Vidor,
Marilia Amaral da Silveira, Miriam Noemi Cáceres Villamayor, Rosa Leamar
Dias Blanco, João Carlos Vernetti dos Santos, Marcelo Barbedo, Marilaine de
Fraga Santana, Julio Cabrera, Augusto Alexandre Durganti de Matos, Paulo
César Cardoso Godoy, Valner João Brusamarello e Milton Zaro, pelo
companheirismo e amizade recebidos ao longo destes anos, e que com o seu
trabalho me fazem trabalhar menos.
Aos nossos queridos desenvolvedores da interface DevC++: Colin Laplace,
Mike Berg, e Hongli Lai, da Bloodshed Software; aos desenvolvedores do
compilador Mingw: Mumit Khan, Jan Jaap van der Heidjen, Colin Hendrix e
também aos demais programadores GNU, que permitiram incluir o
compilador no CD anexo sem custos, e que universalizam o acesso às
tecnologias e promovem o software livre.
Ao nosso prezado Pró-Reitor de Graduação, Nestor Luiz João Beck e à nossa
querida diretora do Núcleo de Avaliações Externas, professora Delzimar da
Costa Lima, pelo apoio recebido.
Aos meus bebês, Linda, Katherine e Marx, pela alegria e carinho.
5
Prefácio
A maioria dos livros de programação em linguagem C enfoca os temas dos
seus capítulos no estudo básico da linguagem e nas suas palavras-chave,
utilizando exemplos simples de processamento da informação. Esses livros
têm como público alvo, pessoas que desejam obter uma noção básica da
linguagem. Por isso, são um pouco deficientes no que se refere à
especificação e planejamento de projetos de software otimizado e ao acesso
ao hardware; temas de conhecimento indispensável para engenheiros e
técnicos da área eletroeletrônica, e especialmente, para aqueles profissionais
que trabalham no desenvolvimento de sistemas de aquisição de dados,
controle automático, comunicação de dados e de processamento digital de
sinais e em sistemas de tempo real.
A lacuna existente de livros de programação que enfatizem o controle do
hardware e a otimização dos recursos, motivou a recopilação de ideias e
projetos elaborados ao longo de anos nas salas de aula, de forma a repassar
essa informação aos técnicos, estudantes e engenheiros que estão iniciando a
aprendizagem da linguagem C.
Este livro aborda a programação utilizando como hardware o PC IBM
compatível com sistema operacional MSDOS® e Windows®, compiladores
Dev C++ da Bloodsheed Software (licença pública GNU), TurboC® da
Borland Inc.(Inprise Inc.) e Microsoft Visual C++® da Microsoft Inc.
Também, são mostrados alguns exemplos utilizando os compiladores Franklin
C® (para a família de microcontroladores 8x51) e PCW (CCS para
microcontroladores PIC da Microchip®). O leitor não terá problemas de
adaptação na utilização de outros sistemas operacionais e compiladores, tais
como os utilizados em ambientes Linux (ou Unix). A linguagem de
programação C é padronizada, mas a sua utilização pode diferir em alguns
pontos quando for utilizada em compiladores para plataformas diferentes de
software e hardware. Nesses casos deverão ser estudadas as modificações a
serem efetuadas no código fonte gerado por cada compilador.
Os exemplos de programação foram escolhidos de forma que estejam
adequados ao escopo deste livro. Nos anexos foi colocada a informação
referente às portas de comunicação do PC, chips de suporte e outras
informações consideradas relevantes. Alguns problemas requerem pequenas
montagens em hardware, cuja implementação também é mostrada através de
diagramas esquemáticos.
O software Dev C++ foi incluído no CDROM que acompanha este livro.
Upgrades e novas versões deste software podem ser obtidas, sem custo, no
6
sítio da Bloodshed Software em: www.bloodshed.net
Os códigos em formato texto e outras informações úteis podem ser acessados
no meu sítio pessoal em
www.cocian.synthasite.com/
Os capítulos deste livro foram organizados da seguinte maneira: os capítulos
1 a 4 fornecem uma breve introdução do funcionamento dos computadores
digitais programáveis, das linguagens de programação, da codificação da
informação e do projeto de sistemas de software; O capítulo 5 oferece um
atalho para o início das atividades práticas, de forma simples e objetiva, com
a finalidade de que o leitor comece a se familiarizar com a linguagem C e
com o uso do compilador da sua escolha; No capítulo 6, inicia um conjunto de
informações formais e completo sobre a linguagem C, estendendo-se até o
capítulo 16; O capítulo 17 trata sobre as interrupções, apresentando exemplos
de utilização, inclusive com aplicações de microcontroladores; O capítulo 18
trata das interfaces paralelas e o 19 do uso e programação das interfaces
seriais.
O apêndice A comenta os padrões seriais de transmissão mais utilizados, tais
como RS232, RS422, RS485 e outros, assim como, a especificação e conexão
dos sistemas, blindagem e proteção contra transientes; os apêndices B e C são
exemplos de aplicação da linguagem C em microcontroladores Microchip PIC
e os da família Intel 8x51; O apêndice D (no CD) mostra um exemplo de
comunicação com as portas no sistema Windows®; O apêndice E (no CD)
apresentauma tabela dos mais variados tipos de conectores, com a descrição
da pinagem correspondente; O apêndice F (no CD) apresenta alguns
exemplos de códigos que fazem o uso da porta paralela para controlar
conversores AD e DA, teclados, módulos de LCD, geradores PWM,
acionamentos de relés, motores de passo e outros programas de cálculo
numérico e de manipulação da informação. E finalmente, o apêndice G
mostra um exemplo de primeiros passos para trabalhar com os mais variados
compiladores propostos neste livro.
O CD que acompanha este livro, possui apresentações Powerpoint® dos
apêndices B e C, os textos dos apêndices D, E e F, códigos compilados, um
programa de gravação para microcontroladores 89C52 elaborado pela Mater
Matris Technae (antiga Antrax Technology), diagramas esquemáticos de
gravador e alguns outros programas gratuitos que podem auxiliar a
aprendizagem, além do Dev C++ que poderá ser utilizado para iniciar as
tarefas de estudo.
Para iniciar as atividades, propõe-se ao leitor efetuar a leitura dos primeiros
quatro capítulos, posteriormente escolher o compilador a ser utilizado durante
a aprendizagem, ler e tentar executar as tarefas do anexo G, para o compilador
escolhido, e depois disto tudo continuar com a leitura do capítulo 5. Às
7
vezes, costumo falar brincando que “...a programação se aprende pelos dedos
e não pelos olhos....”, querendo dizer com isso, que a prática é o componente
essencial da aprendizagem nesta área, e que não adianta decorar e entender
livros inteiros sem ter compilado, executado e depurado pelo menos uma
centena de programas. O nosso lema segue o tradicional ditado que diz “... la
práctica hace al maestro...”.
Este livro contém informação suficiente para uma disciplina de um semestre,
apresentando uma lista de exercícios no final de cada capítulo. Não espero
que este seja o melhor livro de programação para engenheiros, mas é uma
primeira tentativa de reunir informações sobre os mais diversos assuntos
relacionados com a área da engenharia elétrica e de computação. Espero que
com os conhecimentos adquiridos neste livro, o leitor possa facilmente
extrapolar os conhecimentos de programação adquiridos, para começar os
estudos de linguagens mais sofisticadas, tais como C++, assim como as
linguagens de “alto nível” projetadas para trabalhar na rede mundial, tais
como, Java ou C#.
Parte da informação contida neste livro foi adaptada de livros, sítios da
internet, de manuais de fabricantes e de arquivos de ajuda dos principais
compiladores existentes no mercado. Nesses casos, colocaram-se as
referências de origem. Tomou-se especial cuidado para não infringir qualquer
direito autoral, tanto de textos, quanto de trechos de código.
Por ser a primeira edição deste livro, e devido ao seu vasto conteúdo, peço
desculpas pelos erros de edição que venham a acontecer. Os programas de
exemplo foram todos testados nos vários compiladores sugeridos. As
palavras dos comentários efetuados nos programas exemplo não foram
acentuadas de forma proposital, porque alguns compiladores, preparados para
o idioma inglês, não suportam caracteres acentuados. Caso tenha algum
comentário ou sugestão relacionada a este livro, por favor, envie um e-mail
com as suas sugestões para o meu endereço cocian@ig.com.br. Terei prazer
em responder.
Luis Fernando Espinosa Cocian
Porto Alegre, 13 de junho de 2004
8
1. INTRODUCÃO
Os tópicos que serão discutidos neste capítulo incluem:
Linguagens de programação
História da Linguagem C
9
1.1. Por que estudar a linguagem C?
A linguagem C tem sido utilizada com sucesso em todos os tipos imagináveis
de problemas de programação, desde sistemas operacionais, planilhas de
texto, até em sistemas expertos, e hoje em dia, estão disponíveis compiladores
eficientes para máquinas de todo tipo de capacidade de processamento, desde
as Macintosh da Apple, até os supercomputadores Cray. Basicamente todas
as linguagens de programação conseguem os mesmos efeitos, algumas de
forma mais eficiente que outras, sempre dependendo do tipo de aplicação para
a qual será destinada.
A linguagem C de programação tem se tornado muito popular devido à sua
versatilidade e ao seu poder. Uma das grandes vantagens da linguagem C é a
sua característica de "alto nível" e de "baixo nível" ao mesmo tempo,
permitindo o controle total da máquina (hardware e software) por parte do
programador, permitindo efetuar ações sem depender do sistema operacional
utilizado. A linguagem C é frequentemente denominada de linguagem de
programação de “nível médio”. Isso não é no sentido de capacidade de
processamento entre as linguagens de alto e as de “baixo nível”, mas sim no
sentido da sua capacidade de acessar as funções de “baixo nível”, e ao mesmo
tempo, de constituir os blocos de construção para constituir uma linguagem
de “alto nível”. A maioria das linguagens de “alto nível”, por exemplo
FORTRAN, fornece o necessário para que o programador consiga efetuar o
processamento que deseja, já implementado na própria linguagem. As
linguagens de “baixo nível” , como o assembly, fornecem somente o acesso às
instruções básicas da máquina digital. As linguagens de nível médio, tais
como C, provavelmente, não fornecem todos os blocos de construção
oferecidos pelas linguagens de “alto nível”, mas, fornecem ao programador
todos os blocos de construção necessários para produzir os resultados que são
necessários.
Alguns dos pontos positivos que tornaram essa linguagem tão popular são:
1. A portabilidade do compilador
2. O conceito de bibliotecas padronizadas
3. A quantidade e variedade de operadores poderosos
4. A sintaxe elegante
5. O fácil acesso ao hardware quando necessário
6. A facilidade com que as aplicações podem ser otimizadas, tanto na
codificação, quanto na depuração, pelo uso de rotinas isoladas e
encapsuladas.
Em algumas aplicações de engenharia, é necessário manter o controle total do
hardware através do software para efetuar acionamentos e temporizações
precisas em tempo real, basicamente sistemas determinísticos. Uma grande
10
área de atuação da engenharia é na simulação de processos físicos, onde é
necessária uma grande otimização dos recursos, tais como, espaço de
memória e tempo de processamento.
A linguagem C foi projetada para a construção de sistemas operacionais, com
o consequente controle do hardware. Em aplicações de engenharia, a
linguagem C é utilizada frequentemente para implementar:
1. Software básico.
1. Programas executivos e aplicativos em CLPs[1].
2. Firmware[2] e software aplicativo em coletores de dados, telefones
celulares e outros sistemas dedicados.
3. Controle eletrônico automático em automóveis.
4. Instrumentos inteligentes
5. Gateways[3] de comunicação.
6. Modems[4].
7. Programadores de FPGAs[5] (alternativa para a linguagem VHDL).
8. Periféricos em geral.
9. Interfaces Homem-Máquina
10. Sistemas operacionais.
11. Drivers[6] de comunicação e de dispositivos.
12. Programas do tipo Vírus e antivírus.
13. Firmware e software em satélites artificiais e veículos espaciais.
14. Processamento digital de sinais
15. Processamento de Imagens
16. Programas de Inteligência Artificial e redes neurais.
17. Modelagem numérica de sistemas físicos para simulação de efeitos
dinâmicos em eletromagnetismo, fenômenos de transporte e
termodinâmica.
A linguagem C é a indicada em sistemas que envolvem software e hardware,
e onde se deseja tero controle total da máquina digital. Apesar disso, alguns
engenheiros preferem utilizar a linguagem assembly, ainda hoje, por
conhecimento limitado da linguagem C ou pela falta de espaço de memória
disponível, geralmente devido à manutenção de projetos de hardware antigos
ou mal elaborados.
A assembly é a melhor linguagem de programação? A resposta é que a
eficiência da assembly para sistemas grandes e complexos é muito pobre,
além do código não poder ser reutilizado para repetir a aplicação em outros
microprocessadores que não o de origem e de ser de difícil depuração.
Ante essa resposta, frequentemente, alguns os engenheiros respondem que o
código gerado pela assembly é mais rápido e utiliza menos recursos da
máquina, o que otimizaria o seu desempenho. A isso, pode ser
complementado que a maior rapidez na execução e o menor uso de recursos
11
para efetuar uma tarefa vai depender do programador. A probabilidade de que
os programadores de uma empresa que produz compiladores consigam obter
o código mais eficiente em C do que o nosso próprio, em assembly, é muito
maior, já que eles, sem lugar a dúvidas, gastaram muitas horas e dias
procurando gerar o código mais eficiente possível. Isso é comparado com a
escolha do tipo de câmbio quando a compra de um veículo: Câmbio manual
ou automático? Alguns preferem o manual, dando como justificativa de que é
possível a mudança mais rápida das marchas. Mas tem muita gente que não
tem a habilidade motora suficiente para efetuar essa tarefa de modo eficiente
o tempo inteiro. Obviamente, um piloto profissional efetuará as marchas de
forma muito mais rápida do que uma pessoa comum. Nas salas de aula
costumo fazer a seguinte analogia: “programar em assembly é como varrer o
nosso jardim com uma escova de dentes”.
Para se ter uma ideia da importância estratégica na escolha da linguagem,
especialmente na programação de firmware para sistemas integrados, imagine
que um programa feito em linguagem C, com aproximadamente 500 linhas,
considerado de complexidade média, tem como equivalente assembly um
outro que se constitui de aproximadamente 5000 linhas, num compilador
assembly para 8051, constituindo-se num programa de complexidade elevada.
Se compararmos o tempo de desenvolvimento, um programa em linguagem C
com 500 linhas, pode ser escrito e depurado em aproximadamente 20 horas de
trabalho de engenheiro (R$ 50.00/hora), o que teria um custo fixo de
R$1000.00. O mesmo programa em assembly, levará aproximadamente 70
horas, dando um custo final de R$ 3500,00. Mas isso não é o pior, o grande
problema está na atualização e na correção do bugs por outras pessoas que
não os programadores originais. Nestes casos, o custo e tempo de atualizar
um programa em assembly são em torno de dez vezes mais caro, que atualizar
um programa em linguagem C. Isso tem ocasionado que em várias aplicações,
a solução mais viável foi ignorar o programa anterior e refazer tudo
novamente. Nesses casos todo o investimento inicial foi perdido. Já me
deparei com a atualização de sistemas de software, escritos em assembly, que
tinham somado investimentos de US$300.000,00, que tiveram de ser
descartados por um software escrito em linguagem C, mais funcional, rápido,
eficiente e mais fácil de atualizar, com investimentos totais de US$20.000,00,
e tempo de desenvolvimento da ordem de dez por cento com relação ao
projeto anterior.
12
1.2. Quando essa linguagem ficará obsoleta?
Como qualquer ferramenta tecnológica, a linguagem C deverá ficar obsoleta
algum dia, mas pode-se antecipar que isso não ocorrerá até pelo menos o fim
da segunda década dos 2000, devido à quantidade enorme de linhas de código
produzidas e que normalmente são reaproveitadas.
Por ser uma linguagem extremamente simples, fácil de aprender, clara e
objetiva, aplicável à maioria dos problemas de engenharia, essa linguagem
provavelmente sobreviverá por mais 30 anos.
13
1.3. Breve história da Linguagem C
A linguagem C foi inventada na década de 70. Seu inventor, Dennis Ritchie,
implementou-a pela primeira vez, usando um DEC PDP-11 rodando o sistema
operacional UNIX. A linguagem C é derivada de outra: a B, criada por Ken
Thompson. O histórico a seguir mostra a evolução das linguagens de
programação que certamente influenciaram a linguagem C.
Algol 60 – Projetado por um comitê internacional.
CPL – Combined Programming Language. Desenvolvida em
Cambridge e na Universidade de Londres em 1963.
BCPL – Basic Combined Programming Language. Desenvolvida
em Cambridge por Martin Richards em 1967.
B – Desenvolvida por Ken Thompson, nos Laboratórios Bell em
1970, a partir da linguagem BCPL.
C – Desenvolvida por Dennis Ritchie, nos Laboratórios Bell em
1972. Aparece também a figura de Brian Kernighan como colaborador.
ANSI C – O comitê ANSI (American National Standards Institute)
foi reunido com a finalidade de padronizar a linguagem C em 1983.
C++ - A linguagem C se torna ponto de concordância entre teóricos
do desenvolvimento da teoria de Object Oriented Programming
(programação orientada a objetos): surge a linguagem C++ com
alternativa para a implementação de grandes sistemas. Essa linguagem
consegue interpretar linhas de código escritas em C.
A linguagem Algol apareceu alguns anos depois da linguagem Fortran. Esta
era bem sofisticada e, sem lugar a dúvidas, influenciou muito o projeto das
linguagens de programação que surgiram depois. Seus criadores deram
especial atenção à regularidade da sintaxe, estrutura modular e outras
características associadas com linguagens estruturadas de “alto nível”.
Os criadores do CPL pretendiam fazer baixar, até a realidade de um
computador real, os elevados intentos do Algol. Isto tornou a linguagem de
difícil aprendizagem e implementação. Desta surge o BCPL como um
aperfeiçoamento da CPL.
No início da linguagem B, o seu criador Ken Thompson, projetando a
linguagem para o sistema UNIX, tenta simplificar a linguagem BCPL.
Porém, a linguagem B não ficou bem coesiva, ficando boa somente para o
controle do hardware.
Logo após de ter surgido a linguagem B, surge uma nova máquina, o PDP-
11. O sistema operacional Unix e o compilador B foram adaptados para essa
máquina. A linguagem B começa a ser questionada devido à sua relativa
lentidão, por causa do seu desenho interpretativo. Além disso, a linguagem B
14
era orientada a palavra enquanto o PDP-11 era orientado a byte. Por essas
razões começou-se a trabalhar numa linguagem sucessora da B.
A criação da linguagem C é atribuída a Dennis Ritchie, que restaurou algumas
das generalidades perdidas pela BCPL e B. Isso foi conseguido através do
hábil uso dos tipos de dados enquanto mantinha a simplicidade e o contato
com o computador.
15
1.4. Exercícios
1. Quais são os pontos fortes da linguagem C ?
2. Pesquise sobre a linguagem C++ e verifique se é compatível com a linguagem
C. Podemos usar um compilador C++ para compilar programas em C?
3. A linguagem C pode ser utilizada com o sistema operacional Linux? Se a
resposta for positiva, pesquisar dois compiladores na internet.
16
2. OS COMPUTADORES
Os tópicos que serão discutidos neste capítulo incluem:
Os Computadores
Os Programas de Computador
As linguagens de Programação
17
2.1. O que são os computadores ?
Não há como controlar os computadores sem conhecer o que são e como
funcionam. Os computadoressão basicamente máquinas que executam
tarefas, tais como, cálculos matemáticos e comunicações eletrônicas de
informação, sob o controle de um grupo de instruções inserido de antemão,
denominado programa. Os programas usualmente residem dentro do
computador e são lidos e processados pela eletrônica do sistema que compõe
o computador. Os resultados do processamento do programa são enviados a
dispositivos eletrônicos de saída, tais como, um monitor de vídeo, uma
impressora ou um modem. Essas máquinas são utilizadas para efetuar uma
ampla variedade de atividades com confiabilidade, exatidão e velocidade.
18
2.2. Como os computadores funcionam ?
A parte física do computador é conhecida como hardware. O hardware do
computador inclui: a memória, que armazena tanto os dados quanto as
instruções; a unidade central de processamento (CPU[7]) que executa as
instruções armazenadas na memória; o barramento[8] que conecta os vários
componentes do computador; os dispositivos de entrada, tais como, o mouse
ou o teclado, que permitem ao usuário poder comunicar-se com o computador
e os dispositivos de saída, tais como, impressoras e monitores de vídeo, que
possibilitam a visualização das informações processadas pelo computador. O
programa que é executado pelo computador é chamado de software. O
software é geralmente projetado para executar alguma tarefa particular, por
exemplo, controlar o braço de um robô para a soldagem de um chassi de
automóvel ou desenhar um gráfico.
19
2.3. Tipos de Computadores
Os computadores podem ser digitais ou analógicos. A palavra digital refere-
se aos processos que manipulam números discretos (por exemplo, sistemas
binários: 0s e 1s) que podem ser representados por interruptores elétricos que
abrem ou fecham (implementados por transistores trabalhando na região de
saturação e de corte respectivamente). O termo analógico refere-se a valores
numéricos que têm faixa de variação contínua. 0 e 1 são números analógicos,
assim como 1.5 ou o valor da constante . Como exemplo, considere uma
lâmpada incandescente que produz luz em um momento e não a produz em
outro quando manipulado um interruptor (iluminação digital). Se o
interruptor for substituído por um dimmer[9], então a iluminação ficará
analógica uma vez que a intensidade de luz pode variar continuamente entre
os estados de ligada e desligada.
Os primeiros computadores eram analógicos, mas devido à sensibilidade a
perturbações externas e pelas necessidades de serem sistemas confiáveis,
foram substituídos por computadores digitais que trabalham com informação
codificada de forma discreta. Estes são mais imunes a interferências externas
e internas.
A natureza dos sinais utilizados na codificação da informação pode ser na
forma de campos elétricos gerados por cargas (circuitos que utilizam
transistores como chaves), campos eletromagnéticos (computadores que
utilizam fótons), fenômenos eletroquímicos (computadores orgânicos), forças
hidráulicas, pneumáticas e outros.
20
2.4. Software Básico e o Sistema Operacional
Quando um computador é ligado, a primeira coisa que ele faz é a procura de
instruções armazenadas na sua memória. Usualmente o primeiro grupo de
instruções é um programa especial que permite o inicio da operação. Essas
instruções mandam o computador executar outro programa especial chamado
sistema operacional, que é o software que facilita a utilização da máquina por
parte do usuário. Ele faz o computador esperar por instruções do usuário (ou
de outras máquinas) por comandos de entrada, relata os resultados destes
comandos e outras operações, armazenamento e gerenciamento de dados e
controla a sequência das ações do software e do hardware. Quando o usuário
requisita a execução de um programa, o sistema operacional o carrega na
memória de programa do computador e o instrui para executar o mesmo.
Figura 2. Componentes básicos de um sistema computador
2.4.1. A Memória
Para processar a informação eletronicamente, os dados são armazenados no
computador na forma de dígitos binários, ou bits, cada um tendo duas
possíveis representações (0 ou 1 lógicos). Se adicionarmos um segundo bit a
unidade única de informação, o número de representações possíveis é
dobrado, resultando em quatro possíveis combinações: 00, 01, 10, 11. Um
terceiro bit adicionado a esta representação de dois bits duplica novamente o
número de combinações, resultando em oito possibilidades: 000, 001, 010,
011, 100, 101, 110, ou 111. Cada vez que um bit é adicionado, o número
21
possível de combinações é duplicado.
Um conjunto de 8 bits é chamado de byte. Cada byte possui 256 possíveis
combinações de 0 e 1’s. O byte é uma quantidade frequentemente utilizada
como unidade de informação porque possibilita a representação do alfabeto
ocidental completo, incluindo os símbolos das letras em maiúsculas e
minúsculas, dígitos numéricos, sinais de pontuação e alguns símbolos
gráficos. Como alternativa, o byte poderá representar simplesmente uma
quantidade numérica positiva entre 0 e 255.
O computador divide o total da memória disponível em dois grupos lógicos: a
memória de programa e a memória de dados.
A memória física do computador pode ser do tipo RAM[10] que pode ser
tanto lida quanto escrita e ROM (Read Only Memory) que somente pode ser
lida. Em geral as memórias RAM são voláteis, isto é, são apagadas quando o
sistema é desenergizado. Outros tipos de memórias são: PROMs (OTPs),
EPROMs, EEPROMs, Flash-EEPROMs, NVRAM que são não voláteis, isto
é, os dados permanecem inalterados mesmo depois de desligar o sistema.
A Figura 2-1 mostra o processo de gravação da letra ‘A’ (com código ASCII
igual a 41H em hexadecimal, 01000001 em binário ou 65 em decimal) na
posição de memória número 0. Este chip de memória hipotético, possui 16
posições de memória de 1 byte cada, endereçáveis por 4 bits (24 = 16 posições
possíveis). No processo de escrita, a CPU coloca o dado a ser gravado no
barramento de endereços, logo coloca o endereço onde os dados serão
armazenados, no barramento de endereços, e finalmente indica ao chip que a
operação é de escrita ativando o sinal WR (WRITE).
Figura 2-1 – Processo de gravação
No processo de leitura, a CPU coloca o endereço da posição de memória que
deseja ser lida no barramento de endereços e indica que o processo é de
leitura, acionando o pino RD (READ). A memória posteriormente coloca o
dado armazenado naquela memória no barramento de dados. Após certo
intervalo de tempo, a CPU faz a leitura desses dados.
22
2.4.2. Os Barramentos
O barramento (bus) é usualmente um conjunto paralelo de fios que
interconecta os vários dispositivos componentes do hardware do sistema, tais
como a CPU e a memória, habilitando e gerenciando a comunicação de dados
entre estes.
Usualmente existem três tipos de barramentos: o barramento de controle que
controla o funcionamento dos componentes do sistema; o barramento de
endereços, onde é colocada a informação de origem ou destino para a
informação a ser enviada ou recuperada; e o barramento de dados, onde
trafegam os dados.
O número de linhas componentes do barramento limita a capacidade de
endereçamento de memória para o computador. Por exemplo, o 8051 possui
16 linhas de endereços no seu barramento externo, e portanto, ele poderá
endereçar no máximo 216 = 65536 posições de memória. Já um barramento
de 32 bits conseguirá endereçar 232 = 4294967296 posições no máximo.
23
2.5. A Unidade Centralde Processamento - CPU
A informação originária de um dispositivo de entrada ou da memória é
transferida através do barramento para a CPU, sendo esta a unidade que
interpreta os comandos e executa os programas. A CPU é um circuito
microprocessador, constituído de uma peça única feita em silício e óxidos
contendo milhões de componentes eletrônicos numa área muito pequena. A
informação é armazenada em posições de memórias especiais colocadas
dentro do microprocessador chamadas de Registradores. Estes são pequenas
memórias de armazenamento temporário para instruções ou dados.
Enquanto um programa estiver sendo executado, existe um registrador
especial que armazena o próximo lugar da memória de programa que
corresponde à próxima instrução a ser executada; frequentemente chamado de
Contador de Programa[11]. A unidade de controle situada no
microprocessador coordena e temporiza as funções da CPU e recupera a
próxima instrução da memória a ser executada.
Numa sequência típica de operação, a CPU localiza a instrução seguinte no
dispositivo apropriado da memória. A instrução é transferida por meio do
barramento para um registrador especial de instruções dentro da CPU.
Depois disso, o contador de programa é incrementado para se preparar para a
próxima instrução. A instrução corrente é analisada pelo decodificador, que
determina o que esta determina que deve ser feito. Os dados necessários pela
instrução serão recuperados através do barramento e colocados em outros
registradores da CPU. A CPU então executa a instrução e o resultado é
armazenado também em outros registradores especiais ou copiado para
lugares específicos da memória.
24
Figura 2-2 – Processador 8051 FX Intel
Uma característica importante das CPUs é o tamanho dos dados (em número
de bits) que esta pode manipular com uma única instrução. Assim, temos
atualmente, CPUs de 8 bits (ex. PIC16C877, 8051), de 16 bits (ex. 8088,
80286, 80196), 32 bits (80386, 80486, 80586, Pentium) e algumas menos
comuns de 64 bits. O tamanho indica, por exemplo, que um processador
80486 consegue somar dois números representados com 32 bits utilizando
uma instrução de código de máquina, sendo que a mesma operação deverá ser
repartida em várias instruções para ser efetuada em uma CPU com um
número de bits de menor.
A Figura 2-2 mostra um processador 8051FX da Intel com os blocos
componentes da pastilha de circuito integrado.
25
2.6. Programas de Computador
Um programa de computador é basicamente uma lista de instruções que este
deverá executar. Uma vez que as máquinas digitais só conseguem interpretar
informações na forma de sinais elétricos, e que os humanos interpretam na
forma de imagens ou sons, é necessária uma ferramenta que implemente uma
interface simplificada para a programação. Estas ferramentas são chamadas
de linguagens de programação. As linguagens de programação contêm uma
série de comandos que formam o software. Em geral, a linguagem que é
diretamente codificada em números binários interpretáveis pelo hardware do
computador é mais rapidamente entendida e executada. As linguagens que
usam palavras e outros comandos que refletem o pensamento lógico humano
são mais fáceis de utilizar, mas são mais lentas, já que esta deve ser traduzida
antes, para que o computador possa interpretá-la.
O software de um computador consiste em programas ou lista de instruções
que controla a operação da máquina. O termo pode ser referido a todos os
programas utilizados com um computador específico ou simplesmente a um
único programa.
O software é a informação abstrata armazenada como sinais elétricos na
memória do computador, em contraste como os componentes de hardware,
tais como, a unidade central de processamento e os dispositivos de entrada e
saída. Esses sinais são decodificados pelo hardware e interpretados como
instruções, sendo que cada tipo de instrução guia ou controla o hardware por
um breve intervalo de tempo.
Uma grande variedade de softwares é utilizada num computador. Uma forma
fácil de entender esta variedade é pensar em níveis. O nível mais baixo é o
que está mais perto do hardware da máquina. O mais alto está mais perto do
operador humano. Os humanos raramente interagem com o computador no
nível mais baixo, mas o fazem utilizando tradutores chamados
Compiladores. Um compilador é um programa de software cujo propósito é
converter programas escritos em uma linguagem de “alto nível”, numa de
“baixo nível” que possa ser interpretado pelo hardware. Os compiladores
eliminam o processo tedioso de conversar com um computador na sua própria
linguagem binária.
Em uma camada acima do nível mais baixo, poderá existir um software
chamado de Sistema Operacional que controla o sistema em si. Ele organiza
as funções de hardware, tais como, a leitura e escrita em dispositivos de
entrada e saída (teclado, monitor de vídeo, discos magnéticos, etc.),
interpretando comandos do usuário e administrando o tempo e os recursos
para os programas de aplicação
26
O software de aplicação adapta o computador a um propósito especial, tal
como, o processamento e monitoração de variáveis de controle de uma fábrica
ou para efetuar a modelagem de uma parte de uma máquina. As aplicações
são escritas em qualquer uma das várias linguagens compiladas que forem
mais apropriadas para a aplicação específica. Essas linguagens, inventadas
pelos seres humanos, não podem ser diretamente entendidas pelo hardware do
computador. Elas devem ser traduzidas por um compilador apropriado que as
converta em códigos de zeros e uns que a máquina possa processar.
O software é usualmente distribuído em discos magnéticos ou ópticos.
Quando o disco é lido pela unidade de leitura apropriada, o software será
copiado na memória. Então o sistema operacional do computador passa o
controle para a aplicação no processo que ativa o programa. Quando o
programa é terminado, o sistema operacional reassume o controle da máquina
e espera alguma requisição por parte do usuário.
27
2.7. Linguagens de Programação
Como foi visto anteriormente, um programa de computador é um conjunto
instruções que representam um algoritmo para a resolução de algum
problema. Essas instruções são escritas através de um conjunto de códigos
(símbolos e palavras). Esse conjunto de códigos possui regras de estruturação
lógica e sintática própria. Dizemos que esse conjunto de símbolos e regras
forma uma linguagem de programação.
2.7.1. A Linguagem de Máquina
Os programas de computador que podem ser executados por um sistema
operacional são frequentemente chamados de executáveis. Um programa
executável é composto de uma sequência de um grande número de instruções
extremamente simples conhecida como código de máquina. Estas instruções
são específicas para cada tipo especial de CPU e ao hardware à qual se
dedicam (por exemplo, microprocessadores Pentium®, 80486, 8051,
PIC16F877, Z80, etc.) e que têm diferentes linguagens de máquina e
requerem diferentes grupos de códigos para executar a mesma tarefa. O
número de instruções de código de máquina normalmente é pequeno (de 20 a
200 dependendo do computador e da CPU). Instruções típicas são para copiar
dados de um lugar da memória e adicionar o conteúdo de dois locais de
memória (usualmente registradores internos da CPU). As instruções de
código de máquina são informações bináriasnão compreensíveis facilmente
por humanos, e por causa disso, as instruções não são usualmente escritas
diretamente em código de máquina.
2.7.2. A Linguagem Assembly
A linguagem Assembly utiliza comandos que são mais fáceis de entender
pelos programadores que a linguagem de máquina. Cada instrução da
linguagem de máquina tem um comando equivalente na linguagem assembly.
Por exemplo, na linguagem assembly, o comando “MOV A,B” instrui o
computador a copiar dados de um lugar para outro. A mesma instrução em
linguagem de máquina poderá ser uma cadeia de 8 bits binários ou mais,
dependendo do tipo de CPU (por exemplo 0011 1101). Uma vez que o
programa em assembly é escrito, deve ser convertido em um programa em
linguagem de máquina através de outro programa chamado de
Assembler[12]. A linguagem assembly é a mais rápida e poderosa devido a
sua correspondência com a linguagem de máquina. É uma linguagem muito
difícil de utilizar. Às vezes, instruções em linguagem assembly são inseridas
no meio de instruções de “alto nível” para executar tarefas específicas de
hardware ou para acelerar a execução de algumas tarefas.
2.7.3. Linguagens de “Alto Nível”
28
As linguagens de “alto nível” foram desenvolvidas devido às dificuldades de
programação utilizando linguagens assembly. As linguagens de “alto nível”
são mais fáceis de utilizar que as linguagens de máquina e a assembly, devido
aos seus comandos que lembram a linguagem natural humana. Ainda que
essas linguagens independem da CPU a ser utilizada, elas contêm comandos
gerais que trabalham em diferentes CPUs da mesma forma. Por exemplo, um
programador escrevendo na linguagem C para mostrar uma saudação num
dispositivo de saída (por exemplo, um monitor de vídeo) somente teria que
colocar o seguinte comando:
printf(“Bom dia, engenheiro !”);
Esse comando direcionará a saudação para o dispositivo de saída e
funcionará, sem importar que tipo de CPU o computador utiliza. De forma
análoga à linguagem assembly, as linguagens de “alto nível” devem ser
traduzidas. Para isso, é utilizado um software chamado Compilador. Um
compilador transforma um programa escrito numa linguagem de “alto nível”
num programa em código de máquina específico. Por exemplo, um
programador pode escrever um programa em uma linguagem de “alto nível”
tal como C, e então, prepará-lo para ser executado em diferentes máquinas,
tais como, um supercomputador Cray Y-MP ou simplesmente um PC, usando
compiladores projetados para cada uma dessas máquinas. Essa característica
acelera a tarefa de programação e faz o software mais portável para diferentes
usuários e máquinas.
A oficial naval e matemática americana, Grace Murray Hopper, ajudou a
desenvolver a primeira linguagem de software de “alto nível” comercialmente
disponível, a FLOW-MATIC, em 1957. É creditada a ela a invenção do termo
bug, para indicar que o programa executado pelo computador, apresenta um
defeito no funcionamento. Em 1945, ela descobriu uma falha do hardware
num computador Mark II, ocasionada por um inseto que ficou preso entre os
relés eletromecânicos, componentes do sistema lógico.
Na década de 1950 (1954 a 1958), o cientista de computação Jim Backus da
International Business Machines, Inc. (IBM) desenvolveu a linguagem
FORTRAN (FORmula TRANslation). Esta permanece até hoje,
especialmente no mundo científico, como uma linguagem padrão de
programação, já que facilitava o processamento de fórmulas matemáticas.
Em 1964, foi criada a linguagem BASIC (Beginner’s All-purpose Symbolic
Instruction Code), desenvolvida por dois matemáticos: o americano John
Kemeny e o húngaro Thomas Kurtz, no Dartmouth College. Essa linguagem
era muito fácil de aprender, comparada com as anteriores, e ficou popular
devido a sua simplicidade, natureza interativa e a sua inclusão nos
computadores pessoais. Diferente de outras linguagens que requerem que
todas as suas instruções sejam traduzidas para linguagens de máquina, antes
29
de serem executadas, estas são interpretadas, isto é, são convertidas em
linguagem de máquina, linha a linha, enquanto o programa está sendo
executado. Os comandos em BASIC tipificam a linguagem de “alto nível”
devido a sua simplicidade e semelhança com a linguagem natural humana.
Um exemplo de programa que divide um número por dois, pode ser escrito
como:
10 INPUT “ENTRE COM O NÚMERO,” X
20 Y=X/2
30 PRINT “A metade do número é ,” Y
Outras linguagens de “alto nível”, em uso hoje em dia, incluem C, C++, Ada,
Pascal, LISP, Prolog, COBOL, HTML, e Java, entre outras.
2.7.4. Linguagens Orientadas a Objetos
As linguagens de programação orientadas a objetos[13] , tais como o C++,
são baseadas nas linguagens tradicionais de “alto nível”, mas elas habilitam o
programador a pensar em termos de coleções de objetos cooperativos no lugar
de uma lista de comandos. Os objetos, tais como um círculo, têm
propriedades, tais como o raio do círculo e o comando que o desenha na tela.
Classes de objetos podem ter características inerentes de outra classe de
objetos. Por exemplo, uma classe que define quadrados pode herdar
características, tais como, ângulos retos de uma classe que define os
retângulos. Esses grupos de classes de programação simplificam a tarefa de
programação, resultando em programas mais eficientes e confiáveis.
Figura 2-3 - Exemplo de Fluxograma
2.7.5. Algoritmos
Um algoritmo é o procedimento para resolver um problema complexo,
30
utilizando uma sequência precisa e bem determinada de passos simples e não
ambíguos. Tais procedimentos eram utilizados originalmente em cálculos
matemáticos e, hoje são usados em programas de computador e em projetos
de hardware. Usualmente são auxiliados por fluxogramas que são utilizados
para facilitar o entendimento da sequência dos passos. Os fluxogramas
representam a sequência de passos implementada pelo algoritmo de forma
gráfica. A Figura 2-3 mostra o exemplo de um fluxograma.
2.7.6. Exemplos de Codificação de Instruções
Existem muitas linguagens de programação. Pode-se escrever um algoritmo
para resolução de um problema por intermédio de qualquer linguagem. A
seguir, são mostrados alguns exemplos de trechos de códigos escritos
utilizando algumas linguagens de programação.
Exemplo: trecho de um algoritmo escrito numa pseudo-linguagem de “alto
nível”, que recebe um número (armazenado na variável num), calcula e
mostra os valores múltiplos de 1 a 10 para o mesmo.
ler num
para n de 1 até 10 passo 1 fazer
tab num * n
imprime tab
fim fazer
Exemplo: trecho do mesmo programa escrito em linguagem C:
unsigned int num,n,tab;
scanf("%d",&num);
for(n = 1; n <= 10; n++){
tab = num * n;
printf("\n %d", tab);
}
Exemplo: trecho do mesmo programa escrito em linguagem BASIC:
10 INPUT num
20 FOR n = 1 TO 10 STEP 1
30 LET tab = num * n
40 PRINT chr$ (tab)
50 NEXT n
Exemplo: trecho do mesmo programa escrito em linguagem Fortran:
read (num);
do 1 n = 1:10
tab = num * n
write(tab)
10 continue
Exemplo: trecho do mesmo programa escrito em linguagem Assembly para
INTEL 8088:
MOV CX,0 ; coloca zero no registrador CX
IN AX,PORTA ; coloca um valor do buffer de teclado no registrador AX
MOV DX,AX ; copia o valor de AX para DX
LABEL:
INC CX ; incrementa em um o valor armazenado em CX
31
MOV AX,DX; copia o valor de DX para AX
MUL CX ; multiplica o valor armazenado em CX pelo valor em AX
OUT AX, PORTB ; o valor resultante é enviado ao buffer de saída de display
CMP CX,10 ; o valor armazenado em CX é comparado com 10
JNE LABEL ; se a contagem ainda não chegou a 10, pula para LABEL e repete
32
2.8. Exercícios
1. Abra a tampa do seu computador pessoal. Efetue um esboço dos blocos
componentes. Identifique os barramentos, a CPU, memórias e outros dispositivos.
2. Qual é a diferença entre um supercomputador e um computador pessoal?
3. Pesquisar sobre conexão de processadores em paralelo.
4. Quantos sistemas operacionais você conhece?
5. Qual é a diferença entre uma Word e um byte?
6. Qual é a diferença entre um microprocessador e um microcontrolador?
7. Quantos microprocessadores e microcontroladores compõem o seu computador
pessoal?
8. Verificar a existência de um microcontrolador dentro do seu teclado.
9. Por que é importante a elaboração de fluxogramas antes de efetuar a
codificação das instruções.
33
3. A CODIFICAÇÃO DA INFORMAÇÃO
Os tópicos que serão discutidos neste capítulo incluem:
Os sistemas de numeração
Organização dos dados
Operações lógicas
Números com e sem sinal
A codificação ASCII
Durante séculos, o ser humano vem codificando e armazenando a informação
mediante símbolos gráficos (linguagem escrita) e regras de montagem
(gramática). A informação pode ser definida como o conhecimento derivado
do estudo ou a experiência, basicamente, uma coleção de fatos ou dados.
Provavelmente essa capacidade abstrata de armazenar a informação ao longo
dos séculos permitiu ao ser humano se impor sobre as outras espécies
animais.
A informação em si é um conceito abstrato que para ser compartilhado com
outras pessoas deve ser representado de forma coerente e simples. A
linguagem escrita é um meio coerente, onde temos uma série de símbolos que
associados de formas diferentes representam informações diferentes. A
representação da informação abstrata numa série de símbolos (podem ser
gráficos, sonoros, elétricos, luminosos) é conhecida como codificação. A
informação codificada depende fortemente do contexto em que esta for
transmitida, por exemplo, o código ELÉTRON pode significar uma referência
a um componente da matéria, ao nome de um cão de estimação ou
simplesmente a um conjunto de caracteres que implementa uma senha de
acesso.
Existem infinitas possibilidades de codificar uma informação. A informação
escrita é codificada utilizando letras do alfabeto (existem inúmeros alfabetos
diferentes no planeta) e regras de construção (existem inúmeros idiomas com
gramáticas totalmente diferentes). A informação, repassada na forma de
ondas de deslocamento de ar, possui suas regras (língua falada). No século
XX, começaram a ser utilizados outros tipos de codificação, como os sinais
de radiofrequência (ondas de rádio), onde um mecanismo eletrônico
codificava o sinal sonoro ou visual em ondas eletromagnéticas e no receptor
estes sinais são decodificados, ou seja, convertidos novamente em sinais de
imagem ou som. Nos computadores, a informação é codificada na forma de
campos magnéticos (nos discos rígidos ou disquetes) ou de campos elétricos
(nas memórias). Para o ser humano colocar as informações num computador,
precisa de ferramentas que executem esta tradução de sinais gráficos em
sinais elétricos, por exemplo. Para criar essas ferramentas ou utilizá-las com
34
maior eficiência, deve-se conhecer como os computadores codificam a
informação.
Os computadores eletrônicos codificam a informação de forma discreta
utilizando sistemas binários. Esses sistemas trabalham com a unidade mínima
de informação, o bit. O conceito de quantidade de informação está
relacionado diretamente com a probabilidade de acontecer uma determinada
informação. Por exemplo, se existirem duas informações possíveis, a
probabilidade é de 50% para cada uma delas. Se existirem 3 informações
possíveis, a probabilidade diminui para 33.3%. Se somente existir uma
informação, aí a probabilidade de acontecer é 100%, ou seja, não há
informação a ser repassada ou transmitida. Disso, deduz-se que para existir
alguma informação, deverão existir no mínimo duas informações possíveis,
basicamente sistemas binários, onde o mínimo de informação é representado
por uma variável que pode assumir um de dois valores possíveis, sendo a
natureza desse sinal, um campo elétrico, magnético, eletromagnético,
diferença de pressão, temperatura, força, etc..
No caso de ter que codificar um conjunto maior de informações (mais que
duas), procede-se a aumentar o número de unidades de informação (bits) que
as compõem, analogamente, como é feito com as letras do alfabeto.
Como exemplo, pode-se imaginar a codificação da informação abstrata “UM”
como quantidade. Isso pode ser codificado pela mente humana como
símbolos: “UM”, “um”, ”Um”, “uM”, “1”, “ 1 ” , “01”, “1.0”, ”1.100”, “One”,
“Un”, “Uno”, “Une”, “Eins”, etc. Nos computadores binários, devido aos
limitados recursos, quando comparados com os da mente humana, procura-se
sempre a compactação da informação. A informação de quantidade pode ser
armazenada na sua forma mais compacta na memória do computador, como
uma sequência de estados de um conjunto específico de transistores que
estariam, por exemplo, nos estados “0000 0001”, onde um estado
representado por 0 identifica um transistor operando na região de saturação, e
um 1 significa um transistor na região de corte. Este conjunto de bits
forneceria a informação abstrata de quantidade.
Alguns exemplos de codificação binária:
Quantidade abstrata 2 num computador de 8 bits: 0000 0010
Quantidade abstrata 2 num computador de 16 bits: 0000 0000 0000
0010
Símbolo gráfico ‘2’ (Codificação ASCII): 0011 0010
Quantidade abstrata 0 num computador de 8 bits: 0000 0000
Quantidade abstrata 0 num computador de 16 bits: 0000 0000 0000
0000
Símbolo gráfico ‘0’ (Codificação ASCII): 0011 0000
O número total de bits utilizado na representação da informação limita o
35
número máximo de informações possíveis. Assim, se for escolhido um
conjunto de 8 bits para representar um conjunto de informações, o máximo
possível será de 28 = 256 possíveis informações. A regra geral é que o
número máximo de informações possíveis representáveis por n bits será de
2n. Por exemplo, se o nosso conjunto de informações fossem os símbolos
numéricos do sistema decimal, do 0 ao 9 (10 informações ao todo), somente
serão necessários 4 bits (24 = 16) e ainda sobrariam 6 possíveis informações.
36
3.1. Sistemas de Numeração
Os sistemas de numeração [4] são os vários sistemas de notação que são ou
têm sido usados para representar quantidades abstratas chamadas de
números. Um sistema de numeração é definido pela base que utiliza, ou seja,
o número de símbolos diferentes requeridos para que o sistema possa
representar qualquer série infinita de números. Desta forma, o sistema
decimal, utilizado por quase todos os habitantes do planeta (exceto para
aplicações de computadores), requer dez símbolos diferentes, também
chamados de dígitos, para representar os números, sendo um sistema de base
10.Ao longo da história, muitos sistemas de numeração diferentes têm sido
usados, de fato, qualquer número acima de 1 pode ser usado como base.
Algumas culturas têm usado sistemas baseados nos números 3, 4 e 5. Os
babilônios usavam o sistema sexagesimal, baseado no número 60, e os
romanos usavam (para certos propósitos), o sistema duodecimal, baseado no
número 12. Os Maias usavam um sistema bigecimal, baseados no número
20. O sistema binário, baseado no número 2, foi utilizado por algumas tribos
e, junto com o sistema octal baseado no 8 e no hexadecimal, baseado no 16,
são usados hoje em dia em sistemas microprocessados.
Figura 3-1 – Antigos sistemas de numeração[14]
3.1.1. Valores de acordo com a Posição
O sistema universalmente adotado para a notação matemática, hoje em dia, é
o sistema decimal (exceto para sistemas de computação). A posição do
símbolo num sistema de base 10 denota o valor deste em termos de valores
exponenciais da sua base (regra também válida em outros sistemas). Isto é,
no sistema decimal a quantidade representada pela combinação dos dez
símbolos utilizados ( 0, 1, 2, 3, 4, 5, 6, 7, 8, e 9 ) depende da posição no
número. Assim, o número 3098323 é uma abreviação para:
(3 × 106) + (0 × 105) + (9 × 104) + (8 × 103) + (3 × 102) + (2 × 101) + (3 × 100).
O primeiro “3” (lendo da direita para a esquerda) representa três unidades; o
segundo “3” representa trezentas unidades; e o terceiro “3”, três milhões de
37
unidades. Neste sistema o zero representa duas funções muito importantes:
indica a não existência ou nada, e também serve para indicar os múltiplos de
base 10, 100, 1000 e assim sucessivamente. Também é usado para indicar as
frações de valores inteiros: 1/10 pode ser escrito como 0.1, 1/100 , como 0.01
e assim sucessivamente.
Dois dígitos são suficientes para representar um número no sistema binário; 6
dígitos (0, 1, 2, 3, 4, 5) são necessários para representar um número no
sistema sexagesimal; e 12 dígitos (0, 1, 2, ,3 , 4, 5, 6, 7, 8, 9, d (símbolo para
o dez), z (símbolo para o onze)) são necessários para representar um número
no sistema duodecimal. O número 30155 no sistema sexagesimal equivale a:
(3 × 64) + (0 × 63) + (1 × 62) + (5 × 61) + (5 × 60) = 3959 no sistema
decimal.
O número 2zd no sistema duodecimal equivale a (2 × 122) + (11 × 121) + (10
× 120) = 430 no sistema decimal.
Para escrever um número n de base 10 como um número de base b, deve-se
dividir (no sistema decimal) n por b desprezando os valores fracionários,
depois dividir o quociente por b novamente, e assim sucessivamente, até que
seja obtido um quociente igual a zero. Os restos sucessivos da divisão são os
dígitos da expressão de n no sistema de base b. Por exemplo, para expressar o
número 3959 decimal no seu equivalente de base 6 temos:
Multiplicador[15] Divisor Resto
3959 6
659 5
109 5
18 1
3 0
0 3
Assim temos que 395910 = 301556. A base normalmente é escrita na forma
de subscrito do número. Quanto maior for a base, maior o número de
símbolos requeridos, porém, menos dígitos serão necessários para expressar
um dado número. O número 12 é conveniente como base devido que e
exatamente divisível por 2, 3, 4 e 6, por esta razão alguns matemáticos têm
adotado o sistema de base 12, no lugar do sistema de base 10.
3.1.2. Sistema Binário
O sistema binário tem um papel muito importante na tecnologia da
computação. Qualquer número decimal pode ser expresso no sistema binário
pela soma das diferentes potências de dois. Por exemplo, começando pela
direita 10101101 representa (1 × 20) + (0 × 21) + (1 × 22) + (1 × 23) + (0 × 24)
+ (1 × 25) + (0 × 26) + (1 × 27) = 173.
38
Esse exemplo pode ser utilizado para converter números binários em
decimais. Para a conversão de números decimais em binários, procede-se
pelo método de divisões sucessivas, armazenando os restos das divisões,
como visto anteriormente.
As operações aritméticas de sistemas binários são extremamente simples. As
regras básicas são : 1 + 1 = 10, e 1 × 1 = 1. O zero funciona como no sistema
decimal: 1 × 0 = 0, e 1 + 0 = 1. A soma, a subtração e a multiplicação são
executadas de forma similar ao sistema decimal:
Já que somente existem dois dígitos (ou bits) envolvidos, os sistemas binários
são utilizados em computadores, uma vez que qualquer número binário pode
ser representado, por exemplo, pela posição de uma série de chaves liga-
desliga. A posição “liga” poderia corresponder ao 1, e a posição “desliga”
poderia corresponder ao 0. No lugar de chaves, podem ser utilizados pontos
magnetizados de um disco ferromagnético ou magneto-óptico, onde a
magnetização numa direção indica um 1, e na outra um 0. Também podem
ser utilizados Flip-Flops, que são dispositivos eletrônicos que podem ter
somente uma tensão elétrica (ou estado) de saída e podem ser chaveados para
o outro estado através de um pulso elétrico. Os circuitos lógicos nos
computadores executam as diferentes operações com números binários, sendo
que a conversão de números decimais para binários, e vice-versa, é feita
eletronicamente.
Base 10
(Decimal)
Base 2 (Binário)
8 4 2 1
0
1
2
3
4
5
7
8
9
10
11
12
13
14
15
0 0 0 0
0 0 0 1
5 = 0 1 0 1
Base 10 Base 2
8 4 2 1
5 0 1 0 1
5 = (0 x 8) + (1 x 4) + (0 x
2) + (1 x 1)
0 0 1 0
0 0 1 1
0 1 0 0
0 1 0 1
0 1 1 1
1 0 0 0 Na base 10 as colunas são
organizadas pelo peso das
potências de 10 (unidades
1 0 0 1
1 0 1 0
39
1 0 1 1 = 100, dezenas = 101,
centenas = 102, e milhares
103).
Na base 2, as colunas estão
organizadas pelo peso das
potências de 2 (unidades =
20, duplas = 21,
quádruplas = 22 e óctuplas
= 23. Este formato é
chamado de 8-4-2-1 e é
usado também nos
computadores.
1 1 0 0
1 1 0 1
1 1 1 0
1 1 1 1
Tabela 3-1 – Sistema binário
A maioria dos computadores opera usando lógica binária. O computador
representa valores usando dois níveis de tensão elétrica (usualmente 0 e
+5V). Com esses dois níveis pode-se representar exatamente dois valores
diferentes. Por convenção, nomeamos estes dois valores possíveis como zero
e um. Esses dois valores, coincidentemente, correspondem aos dois dígitos
usados no sistema de numeração binário. Uma vez que existe uma
correspondência entre os níveis lógicos usados nos microprocessadores e os
dois dígitos usados em sistemas binários, existirá uma identificação perfeita
entre o sistema de numeração e o sistema físico.
3.1.3. Formatos Binários
Teoricamente um número binário pode conter um número infinito de dígitos
(ou bits). Por exemplo, pode-se representar o número 5 por:
101000001010000000000101...000000000000101
Ainda qualquer número 0 pode preceder o número binário sem mudar o seu
valor. Por analogia ao sistema decimal, podemos ignorar os zeros colocados à
esquerda. Por exemplo, o valor 101 representa o número 5 por convenção.
Num dispositivo microprocessador, por exemplo, um 8051 que trabalha com
grupos de 8 bits, a interpretação dos números binários é facilitada
representando-os com um conjunto múltiplo de 4 ou 8 bits. Assim, seguindo
a convenção, podemos representar o número cinco como 01012 ou 000001012.
Quando o número de dígitos for muito grande no sistema decimal, é usual
separar os múltiplos de 1000 através de pontos para facilitar a leitura. Por
exemplo, o número 4.294.967.296 fica mais fácil de interpretar que
4294967296. Da mesma forma, para números binários extensos écomum
adotar uma técnica semelhante, que consiste em separar os dígitos em grupos
de 4 bits separados por um espaço, o que é adequado para a representação no
sistema hexadecimal. Por exemplo, o número binário 1010111110110010
será escrito como 1010 1111 1011 0010.
Freqüentemente são compactadas informações diferentes no mesmo número
40
binário. Por exemplo, uma das formas da instrução MOV do processador
80x86 utiliza o código de 16 bits 1011 0rrr dddd dddd para empacotar três
informações no mesmo número: cinco bits para o código de operação
(10110), um campo de três bits para indicar o número do registrador (rrr) e
um valor numérico de oito bits (dddd dddd). Por conveniência, designa-se
um valor numérico às posições de cada bit:
O bit que fica na extremidade direita do número binário é o bit da posição
zero, também chamado de bit menos significativo ou LSB[16].
A cada bit à esquerda é atribuído um número sucessivo até o bit que fica na
extremidade esquerda do número, também chamado de bit mais significativo
ou MSB[17].
Um número de 8 bits tem posições que vão do zero até sete.
Bit x x x x x x x x
Posição 7 6 5 4 3 2 1 0
Um número de 16 bits tem posições que vão do zero até quinze.
Bit x x x x x x x x x x x x x x x x
Posição 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
41
3.2. Organização dos Dados
Na matemática pura, um valor pode ser representado por um valor arbitrário
de bits. Por outro lado, em aplicações de computadores, geralmente se
trabalha com um grupo específico de bits, dependendo do microprocessador
utilizado. Coleções de bits comuns são grupos de quatro bits (chamados
nibbles), de oito bits (chamados bytes), grupo de n bits (chamados de
words). Por exemplo, os microprocessadores Pentium utilizam words de 32
bits (4 bytes), alguns microcontroladores PIC utilizam 14 bits de palavra de
código e 1 byte (8 bits) de dados.
3.2.1. Bits
A menor unidade de dados em um computador binário é um único bit. Foi
visto que um bit é capaz de representar somente um de dois valores diferentes
(tipicamente zero e um). Baseado nesta suposição, pode-se ter a impressão de
que existe um número muito pequeno de itens que podem ser representado
com um único bit. Isso não é necessariamente verdade, desde que há um
número infinito de itens que podem ser representados por um único bit.
Com um único bit podem ser representados quaisquer dois itens diferentes.
Exemplos incluem zero ou um, verdadeiro ou falso, ligado ou desligado,
macho ou fêmea, certo ou errado, azul e vermelho, etc.. Dessa forma, não há
limite para representar tipos de dados em binário (i.e. aqueles objetos que
podem ter somente um de dois valores distintos). Pode ser utilizado um único
bit para representar os números 966 e 1326, ou no lugar destes, 6242 e 6.
Também podem ser representados dois objetos não relacionados entre si, com
um único bit, como por exemplo, a cor vermelha e o número 3926.
Generalizando ainda mais, bits diferentes podem representar coisas diferentes,
por exemplo, um bit pode ser utilizado para representar os valores zero e um,
enquanto o bit adjacente pode ser utilizado para representar os valores
verdadeiro e falso. Agora como podemos saber o que os bits significam
somente olhando para eles ? A resposta é que não há como. Mas isso mostra
tudo o que está por trás das estruturas de dados do computador: os dados são
o que o programador define que sejam. Se um programador utilizar um bit
para representar o valor booleano (verdadeiro ou falso), então aquele bit (por
definição do programador) representará verdadeiro ou falso. Para que esse bit
tenha significado, o programador deverá ser consistente, ou seja, se utilizar
um bit para representar verdadeiro ou falso em um ponto do seu programa,
não poderá usar os valores verdadeiro ou falso usados naquele bit para
representar vermelho ou azul depois.
A maioria dos itens que podem ser modelados, requer mais que dois valores
diferentes, desta forma, bits isolados são o tipo de dados menos utilizado
42
quando há informações mais complexas.
3.2.2. Nibbles
Um nibble é uma coleção de 4 bits. Não é necessariamente uma estrutura de
dados interessante, exceto por duas coisas: para os números BCD[18] e para
os números hexadecimais. São necessários 4 bits para representar um único
dígito BCD ou hexadecimal. Com um único nibble pode-se representar até 16
valores distintos (24). No caso dos números hexadecimais, os valores 0, 1, 2,
3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, e F são representados por quatro bits. Os
números em BCD utilizam dez dígitos diferentes dígitos (0, 1, 2, 3, 4, 5, 6, 7,
8, 9) e também requerem de quatro bits. De fato, quaisquer dezesseis valores
distintos podem ser representados por um nibble..
3.2.3. Bytes
Sem dúvidas, a estrutura mais importante usada nos computadores é o byte.
Um byte consiste de um grupo de oito bits.
Como um byte contém 8 bits, ele pode representar 28 ou seja 256 valores
diferentes. Geralmente, é usado um byte para representar valores numéricos
na faixa de 0 a 255, ou números com sinal na faixa de –128 a +127. Os
caracteres ASCII são outro tipo especial de dados que requerem mais de 256
valores diferentes para identificar as letras, números e principais símbolos do
alfabeto ocidental.
3.2.4. Words
Uma word é um grupo de n bits. A maioria dos novos sistemas trabalha com
words de 16 bits, mas também existem sistemas com words de 8, 14, 32, 64 e
128 bits.
As words são usualmente subdivididas em nibbles ou em bytes de acordo
com a conveniência, devido ao seu grande número de bits.
Uma word de 16 bits pode representar 65536 valores diferentes (216), que
poderão ser valores numéricos positivos (unsigned) na faixa de 0 a 65535, ou
valores com sinal (signed) de –32768 a +32767, ou ainda qualquer tipo de
dado com não mais de 65536 valores. Alguns usos típicos para este tipo de
variável são para representar valores numéricos inteiros, offsets[19] e
segmentos de endereços de áreas de memória ou endereços de I/O.
3.2.5. Double Words
Uma double word é um tipo de dado com o tamanho de duas words.
Normalmente, dividida em uma word de ordem maior ou mais significativa, e
uma word menos significativa.
Os sistemas 80x86 utilizam por exemplo, words de 16 bits e double words de
32 bits. Uma double word de 32 bits pode representar um número inteiro sem
43
sinal na faixa de 0 a 4294967295 ou um número inteiro com sinal na faixa de
–2147483648 a 2147483647.
3.2.6. Números com Ponto Flutuante
Em essência, os computadores são máquinas que operam números inteiros,
mas são capazes de representar números reais pela utilização de códigos
complexos. O código mais popular para representar números reais é o padrão
definido pela IEEE.
Os computadores processam a informação em conjuntos de bits. Não há
como colocar um ponto fracionário pela natureza física do armazenamento.
Um número real pode ser representado em notação exponencial, i.e. por um
valor inteiro multiplicado pela base elevada a um expoente. Por exemplo, o
número 3.26 pode ser representado por 326 x 10-2. Utilizando esse tipo de
representação, podemos definir um número real utilizando somente números
inteiros, i.e. armazenando a informação da mantissa (326) e o expoente (-2)
em um conjunto de bits.
O termo “ponto flutuante” deriva do fato de que não há um número fixo de
dígitos antes ou depois do ponto decimal; i.e. o ponto decimal pode flutuar.
Existem, também, representações nas quais o número de dígitos, antes e
depois do ponto, é fixo. Estas são chamadasde representações de ponto
fixo. Em geral, as representações de ponto flutuante são lentas e menos
exatas que as representações de ponto fixo, mas elas podem manipular uma
faixa maior de números.
Assim deve ser notado que a maioria dos números em ponto flutuante que um
computador pode representar são somente aproximações. Um dos desafios da
programação, com variáveis de ponto flutuante, é de assegurar que as
aproximações levem a resultados precisos. Se o programador não tiver
cuidado, pequenas discrepâncias nas aproximações podem levar a resultados
finais errados.
Devido ao fato que a matemática de ponto flutuante requerer grande parte dos
recursos do computador (memória e tempo de processamento), muitos
microprocessadores são equipados com um chip chamado de FPU[20],
especializado em executar a aritmética de ponto flutuante, sendo também
chamados de coprocessadores matemáticos.
Usualmente os números de ponto flutuante são representados em 32 bits (4
bytes), mas também há representações em 64 (double, 8 bytes), 80 (long
double, 10 bytes) e 128 bits (16 bytes). Um número float representado em 32
bits pode armazenar valores na faixa de 3.4E+/-38 (7 dígitos), um double de
64 bits, 1.7E+/-308 (15 dígitos) e um long double de 80 bits, 1.2E+/-4932 (19
dígitos).
44
45
3.3. O Sistema de Numeração Hexadecimal
O grande problema de trabalhar com o sistema binário é o grande número de
dígitos para representar valores relativamente pequenos. Para representar o
valor 202 precisamos de 8 dígitos binários. A versão decimal requer
somente três dígitos decimais, e desta forma, pode representar os números de
forma muito mais compacta que o sistema de numeração binário. Esse fato
não era muito aparente quando os engenheiros projetavam os primeiros
sistemas computacionais binários.
Quando se começa a manipular grandes valores, os números binários
rapidamente ficam muito extensos. Desafortunadamente, os computadores
trabalham em binário, de modo que na maioria das vezes é conveniente usar o
sistema de numeração binário. Uma solução seria converter os números
binários para o sistema decimal, mas a conversão não é uma tarefa trivial. O
sistema hexadecimal (de base 16) oferece as duas características desejadas:
são compactos e simples de convertê-los em binário e vice-versa. Por causa
disso, a maioria dos computadores de hoje em dia usam a representação
hexadecimal.
Uma vez que a base do sistema hexadecimal é dezesseis, cada dígito à
esquerda do ponto decimal representa algum valor, vezes as potências
sucessivas de 16. Por exemplo, o número 123416 é equivalente a:
1 x 163 + 2 x 162 + 3 x 161 + 4 x 160
ou
4096 + 512 + 48 + 4 = 4660 (decimal).
Cada dígito hexadecimal pode representar um de dezesseis valores entre 0 e
15. Uma vez que existem somente dez dígitos decimais, é necessário
adicionar seis símbolos adicionais para os dígitos que representarão os valores
de 10 a 15. Ao invés de criar novos símbolos para estes dígitos, foram
redefinidas as letras A à F para simbolizar as novas quantidades. O exemplo
que segue é um número hexadecimal válido:
1234 DEAD BEEF 0AFB FEED DEAF
Usualmente é colocada a letra H (ou h) no final do número em hexadecimal,
para deixar a base explícita. A seguir alguns exemplos de números em
hexadecimal.
1234h
0DEADH
0BEEFh
2AFBh
6567FEEDh
46
Como pode ser observado, os números em hexadecimal são compactos e
fáceis de ler. Além disso, a relação entre os números em hexadecimal e
binário é direta, isto é, devido ao fato da base 16 ser uma potência exata da
base 2. Um dígito em hexadecimal é representado por um nibble. A
conversão é feita segundo a seguinte tabela:
Binário Hexadecimal
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 A
1011 B
1100 C
1101 D
1110 E
1111 F
Tabela 3-2 – Equivalência entre os sistemas binário e hexadecimal
A tabela mostra toda a informação necessária para converter qualquer número
hexadecimal em um número binário e vice-versa.
Para converter um número hexadecimal em números binários, simplesmente
substituem-se os quatro bits correspondentes para cada dígito hexadecimal.
Por exemplo, para converter 0ABCDh em um valor binário, simplesmente
converte-se cada dígito hexadecimal utilizando a tabela acima, assim:
0 A B C D Hexadecimal
0000 1010 1011 1100 1101 Binário
Para converter um número binário no formato hexadecimal procede-se da
seguinte forma: o primeiro passo é deixar o número binário com o número de
dígitos divisível por quatro, colocando zeros à esquerda se for necessário. Por
exemplo, dado o número binário 1011001010 com dez dígitos; acrescentam-
se 2 dígitos à esquerda para obter um número de dígitos divisível por quatro
(12 bits), obtendo o número 001011001010. O passo seguinte é separar o
número binário em grupos de quatro bits; assim tem-se 0010 1100 1010.
Finalmente compara-se cada grupo de quatro bits na tabela e substitui-se
pelos dígitos equivalentes em hexadecimal, ficando assim 2CAh.
A tabela de conversão normalmente é memorizada em poucos minutos, o que
47
agiliza o processo de conversão entre esses dois sistemas.
48
3.4. Operações Lógicas
Existem quatro operações lógicas principais para trabalhar com números
binários: AND, OR, XOR [21] e NOT. O operador AND precisa de pelo
menos duas variáveis binárias, e o seu resultado é mostrado a seguir:
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
Uma forma mais compacta de representação é a forma tabular, chamada de
tabela verdade. Usualmente o operador AND pode ser substituído pelo
símbolo “.”.
As funções lógicas também são implementadas em hardware (portas lógicas)
possuindo símbolos especiais como mostra na Figura 3-2.
Figura 3-2 – Porta AND
“Se ambos os operandos de uma função AND são verdadeiros, o resultado
será verdadeiro; caso contrário o resultado será falso”.
Um fato importante de notar acerca do operador lógico AND é que o mesmo
pode ser utilizado para forçar um resultado zero. Se um dos operandos for
zero, o resultado será sempre zero independente do valor do segundo
operando. Esta característica da operação AND é muito importante,
especialmente quando se trabalha com conjuntos de bits e for necessário
forçar certos bits para zero, mantendo os outros intactos.
O operador OR lógico possui a seguinte característica:
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
Figura 3-3 – Porta OR
Usualmente o operador OR pode ser substituído pelo símbolo “+”.
49
“Se um dos operandos da função OR for verdadeiro, o resultado será
verdadeiro. O resultado falso será obtido somente quando os dois operadores
forem falsos”.
Um fato importante de notar-se acerca do operador lógico OR é que o mesmo
pode ser utilizado para forçar um resultado “1”. Se um dos operandos for
“1”, o resultado será sempre “1” independente do valor do segundo
operando. Esta característica da operação OR é muito importante,
especialmente quando se trabalha com conjuntos de bits e for necessário
forçaralguns destes para “1”, mantendo os outros intactos.
O operador lógico XOR é definido como segue:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0
Usualmente o operador XOR pode ser substituído pelo símbolo “ ”. A
tabela verdade e o símbolo de hardware é mostrado a seguir.
Figura 3-4 – Porta XOR
“O resultado da operação XOR é falso se ambos os operandos forem iguais;
caso contrário o resultado é verdadeiro”
O resultado da operação XOR pode também ser definido como tendo valor
um quando qualquer um dos operandos for igual a um e não ambos ao mesmo
tempo.
Esta característica do operador XOR é útil para inverter bits de forma seletiva
em uma cadeia de bits.
O operador NOT é de operando único e é definido como mostra a seguir:
NOT 0 = 1
NOT 1 = 0
Usualmente o operador NOT pode ser substituído por uma barra em cima da
variável. A tabela verdade e o símbolo de hardware são mostrados a seguir.
50
Figura 3-5 – Porta NOT
“O operando NOT inverte a entrada, a saída será verdadeira se a entrada for
falsa e vice-versa”.
Outros operadores usuais são o NAND (NOT AND) e NOR (NOT OR), cujos
símbolos e tabelas verdade são mostrados a seguir.
Figura 3-6 – Portas NAND e NOR
51
3.5. Operações Lógicas em Números Binários e Cadeias de Bits
Como descrito na seção anterior, os operadores lógicos trabalham com
operandos de um único bit. Os microprocessadores em geral utilizam grupos
de 8, 16 ou 32 bits para representar a informação. Os conceitos anteriores
podem ser expandidos para o seu uso com variáveis de mais de um bit.
Usualmente as operações lógicas implementadas no processador operam na
base do bit a bit (bitwise). Dados dois valores representados com um
conjunto de bits maior que um, os operadores operam com o primeiro bit de
cada valor, dando o primeiro bit do resultado, e assim sucessivamente para os
posteriores. Por exemplo, se tiver que calcular um AND lógico de dois
valores representados por oito bits cada, a operação AND será feita em cada
coluna independente da outra:
1011 0101
1110 1110
________
1010 0100
Esta forma de execução bit a bit pode ser facilmente aplicada as outras
operações lógicas.
A capacidade de forçar bits para “0” e para “1” usando os operadores AND e
OR, junto com a capacidade de invertê-los usando o operador XOR, são
muito importantes quando se trabalha com conjuntos de bits. Cada bit (ou
subgrupo de bits) tem um significado diferente e independente dos outros.
Estes operadores permitem ao programador manipular os bits de forma
seletiva, i.e. sem afetar bits não desejados.
Por exemplo, tendo um valor ‘X’ de oito bits e precisa-se garantir que o os
bits quarto até o sétimo, contenham zeros, sendo que os quatros primeiros não
devem ser afetados. Neste caso poderá ser utilizada a função lógica AND do
valor ‘X’ com o valor binário 0000 1111. O operador AND lógico forçará os
quatro bits mais significativos de ‘X’ para zero, mantendo os quatro bits
menos significativos não modificados.
De forma análoga, por exemplo, pode ser forçado o bit menos significativo de
‘X’ para 1 utilizando o operador OR com o número binário 0000 0001.
Utilizar operadores lógicos AND, OR, XOR e NOT, para manipular cadeias
de bits, é um processo conhecido como mascaramento de bits. O termo
mascaramento é utilizado quando são usados certos valores (um para AND,
zero para OR e XOR) para mascarar certos bits a partir de operações que
forçam estes para “0”, “1” ou os invertem.
52
53
3.6. Números com Sinal e sem Sinal
Nas seções anteriores os números formam tratados como sendo valores sem
sinal. O número binário ....00000 representa 0, ....000001 representa 1,
...00000010 representa 2, e assim sucessivamente até o infinito. A questão é
como representar números negativos? Os valores com sinal (signed) têm sido
mencionados nas seções anteriores, mas não foram definidas as regras de
representação dos números binários.
Para representar números com sinal usando o sistema de numeração binário
devem ser colocadas certas restrições, já que estes possuem um número finito
de bits quando se trabalha com computadores que possuem recursos bastante
limitados. Usualmente essas limitações são definidas pelo tipo de dado e,
portanto, o seu tamanho em número de bits.
Um número fixo de bits pode representar certo número de objetos. Por
exemplo, com oito bits pode-se representar 256 objetos diferentes. Os valores
negativos são objetos também, da mesma forma que os números positivos.
Desta forma, podemos utilizar alguns dos 256 valores diferentes para
representar os números negativos. Em outras palavras, teremos que sacrificar
alguns números positivos para representar os números negativos.
Para executar isso da forma mais lógica, será designada uma das metades das
possíveis combinações para os valores negativos, e a segunda metade para os
positivos. Assim, pode-se representar os valores negativos de –128 a –1 e os
valores positivos de 0 a +127 com um único conjunto de 8 bits. Com uma
palavra de 16 bits poderão ser representados valores na faixa de –32768 a +
32767. Com uma palavra de 32 bits pode-se representar valores na faixa de
2147483648 a +2147483647. De forma geral, com n bits podem ser
representados valores com sinal na faixa de -2(n-1) a +2(n-1)-1.
Existem muitas formas de organizar os números negativos em binário para a
faixa definida, mas a maioria dos sistemas microprocessados adota o padrão
chamado de “complemento de dois”. Num sistema que utiliza o
complemento de dois, o bit mais significativo é o bit de sinal. Se o bit mais
significativo for zero, o número é positivo, caso contrário, será negativo.
Observar os seguintes exemplos:
Para números de 16 bits com sinal:
8000h é negativo pois o bit de sinal está em 1 (1000 0000 0000
0000).
0100h é positivo porque o bit mais significativo está em 0 (0000
0001 0000 0000).
7FFFh é positivo (0111 1111 1111 1111).
FFFFh é negativo (1111 1111 1111 1111).
54
0FFFh é positivo (0000 1111 1111 1111).
Se o bit mais significativo é “0”, então o número é positivo e armazena um
valor em binário padrão. Se o bit mais significativo for “1”, então o número é
negativo e é armazenado na forma de complemento de dois. Para converter
um número positivo no seu equivalente negativo na forma de complemento de
dois, utilizar o seguinte algoritmo:
1. Inverter todos os bits no número, i.e. deve-se aplicar o operador
NOT bit a bit.
2. Adicionar um ao número invertido resultante.
Por exemplo, para calcular o valor de oito bits equivalente a –5:
0000 0101 Cinco (em binário).
1111 1010 Invertendo todos os bits.
1111 1011 Somando um obtém-se o resultado (-5 em complemento
de dois).
Tomando o resultado obtido (-5) e procedendo-se da mesma maneira, obtém-
se novamente o valor original, com era esperado:
1111 1011 Complemento de dois para -5.
0000 0100 Invertendo todos os bits.
0000 0101 Somando um obtemos o resultado (+5).
Os exemplos a seguir ilustram alguns valores positivos e negativos de 16 bits:
7FFFh: +32767, o maior número positivo com sinal de 16-bit.
8000h: -32768, o menor número (com sinal) de 16 bits.
4000h: +16,384.
Para converter números acimasiga os passos do algoritmo, assim:
7FFFh: 0111 1111 1111 1111 +32,767
1000 0000 0000 0000 Inverter todos os bits (8000h)
1000 0000 0000 0001 Adicione “1” (8001h ou -32,767)
8000h: 1000 0000 0000 0000 -32,768
0111 1111 1111 1111 Inverter todos os bits (7FFFh)
1000 0000 0000 0000 Adicione “1” (8000h ou -32768)
4000h: 0100 0000 0000 0000 16,384
1011 1111 1111 1111 Inverter todos os bits (BFFFh)
1100 0000 0000 0000 Adicionar “1” (0C000h ou -16,384)
O valor 8000h invertido fica 7FFFh. Depois de adicionar “1” se obtém
8000h!. Ou seja +32767 + 1 = -32768 !. Fica claro que com um sistema de
numeração com sinal de 16 bits não se pode representar o valor +32768,
assim como também não se pode representar valores negativos menores que –
55
32768. Se o programador não tiver cuidado com isto, poderão acontecer erros
na implementação difíceis de serem detectados. Usualmente os
microprocessadores vêm equipados com um flag[22] que indica este tipo de
ocorrências, chamado de flag de overflow[23].
Alguns questionamentos comuns são os seguintes :
Por que usar a notação em complemento de dois ?.
Por que não usar simplesmente um bit como sinal e usando os bits
restantes para armazenar o equivalente positivo do número ?
A resposta está no hardware. Tornar valores positivos em negativos pode ser
um trabalho tedioso, mas usando o complemento de dois, a maioria das outras
operações ficam mais fáceis de serem executadas. Por exemplo, suponha que
devem ser somados dois números 5 + (-5) representados com oito bits. O
resultado é zero. Considere o que acontece quando são somados estes dois
valores num sistema que usa o complemento de dois:
00000101
11111011
_________
1 00000000
No final da soma existe um vai-um (carry) que deverá ocupar o nono bit,
sendo que os outros são iguais a zero. Se inicialmente o bit de vai-um for
ignorado, o resultado da soma de dois valores com sinal sempre produzem o
resultado correto, quando usam o sistema de complemento de dois. Isso
permite que possa ser utilizado o mesmo hardware para somas e subtrações de
números com sinal e sem sinal. Para outros tipos de representação isso não é
verdade.
De qualquer maneira, deve ser notado que os dados representados por um
grupo binário de bits depende inteiramente do contexto. O valor binário de
oito bits 1100 0000 pode representar um caractere ASCII, um valor decimal
(192), o valor –64, uma cor, etc.. O programador tem como responsabilidade
a utilização desses dados de forma consistente.
56
3.7. Extensão de Sinal e de Zeros
Uma vez que o formato de complemento de dois tem comprimento fixo, surge
um pequeno problema: o que acontecerá se for necessário converter um
número de oito bits em complemento de dois para um valor de 16 bits ?. Esse
problema e o seu inverso (converter um valor de 16 bits num outro de 8 bits)
podem ser tratados via operações de extensão de sinal e contrações. A
extensão de zeros permite converter pequenos valores sem sinal em grandes
valores, também sem sinal.
Considere o valor –64. O valor em oito bits em complemento de dois para
este número é C0h. O equivalente de 16 bits para o mesmo valor é FFC0h.
Agora considere o valor +64. As versões deste valor para oito e dezesseis bits
são respectivamente 40h e 0040h. A diferença entre os números de oito e
dezesseis bits pode ser descrita pela seguinte regra: “Se o número for
negativo, o byte mais significativo do número de 16 bits conterá FFh; se o
número é positivo, o byte mais significativo conterá 00h”. Isso é válido
convertendo um número com sinal de oito bits num equivalente de dezesseis e
não ao contrário.
Para fazer a extensão de sinal de um valor para o seu equivalente com maior
número de bits, simplesmente deve-se copiar o bit de sinal em todos os bits
adicionais do novo formato. Por exemplo, para estender o sinal em um
número de oito bits para um de dezesseis, simplesmente deve-se copiar o
sétimo bit (mais significativo) do número de oito bits nos bits 8 a 15 do novo
número de 16 bits. Para estender o sinal de um número de 16 bits para um
número de 32, simplesmente deve-se copiar o bit 15 nos bits 16 a 31 do novo
formato.
A extensão de sinal é necessária quando se manipulam valores com sinal de
tamanhos diferentes. Freqüentemente é necessário adicionar um byte com
uma word. Para isso deve-se estender o sinal do byte antes de efetuar a
operação. Outras operações, tais como, multiplicação e divisão,
especialmente, podem requerer uma extensão de sinal de 32 bits. A extensão
de sinal não vale para sistemas de números sem sinal (unsigned).
Exemplos de extensão de sinal:
f Dezesseis bits Trinta e dois bits
80h FF80h FFFFFF80h
28h 0028h 00000028h
9Ah FF9Ah FFFFFF9Ah
7Fh 007Fh 0000007Fh
- 1020h 00001020h
- 8088h FFFF8088h
Tabela 3-3 - Exemplos de extensão de sinal
57
Para estender um número sem sinal (unsigned) deve ser utilizada a extensão
de zeros. A extensão de zeros é direta, onde os zeros são diretamente
colocados nos lugares vagos mais significativos. Por exemplo, para estender
os zeros do valor 82h de oito bits, para o equivalente de dezesseis,
simplesmente deve-se adicionar zeros no byte mais significativo, obtendo
0082h.
Oito bits Dezesseis bits Trinta e dois bits
80h 0080h FFFFFF80h
28h 0028h 00000028h
9Ah 009Ah FFFFFF9Ah
7Fh 007Fh 0000007Fh
- 1020h 00001020h
- 8088h 00008088h
Tabela 3-4 – Mais exemplos de extensão de sinal
A contração de sinal, convertendo um valor em outro idêntico, com número
menor de bits, é um pouco mais complicada. A extensão de sinal vista
anteriormente nunca falha. Dado um número com sinal de m bits, sempre
poderá ser convertido num número de n bits, se n > m, usando a extensão de
sinal. Desafortunadamente, dado um número de n bits, não pode ser sempre
convertido num número de m bits se m < n. Por exemplo, considerar o valor
–448. Usando 16 bits a sua representação é 0FE40h. A magnitude desse
número é maior daquela que pode ser comportada num valor de oito bits, de
forma que não pode ser contraído em oito bits. Esse é um exemplo de uma
condição de overflow que pode ocorrer durante a conversão.
Para poder contrair apropriadamente um valor em outro, deve ser verificado
o(s) byte(s) mais significativo(s) que se desejam descartar. O byte mais
significativo que se deseja remover deverá conter 00h ou FFh. Se tiver
qualquer outro valor, o número não poderá ser contraído sem efetuar um
overflow. Finalmente o bit mais significativo do valor resultante deve ter o
mesmo valor que os que foram removidos do número.
Exemplos para números de 16 bits.
FF80h pode ser contraído em 80h
0040h pode ser contraído em 40h
FE40h não pode ser contraído em 8 bits.
0100h não pode ser contraído em 8 bits.
58
3.8. A Codificação ASCII
ASCII são as siglas para American Standard Code for Information
Interchange. É um código que designa valores numéricos às letras, números,
sinais de pontuação e outros símbolos especiais. Esse padrão foi criado para
organizar e compatibilizar a troca de informação entre vários computadores e
sistemas.
O ASCII define 256 códigos divididos em dois conjuntos, o padrão e o
estendido, com 128 códigos cada. Estes grupos representam o total de
representações possíveis de 8 bits (1 byte). O conjunto básico ou padrão,
utiliza 7 bits para cada código, usando do código 0 até o 127 (00h a 7Fh),
sendo queo conjunto estendido utiliza os códigos de 128 a 255 (80h a FFh).
No conjunto padrão, os primeiros 32 caracteres são designados para os
códigos de comunicação e controle de impressão (basicamente caracteres não
imprimíveis), tais como, retorno de posição, retorno de carro, nova linha e
tabulação, que são utilizados para controlar a forma com que a informação é
transferida de um computador para outro sistema. Os 96 códigos
remanescentes são designados aos sinais de pontuação, os dígitos 0 ao 9, e as
letras maiúsculas e minúsculas do alfabeto romano.
O conjunto estendido de códigos é designado para grupos variáveis de
caracteres para serem usados pelos fabricantes de computadores e
engenheiros de software. Esses códigos não são intercambiáveis entre
diferentes programas de computador como o conjunto padrão de caracteres
ASCII.
3.8.1. O Grupo Padrão de Caracteres
O grupo padrão de caracteres pode ser dividido em quatro subgrupos de 32
caracteres. Os primeiros 32 caracteres (código 00h a 1Fh) formam um grupo
especial de caracteres não imprimíveis chamados caracteres de controle
porque executam várias funções de controle de impressão.
Desafortunadamente, os caracteres de controle executam diferentes operações
em dispositivos diferentes de saída de dados. Existe uma padronização muito
fraca nos dispositivos de saída em geral.
O segundo subgrupo consiste em vários símbolos de pontuação, caracteres
especiais e dígitos numéricos. Os caracteres mais notáveis deste grupo
incluem o caractere “espaço” (código 20h) e os dígitos numéricos (códigos
30h a 39h). Deve ser notado que os dígitos numéricos diferem dos seus
valores numéricos somente no nibble mais significativo. Subtraindo 30h do
código em ASCII, pode ser obtido o equivalente numérico para aquele dígito.
O terceiro subgrupo de 32 caracteres é reservado para os símbolos das letras
maiúsculas do alfabeto. Os códigos ASCII para os caracteres de ‘A’ a ‘Z’
59
ficam na faixa de 41h a 5Ah. Como existem somente 26 caracteres
alfabéticos definidos, os seis códigos remanescentes servem para representar
outros símbolos especiais.
O quarto e último subgrupo de 32 caracteres é reservado para os símbolos das
letras minúsculas do alfabeto, cinco símbolos especiais e outro caractere de
controle (delete). Notar que os símbolos dos caracteres minúsculos usam os
códigos 61h a 7Ah. Convertendo os códigos para os caracteres maiúsculos e
minúsculos, nota-se que a diferença entre ambos os tipos diferem da posição
de somente um bit. Por exemplo, considerar o código para as letras ‘E’ (45h)
e ‘e’ (65h)
‘E’: 0100 0101
‘e’: 0110 0101
Ambos os códigos diferem somente no bit cinco. Os caracteres em
maiúsculas sempre contêm um “0” no bit número cinco e os caracteres em
minúsculas, em um. Essa característica pode ser facilmente usada para
converter rapidamente maiúsculas em minúsculas e vice-versa. Tendo um
caractere em maiúsculas pode ser forçado para minúsculas setando[24] o bit
número cinco. Essas tarefas são facilmente executadas utilizando as funções
lógicas vistas nas seções anteriores.
Em resumo, os bits número cinco e seis determinam o subgrupo:
Bit 6 Bit 5 Subgrupo
0 0 Caracteres deControle
0 1 Dígitos e Pontuação
1 0 Maiúsculas eespeciais
1 1 Minúsculas &especiais
Tabela 3-5 – Subgrupo de bits de controle
Dessa maneira, podem ser convertidos quaisquer caracteres maiúsculos ou
minúsculos nos seus equivalentes caracteres de controle resetando os bits
número cinco e seis.
Considere no momento, os códigos ASCII para os dígitos numéricos:
"0" 48 30h
"1" 49 31h
"2" 50 32h
"3" 51 33h
"4" 52 34h
"5" 53 35h
"6" 54 36h
"7" 55 37h
"8" 56 38h
60
"9" 57 39h
Caractere Decimal Hexadecimal
Tabela 3-6 – Valores ASCII dos dígitos numerais
A representação decimal destes códigos não é muito clara em relação ao
símbolo que representam. A representação hexadecimal deste código ASCII
revela algumas características importantes como que o nibble menos
significativo do código é equivalente ao valor do número representado. Desta
forma, resetando para zero o nibble menos significativo do código numérico
do caractere numérico, pode ser convertido no seu significado em binário
correspondente. De forma análoga, pode ser convertido um valor numérico
binário na sua representação em código ASCII simplesmente setando para “1”
os dois primeiros bits do nibble mais significativo. Notar que pode ser
utilizado um operador lógico AND para forçar os bits mais significativos para
zero, ou OR para forçá-los para um.
Deve ser notado que não é possível converter uma cadeia (string) de
caracteres numéricos para a sua representação equivalente em binário
simplesmente ajustando o nibble mais significativo para cada dígito da string.
Se for convertido o número 123 (31h 32h 33h) desta forma obteremos três
bytes: 010203h, diferente do valor correto, que deveria ser 7Bh. A conversão
de cadeias de dígitos em números inteiros, requer maior sofisticação, sendo
que a conversão sugerida anteriormente funciona somente com dígitos
isolados.
Embora, seja dado o nome de padrão ASCII, o simples uso desta codificação
não garante a compatibilidade entre sistemas. Se for verdade que a letra ‘A’
numa máquina é frequentemente uma ‘A’ na outra máquina, não existe uma
verdadeira padronização entre máquinas com respeito aos caracteres de
controle. Do total de 32 caracteres de controle mais o “delete”, existem
somente quatro códigos de controle comumente suportados: o retorno de
cursor (backspace - BS), a tabulação, retorno de carro (CR) e nova linha
(LF). O que é pior, diferentes máquinas frequentemente utilizam estes
códigos de controle de formas diferentes. O fim de linha é um exemplo
particularmente problemático. Enquanto os sistemas MS-DOS, CP/M e
outros sistemas marcam o final de uma linha com uma sequência de dois
caracteres (CR e LF), os sistemas Apple Macintosh, Apple II e outros marcam
o final de linha com um único caractere (CR).
Os sistemas UNIX marcam o final de uma linha com um único caractere LF.
Não é necessário dizer que tentando intercambiar simples arquivos de texto
entre estes sistemas pode ser frustrante. Se forem utilizados os caracteres
padrão ASCII em todos seus arquivos, será necessário converter os dados
para intercambiar dados com outro que não tem o mesmo padrão. Felizmente
tal conversão é bastante simples.
61
Outro tipo de formato que pode ser usado é o formato ANSI. Ambos os
sistemas, ASCII e ANSI, foram desenvolvidos para padronizar a comunicação
entre computadores.
Os países cujo idioma não é o inglês têm desenvolvido outros métodos de
codificação. É interessante considerar outros idiomas, tais como o japonês, o
chinês e o coreano, que possuem mais caracteres que os do alfabeto inglês.
Esse problema requer um sistema de codificação diferente do ASCII com seus
127 caracteres ou o ANSI com seus 256 caracteres. Os japoneses por
exemplo usam a codificação EUC, JIS, S-JIS, e JASCII para manipular
caracteres. A internacionalização tenta definir um padrão para todos os
códigos de caracteres num padrão universal. Um destes esquemas de
codificação é chamado UNICODE.
A seguir é apresentada a tabela de códigos ASCII padrão.
Character
Name Char Code Decimal Binary Hex
Null NUL Ctrl@ 0 00000000 00
Start of
Heading SOH
Ctrl
A 1 00000001 01
Start of Text STX CtrlB 2 00000010 02
End of Text ETX CtrlC 3 0000001103
End of
Transmit EOT
Ctrl
D 4 00000100 04
Enquiry ENQ CtrlE 5 00000101 05
Acknowledge ACK CtrlF 6 00000110 06
Bell BEL CtrlG 7 00000111 07
Back Space BS CtrlH 8 00001000 08
Horizontal Tab TAB Ctrl I 9 00001001 09
Line Feed LF Ctrl J 10 00001010 0A
Vertical Tab VT CtrlK 11 00001011 0B
Form Feed FF CtrlL 12 00001100 0C
Carriage
Return CR
Ctrl
M 13 00001101 0D
Shift Out SO CtrlN 14 00001110 0E
Shift In SI CtrlO 15 00001111 0F
Data Line
Escape DLE
Ctrl
P 16 00010000 10
Device Control DC1 Ctrl 17 00010001 11
62
1 DC1 Q 17 00010001 11
Device Control
2 DC2
Ctrl
R 18 00010010 12
Device Control
3 DC3
Ctrl
S 19 00010011 13
Device Control
4 DC4
Ctrl
T 20 00010100 14
Negative
Acknowledge NAK
Ctrl
U 21 00010101 15
Synchronous
Idle SYN
Ctrl
V 22 00010110 16
End of
Transmit
Block
ETB CtrlW 23 00010111 17
Cancel CAN CtrlX 24 00011000 18
End of
Medium EM
Ctrl
Y 25 00011001 19
Substitute SUB CtrlZ 26 00011010 1A
Escape ESC Ctrl [ 27 00011011 1B
File Separator FS Ctrl \ 28 00011100 1C
Group
Separator GS Ctrl ] 29 00011101 1D
Record
Separator RS
Ctrl
^ 30 00011110 1E
Unit Separator US Ctrl_ 31 00011111 1F
Space 32 00100000 20
Exclamation
Point !
Shift
1 33 00100001 21
Double Quote " Shift‘ 34 00100010 22
Pound/Number
Sign #
Shift
3 35 00100011 23
Dollar Sign $ Shift4 36 00100100 24
Percent Sign % Shift5 37 00100101 25
Ampersand & Shift7 38 00100110 26
Single Quote ‘ ‘ 39 00100111 27
Left
Parenthesis (
Shift
9 40 00101000 28
Right
Parenthesis )
Shift
0 41 00101001 29
Asterisk * Shift8 42 00101010 2A
Plus Sign + Shift= 43 00101011 2B
Comma , , 44 00101100 2C
63
Minus Sign - - 45 00101101 2D
Period . . 46 00101110 2E
Forward Slash / / 47 00101111 2F
Zero Digit 0 0 48 00110000 30
One Digit 1 1 49 00110001 31
Two Digit 2 2 50 00110010 32
Three Digit 3 3 51 00110011 33
Four Digit 4 4 52 00110100 34
Five Digit 5 5 53 00110101 35
Six Digit 6 6 54 00110110 36
Seven Digit 7 7 55 00110111 37
Eight Digit 8 8 56 00111000 38
Nine Digit 9 9 57 00111001 39
Colon : Shift; 58 00111010 3A
Semicolon ; ; 59 00111011 3B
Less-Than
Sign <
Shift
, 60 00111100 3C
Equals Sign = = 61 00111101 3D
Greater-Than
Sign >
Shift
. 62 00111110 3E
Question Mark ? Shift/ 63 00111111 3F
At Sign @ Shift2 64 01000000 40
Capital A A ShiftA 65 01000001 41
Capital B B ShiftB 66 01000010 42
Capital C C ShiftC 67 01000011 43
Capital D D ShiftD 68 01000100 44
Capital E E ShiftE 69 01000101 45
Capital F F ShiftF 70 01000110 46
Capital G G ShiftG 71 01000111 47
Capital H H ShiftH 72 01001000 48
Capital I I ShiftI 73 01001001 49
Capital J J ShiftJ 74 01001010 4A
Capital K K ShiftK 75 01001011 4B
Capital L L ShiftL 76 01001100 4C
Capital M M Shift 77 01001101 4D
64
Capital M M M 77 01001101 4D
Capital N N ShiftN 78 01001110 4E
Capital O O ShiftO 79 01001111 4F
Capital P P ShiftP 80 01010000 50
Capital Q Q ShiftQ 81 01010001 51
Capital R R ShiftR 82 01010010 52
Capital S S ShiftS 83 01010011 53
Capital T T ShiftT 84 01010100 54
Capital U U ShiftU 85 01010101 55
Capital V V ShiftV 86 01010110 56
Capital W W ShiftW 87 01010111 57
Capital X X ShiftX 88 01011000 58
Capital Y Y ShiftY 89 01011001 59
Capital Z Z ShiftZ 90 01011010 5A
Left Bracket [ [ 91 01011011 5B
Backward
Slash \ \ 92 01011100 5C
Right Bracket ] ] 93 01011101 5D
Caret ^ Shift6 94 01011110 5E
Underscore _ Shift- 95 01011111 5F
Back Quote ` ` 96 01100000 60
Lower-case A a A 97 01100001 61
Lower-case B b B 98 01100010 62
Lower-case C c C 99 01100011 63
Lower-case D d D 100 01100100 64
Lower-case E e E 101 01100101 65
Lower-case F f F 102 01100110 66
Lower-case G g G 103 01100111 67
Lower-case H h H 104 01101000 68
Lower-case I I I 105 01101001 69
Lower-case J j J 106 01101010 6A
Lower-case K k K 107 01101011 6B
Lower-case L l L 108 01101100 6C
Lower-case M m M 109 01101101 6D
Lower-case N n N 110 01101110 6E
65
Lower-case P p P 112 01110000 70
Lower-case Q q Q 113 01110001 71
Lower-case R r R 114 01110010 72
Lower-case S s S 115 01110011 73
Lower-case T t T 116 01110100 74
Lower-case U u U 117 01110101 75
Lower-case V v V 118 01110110 76
Lower-case W w W 119 01110111 77
Lower-case X x X 120 01111000 78
Lower-case Y y Y 121 01111001 79
Lower-case Z z Z 122 01111010 7A
Left Brace { Shift[ 123 01111011 7B
Vertical Bar | Shift\ 124 01111100 7C
Right Brace } Shift] 125 01111101 7D
Tilde ~ Shift` 126 01111110 7E
Delta 127 01111111 7F
Tabela 3-7 – Tabela ASCII
66
3.9. Exercícios
1. Para que codificar a informação?
2. Por que é utilizado o sistema binário em sistemas de informação?
3. Qual a utilidade dos números em ponto flutuante?
4. Por que são utilizados códigos no sistema hexadecimal?
5. Qual a utilidade dos operadores lógicos? Dê exemplos de aplicações.
67
4. PROJETO DE SISTEMAS DE SOFTWARE
Os tópicos que serão discutidos neste capítulo incluem:
A metodologia Top Down
Algoritmos
Fluxogramas
Compilação e Enlace
Normalmente o projeto de um sistema não envolve somente os passos da
programação e o código fonte, mas uma série de etapas importantes que
devem ser levadas em conta antes de começar o projeto.
Qualquer projeto pode ser dividido em quatro etapas principais: uma etapa de
especificação do sistema (software e hardware), uma etapa de implementação,
uma etapa de depuração e uma etapa de validação.
Todo projeto começa com uma especificação escrita e detalhada de todo os
sistema desejado, usualmente chamado de Especificação de Sistema. Nele
deverão estar descritas, da forma mais detalhada possível, todas as
características de hardware desejadas, as funções do software, os protocolos
de comunicação a serem utilizados e os cronogramas completos, entre outros
detalhes. Qualquer detalhe omitido ou esquecido nesta etapa pode inutilizar o
projeto todo. A etapa de especificação é a mais importante do projeto e deve
ser alocado o tempo suficiente para ela. É comum que projetos de software
de grande porte tenham a metade do tempo de projeto alocado somente para a
especificação.
Na etapa de implementação são construídos e definidos os algoritmos de
software[25] durante a especificação. A etapa de depuração serve para a
detecção e correção de erros que aconteceram nas interfaces entre as sub-
etapas do projeto.
A etapa de validação serve para avaliar os objetivos alcançados em relação
aos propostos na etapa de especificação, e a de encontrar problemas não
detectados nas etapas anteriores. Usualmente, devido a validação, deverá ser
executada novamente uma etapa de depuração para correção de erros e
melhorias de funcionamento, cujos resultados deverão ser validados
novamente.
As etapas mencionadas anteriormente possuem uma sub-etapa de
documentação que normalmente é executada em paralelo, e que incluem,
descrições de software e hardware, relatórios de validação, lista de erros e
melhorias a serem efetuadas, etc..
Os projetos de engenharia eletrônica, normalmente se constituem de sistemas
de hardware e de software. Nos projetos de grandes sistemas há uma
68
necessidade de estilo, abstração e formalismo.
69
4.1. Top Down
4.1.1. Estilo
A mente humana precisa de ajuda quando efetua tarefas grandes ou
complexas. O estilo é o método de particionar um problema grande em sub-
unidades manipuláveis de uma forma sistemática e inteligível. A necessidade
de ter um bom estilo fica mais aparente quando o problema é maior. Osprogramas de computador são um dos grandes tipos de projetos complexos
implementados pelo homem, alguns deles excedendo 5000000 de linhas de
código, sendo estes tão grandes que nenhuma pessoa pode acompanhar o
programa completo, e nem uma parte significativa do mesmo.
O estudo do estilo de programação tem forçado aos projetistas a encarar o
particionamento do problema como uma arte, como um caminho para ganhar
controle sobre os seus projetos. O estilo em programação pode ser definido
por um grupo de técnicas chamadas de “top down” e “estruturada”. O
hardware de grandes computadores envolve uma complexidade que fica na
mesma escala daqueles programas gigantes. Podemos citar algumas regras
para obter um bom estilo em projetos de sistemas:
Projete de cima para baixo (top for down): O projeto começa com a
especificação do sistema completo de forma suficientemente compacta
para que uma pessoa possa rapidamente compreendê-la. O projeto
procede pela divisão do sistema em subsistemas e sub-unidades com as
suas inter-relações bem definidas. Depois disso, cada subsistema poderá
ser descrito em detalhes mantendo a capacidade de compreender os
detalhes da unidade e do sistema como um todo. Esse processo continua
até o sistema ter sido especificado de forma
completa e detalhada, podendo prosseguir com a elaboração do
cronograma.
Sempre se devem utilizar técnicas que mantenham o projetista no
caminho correto, dentro do processo da implementação (técnicas
foolproof[26]). O hardware permite um alto grau de flexibilidade no
projeto. Essa excessiva flexibilidade permite aos projetistas utilizar
rotinas e circuitos complexos e pouco comuns. O uso incontrolado dessa
flexibilidade promove a implementação de forma indisciplinada, não-
inteligente e incorreta. Esse fenômeno tem a contrapartida (de forma
menos severa) em software de computadores, onde a linguagem
assembly permite o acesso a todo o poder do computador. A experiência
na solução de problemas de software e hardware tem mostrado que se
deve restringir as ferramentas e técnicas de projeto que mostrem uma
capacidade funcional e interpretativa, sobre uma variedade de
circunstâncias.
70
Usar técnicas de documentação para o nível de sistema e para nível
dos circuitos ou rotinas de software (descrições de software e hardware)
que mostrem claramente o que o projetista estava pensando, quando o
problema foi primeiramente abstraído e depois para a implementação do
software e do hardware. A violação deste preceito atua contra o
princípio da “cortesia comum”. Durante a documentação o projetista
deve-se colocar no lugar do usuário ou mantenedor do seu projeto,
mantendo uma documentação clara e completa.
4.1.2. Abstração
Neste contexto, a abstração permite encarar o problema num nível
conceptual. O conceito de memória é um exemplo de abstração. Quando o
projeto começar, é necessário encará-lo com elementos conceituais a as suas
inter-relações. Somente mais tarde, durante o processo de implementação,
será necessário trabalhar com conceitos reais. Essa liberdade é absolutamente
essencial para um começo apropriado de um projeto de complexidade
considerável. Começa-se de cima e continua-se reduzindo o problema, até
seus elementos conceituais básicos. Por exemplo, um computador precisará
de uma memória, um sistema de entrada e saída, uma unidade aritmética e
outros subsistemas.
Comumente começa-se o projeto neste nível de abstração, e prossegue-se
descendo a níveis inferiores, um por um, sempre no ponto de vista conceitual.
Desta forma, no próximo nível será desenhado um diagrama de blocos de uma
unidade aritmética, pela interconexão das suas unidades funcionais, tais como
registradores, unidades de controle, e barramento de dados. A abstração
inicial é a parte crítica de qualquer projeto, desde que, um planejamento
errado nas suas fases iniciais, levará inevitavelmente a implementações
erradas. Normalmente não há forma de resgatar um projeto mal planejado
utilizando circuitos exóticos ou rotinas de ajuste.
4.1.3. Formalismo
O formalismo é a teoria do comportamento do sistema. Em um projeto, o
formalismo ajuda a estabelecer regras e procedimentos sistemáticos com
características conhecidas. Os formalismos são importantes em todos os
níveis do projeto. Os formalismos de “alto nível” não são de grande
importância para o bom desenvolvimento do projeto e servem somente para
adotar métodos sistemáticos em todos os níveis nos quais se espera a
transformação correta dos conceitos em hardware ou software. No nível de
implementação, o formalismo é extremamente necessário e deve ser rígido o
suficiente para evitar erros na implementação.
Duas naves espaciais destinadas a mapear o planeta Marte foram perdidas no
espaço em 1999 porque alguns dos algoritmos projetados utilizavam unidades
71
do sistema inglês, enquanto que outros utilizavam unidades no sistema
internacional. Como o erro era muito pequeno para ser detectado nos testes,
resultou em um dos erros de formalismo mais caros da história.
4.1.4. O Projeto em Top Down
O projeto começa com o estudo cuidadoso do problema geral.
Deliberadamente, devem ser ignorados os detalhes neste estágio, e devem ser
feitas perguntas como:
O problema está claramente definido ?
É possível remodelar o problema para obter mais clareza ou
simplificá-lo ?
Se estivesse trabalhando com um subsistema de um sistema maior;
quais seriam as relações com o subsistema de hierarquia maior ?. Poderá
um particionamento diferente do sistema inteiro simplificar a estrutura ?
Neste estágio, o entendimento é global e deve permanecer neste nível até ter
esmiuçado e digerido o problema, chegando ao ponto onde haja o
convencimento de que este pode ser resolvido. Isto é essencial, desde que
qualquer dificuldade neste nível é séria e pode ser insolúvel.
Após ter especificado claramente o problema em nível global, procede-se ao
particionamento racional do problema em pequenas peças com inter-relações
claramente definidas. O objetivo é de escolher as peças “naturais”, de tal
forma que, cada peça possa ser compreendida como uma unidade e sejam
bem compreendidas as interações entre as unidades. Esse processo de
particionamento continua para níveis inferiores, até a escolha final das
funções a serem utilizadas, circuitos integrados a serem empregados, etc..
72
4.2. Algoritmos
Nas ciências matemáticas, um algoritmo é um método de resolver um
problema pelo uso repetitivo de métodos computacionais simples. Um
exemplo básico é o processo da divisão de números grandes. O termo
algoritmo, hoje em dia, é aplicado para vários tipos de soluções de problemas
que empregam uma sequência mecânica de passos, como nos programas de
computador. A sequência pode ser representada na forma de um diagrama de
fluxo ou fluxograma para facilitar o entendimento da sequência.
Assim como os algoritmos utilizados na aritmética, os algoritmos para
computadores podem ser simples ou altamente complexos. Em todos os
casos, a tarefa que o algoritmo vai desempenhar deve ser bem definida. Isto
é, as definições podem envolver termos matemáticos ou lógicos ou um
conjunto de dados ou instruções escritas, mas a tarefa em si deve ser de tal
forma que possa ser representada de uma maneira simples.
Em dispositivos, tais como computadores, a lógica é aforma de algoritmo.
Como os computadores aumentam em complexidade, mais e mais algoritmos
de programas de software tomam a forma do que é chamado de “hard
software”. Isto é, há um aumento da parte básica do circuito elétrico dos
computadores que facilitam a inserção de lógicas em hardware. Muitos
algoritmos de aplicação diferentes estão disponíveis, e sistemas altamente
avançados, tais como algoritmos de inteligência artificial, ficarão muito
comuns num futuro próximo.
73
4.3. Fluxogramas
Um fluxograma [4] ou diagrama de fluxo, é um diagrama sequencial
empregado em várias áreas da tecnologia para mostrar os procedimentos,
passo a passo, que devem ser executados para a implementação de certa
tarefa ou para a geração de um produto; por exemplo, para descrever
processos de manufatura, ou para resolver um determinado problema em
algoritmos.
Um fluxograma é basicamente a representação gráfica por meio de símbolos
geométricos, da solução algorítmica de um problema. Os fluxogramas são
compostos de blocos ou caixas, conectadas por setas. Para descrever o
processo descrito em um diagrama de fluxo, começa-se pelo bloco inicial e
segue-se de bloco em bloco seguindo as setas e executando as ações
indicadas. A forma de cada bloco indica o tipo de ação que esta representa,
tais como processamento, tomadas de decisão e controle.
Os blocos de processo indicam a execução de uma determinada ação.
Figura 4-1 – Bloco de processo
As caixas de decisões indicam o caminho a ser tomado de acordo com uma
condição.
Figura 4-2 – Bloco de tomada de decisão
Os blocos e as setas de conexão são suficientes para representar qualquer
diagrama de fluxo. Existem outros tipos de blocos que especificam tipos de
processos específicos, mas que não são imprescindíveis.
74
75
4.4. Componentes Básicos de um Programa
Um programa de computador é baseado no fluxograma do algoritmo e
implementa a solução de software para o problema.
Um programa em qualquer linguagem normalmente é composto pelos
seguintes itens::
Os programas devem obter informação de alguma fonte de entrada.
Os programadores devem decidir a forma em que os dados de
entradas serão armazenados e dispostos.
Os programas devem utilizar uma série de instruções para manipular
as entradas. Estas instruções são do tipo simples, condicionais, laços e
funções ou sub-rotinas.
Os programas devem apresentar os resultados da manipulação dos
dados das entradas.
Uma aplicação correta incorpora os fundamentos acima listados,
expressos através da utilização de um projeto modular, incluindo uma
especificação completa, uma codificação devidamente documentada e
um esquema de apresentação apropriado.
76
4.5. Procedimento Geral
Um programa codificado usando uma linguagem de programação, deverá ser
transformado no seu equivalente em código de máquina. Dependendo da
forma com que isso é feito, existe uma classificação em linguagens
compiladas e interpretadas. Nas linguagens interpretadas, cada linha de
programa é interpretada e posteriormente executada durante a execução do
programa, por meio de um software chamado interpretador. Um exemplo
deste tipo de linguagem é o BASIC. As linguagens compiladas são
transformadas em equivalentes de linguagem de máquina, antes da execução.
Alguns exemplos desse tipo de linguagem são as linguagens C e Pascal. As
linguagens compiladas são executadas mais rapidamente do que suas
equivalentes interpretadas, com a desvantagem de que as compiladas não
possuem verificação de erros em tempo de execução.
O primeiro passo no processo de construção de um programa é a criação dos
arquivos de código fonte, com as instruções desejadas[2]. Após isso, deve ser
invocado o Compilador que antes de efetuar a compilação executa outro
programa chamado Pré-processador que criará a entrada para o compilador.
O compilador então efetua a checagem da sintaxe e cria um arquivo objeto
que contém código de máquina, diretivas de ligação, seções, referências
externas, nome de funções e de dados gerados a partir do código fonte.
Finalmente, o Linker combina o código objeto com as bibliotecas estáticas
utilizadas e outros códigos objeto, define os recursos necessários e cria um
arquivo executável. Tipicamente, um arquivo de construção chamado
makefile coordena a combinação dos elementos e ferramentas necessárias
para a criação do arquivo executável.
4.5.1. Compilação
O compilador é um tipo de software projetado para traduzir os programas
escritos em linguagem de “alto nível” em instruções de linguagem de
máquina elementares para um tipo de computador em particular. Uma
linguagem de “alto nível” particular pode ser utilizada por muitos tipos de
computadores independendo do hardware. Por outro lado, os compiladores
são projetados para operar num tipo de máquina em particular sendo
dependente do hardware.
Os compiladores são, basicamente, programas de computador capazes de
transformar um grupo de símbolos em outro diferente, segundo um grupo de
regras sintáticas e semânticas bem definidas.
77
Figura 4-3 – Fluxograma Genérico
Os compiladores dedicados a microcontroladores, e que são executados em
computadores pessoais, são frequentemente chamados de “Cross Compilers”.
A Figura 4-4 mostra o procedimento para a geração de um programa
executável.
Figura 4-4 – Procedimento de geração de um programa executável
78
79
4.6. Exercícios
1. Por que é importante a elaboração de um projeto de software antes da
implementação do mesmo?
2. O que é mais importante num projeto? O estilo, a abstração ou o formalismo?
Justifique.
3. Quais as vantagens da metodologia Top Down?
4. Existem outras metodologias de projeto além do Top Down? Pesquise.
5. Qual a importância dos fluxogramas nos projetos de software?
6. Qual a função do compilador e do linker?
80
5. FUNDAMENTOS DA LINGUAGEM C
Os tópicos que serão discutidos neste capítulo incluem:
Características da Linguagem C
Pontos Positivos e Negativos
Palavras reservadas
Estrutura de um programa em C
Conjunto de caracteres
Diretivas de compilação
Declaração das variáveis
Introdução às entradas e saídas de dados
Breve introdução às funções
Primeiros Passos
Neste capítulo serão vistos os fundamentos da linguagem C. O conceito de
linguagem de programação, linguagens de alto e baixo nível, linguagens
genéricas e específicas. Será visto um pouco do histórico da criação da
linguagem e a descrição das características mais importantes da linguagem C.
Finalmente, será visto o aspecto geral de um código fonte escrito em C. No
Apêndice G - pode ser visto um exemplo de utilização de alguns
compiladores tradicionais.
81
5.1. Características da Linguagem C
Entre as principais características da linguagem C, pode-se citar:
É uma linguagem de “alto nível” de sintaxe estruturada e flexível,
tornando sua programação bastante simplificada.
Os programas em C são compilados, gerando programas
executáveis depois de montados (linker).
A linguagem C compartilha recursos de alto e de baixo nível, pois
permite acesso e programação direta do hardware do computador.
Assim, as rotinas cuja dependência detempo seja crítica, podem ser
facilmente implementadas usando instruções em Assembly. Por essa
razão, a linguagem C, é a preferida dos engenheiros programadores de
aplicativos.
A linguagem C é estruturalmente simples e portável. O compilador
C gera códigos menores e mais velozes do que outras linguagens de
programação.
Embora, estruturalmente simples (poucas funções intrínsecas), a
linguagem C não perde funcionalidade, pois permite a inclusão de uma
farta quantidade de rotinas do usuário. Os fabricantes de compiladores
fornecem uma ampla variedade de rotinas pré-compiladas em
bibliotecas.
82
5.2. Pontos Positivos da Linguagem
Tamanho Pequeno: A linguagem C possui poucas regras de sintaxe, quando
comparada com outras linguagens. Um compilador C pode ser implementado
com apenas 256 KB de memória.
Poucos Comandos: A linguagem C é extremamente pequena. O número
total de palavras-chave é de 43. Isto faz dela uma linguagem extremamente
simples de aprender.
Velocidade: A combinação de uma linguagem pequena com regras de sintaxe
simples; a falta de verificação durante a execução; e uma linguagem parecida
com o assembly faz com que o código gerado seja executado em velocidades
próximas a do assembler.
Linguagem Estruturada: Contém todas as estruturas de controle utilizadas
nas linguagens de programação mais modernas. Tem recursos de escopo
utilizando variáveis locais.
Não Fortemente Figurada: Os dados são tratados de maneira muito
flexível, o que permite uma grande versatilidade.
Suporte de Programação Modular: Suporta a compilação e montagem
(linker) separadas, o que permite recompilar somente as partes de um
programa que tenham sido alteradas durante o desenvolvimento.
Manipulação de Bits: Uma vez que a linguagem foi criada para a
implementação de sistemas operacionais, esta linguagem foi dotada de uma
vasta série de operadores para a manipulação direta de bits.
Interface para Rotinas em Assembly: Suporta a inclusão de rotinas em
assembly diretamente no mesmo código fonte em C.
Variáveis Ponteiros: Um sistema operacional deve ser capaz de endereçar
áreas específicas da memória ou dispositivos de I/O. A linguagem C utiliza
variáveis do tipo ponteiro permitindo manipulá-los aritmeticamente. Uma
variável tipo ponteiro guarda no seu conteúdo uma informação de endereço da
informação.
Estruturas Flexíveis: Os arranjos de dados são unidimensionais. Os
arranjos multidimensionais são construídos a partir de arranjos
unidimensionais.
Bibliotecas de Funções: Existem vastas bibliotecas de funções prontas que
podem ser anexadas aos executáveis durante a montagem (linker).
Uso Eficiente da Memória: Os programas em C tendem a ser mais
eficientes em termos de memória devido à falta de funções embutidas que não
são necessárias à aplicação.
Portabilidade: A portabilidade indica a facilidade de se converter um
83
programa feito para um hardware específico e sistema operacional, em um
equivalente que possa ser executado em outro hardware ou sistema
operacional. Atualmente, ainda pode ser considerada como uma das
linguagens mais portáveis.
84
5.3. Pontos Negativos
Não fortemente Figurada: Este fato é também um ponto negativo da
linguagem. Trata-se do processamento dos dados de acordo com a sua
natureza ou tipo de dado. Este processamento é chamado comumente de
tipagem que indica o quanto a linguagem permite a troca de dados entre duas
variáveis de tipos diferentes. O uso acidental de misturas de tipos de dados
pode gerar erros de execução no programa.
Verificação em Tempo de Execução: A linguagem C não possui verificação
em tempo de execução, de forma que podem acontecer problemas cujas
origens são muito difíceis de detectar.
85
5.4. O Padrão ANSI C
O comitê ANSI[27] desenvolveu padrões para a linguagem C.
Anteriormente, a única referência da linguagem era o livro The C
Programming Language (V. Kernighan e D. Ritchie, Laboratórios Bell,
1988). Este livro não é muito específico em certos detalhes da linguagem, o
que levou a divergências entre os fabricantes de compiladores, prejudicando a
portabilidade. O padrão ANSI surgiu para remover ambiguidades, embora,
nem todas tenham sido corrigidas, ele permanece como a melhor alternativa
para produzir um código C portátil.
O comitê ANSI adotou como norma três frases que foram denominadas “o
espírito da linguagem C”, dentre elas:
“Não impeça que o programador faça aquilo que precisa ser feito”.
“Confie no programador”.
“Mantenha a linguagem pequena e simples”.
Além disso, a comunidade internacional foi consultada para garantir que o
padrão ANSI C americano seria idêntico à versão do padrão ISO
(International Standards Organization)
86
5.5. Palavras Reservadas da Linguagem C
Todas as linguagens de programação têm palavras reservadas. As palavras
reservadas não podem ser usadas a não ser para seus propósitos originais, i.e.,
não podem ser declaradas funções nem variáveis com os mesmos nomes.
Como a linguagem C é "case sensitive" (sensível a maiúsculas e minúsculas)
pode-se declarar uma variável chamada For, apesar de haver uma palavra
reservada for. Porém, isso não é recomendável, pois gera confusão no
código.
A seguir são listadas as palavras reservadas do ANSI C, ao todo 32 palavras.
Veremos o significado destas palavras-chave, à medida que os demais
conceitos forem apresentados.
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
87
5.6. Estrutura de um Programa em C
Um programa em C é constituído de:
um cabeçalho contendo as diretivas de compilador onde se
definem o valor de constantes simbólicas, declaração de variáveis e
funções, inclusão de bibliotecas, macros, etc.;
um bloco de instruções chamado de função principal (main) e
outros blocos de funções secundárias;
comentários do programa que constituem a documentação in situ.
Programa Exemplo: O paralel.c é um programa que calcula a resistência
equivalente de um par de resistores conectado em paralelo.
#include <stdio.h> /* Biblioteca padrão de I/O (ex. video e teclado) */
void main(void){ /* funcao principal */
float RA; /* Declaração da variável para a Resistência A */
float RB; /* Declaração da variável para a Resistência B */
float Req;
printf("Programa que calcula o equivalente de duas resistências");
printf(" conectadas em paralelo");
printf("\n"); /* pula uma linha do monitor de video */
printf("Entre com o valor de RA:"); /* imprime a string no vídeo */
scanf("%f",&RA); /* espera o valor do teclado */ printf("\n");
/* pula uma linha do monitor de video */
printf("Entre com o valor de RB:"); /* imprime a string no vídeo */
scanf("%f",&RB); /* espera o valor do teclado */
Req = (RA * RB)/(RA + RB); /* Calcula a resistencia equivalente */
printf("\n"); /* pula uma linha do monitor de video */
printf("A Resistencia Equivalente para RA//RB = ");printf("%f",Req);
} /* Fim da função principal e do programa */
Código 5-1
88
5.7. Conjunto de caracteres
Um programa fonte em C é um texto não formatado, escrito utilizando um
software editor de textos e usando um conjunto padrão de caracteres ASCII. A
seguir, estão os caracteres válidos utilizados em C:
Caracteres válidos:
a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
1 2 3 4 5 6 7 8 9 0
+ - * / \ = | & ! ? # % ( ) { } [ ] _ ‘ “ . , : < >
Exemplos de caracteres inválidos:
@ $ ¨ á é õ ç
Os caracteres acima são válidos apenas em strings. Maiores detalhes serão
tratados no capítulo 11.
89
5.8. Comentários
Em C, comentários do programa, podem ser escritos em qualquer lugar do
texto para facilitar a interpretação do algoritmo. Para que um comentário seja
identificado como tal, ele deve ter um conjunto de símbolos /* antes do
comentário, e outroconjunto */ depois do mesmo[28]. Observe que no
exemplo paralel.c.
Exemplo:
/* esta e uma linha de comentário em C */
Alguns compiladores aceitam colocar caracteres acentuados no meio dos
comentários, outros não.
Exemplo:
// este e’ um comentário valido para todos os compiladores C++
// e alguns compiladores C mais novos
O uso de comentários torna o código do programa mais legível de ser
entendido. Os comentários do C devem começar com o símbolo composto /*
e terminar com */. A linguagem C padrão não permite comentários
aninhados (um dentro do outro), até porque seria incoerente, mas existem
alguns compiladores que os aceitam sem gerar erros de sintaxe.
90
5.9. Diretivas de Compilação[29]
Em C, existem comandos que são processados antes da compilação do
programa. Esses comandos são genericamente chamados de diretivas de
compilação e servem para informar ao compilador, quais são as constantes
simbólicas usadas no programa e quais bibliotecas devem ser anexadas ao
programa executável entre outras funções.
A diretiva #include instrui ao compilador para incluir na compilação do
programa o conteúdo de outros arquivos. Normalmente, esses arquivos
contêm declarações de funções da biblioteca ou rotinas do usuário.
A diretiva #define diz ao compilador quais são as constantes simbólicas
usadas no programa código fonte.
91
5.10. Declaração de Variáveis[30]
Em C, como na maioria das linguagens, as variáveis devem ser declaradas
antes de serem utilizadas. Existem dois tipos de variáveis de acordo com o
escopo em que estas podem ser acessadas: as variáveis globais e as locais.
Variáveis globais devem ser declaradas no início do programa e fora de
qualquer função. As variáveis locais devem ser declaradas no inicio da
função onde elas serão válidas.
As variáveis podem ser de vários tipos: int (inteiras), float (real de simples
precisão) e outras que serão vistas no capítulo a seguir. No exemplo acima,
Req, RA e RB são declaradas como variáveis float (reais).
Os nomes das variáveis indicam o endereço de memória onde está um
determinado dado. Cabe ressaltar, que os nomes das variáveis não são
armazenados no código executável, e que elas fazem sentido só para o
instante da compilação e linker. Isso denota que não pode ser interpretado um
código fonte a partir de um arquivo executável.
92
5.11. Uma Introdução às Entrada e Saída de Dados[31]
Em C existem várias maneiras de fazer a leitura e escrita de informações.
Estas operações são chamadas de operações de entrada e saída, ou
simplesmente operações de I/O. No capítulo a seguir, serão vistas algumas
funções padronizadas. Um exemplo típico de dispositivo de saída é o monitor
de vídeo, e um de dispositivo de entrada, o teclado. Existem também funções
padronizadas de I/O em arquivos, portas seriais e paralelas. Um exemplo de
função padronizada é a função printf que é uma rotina de envio de dados
formatados, utilizada para enviar caracteres ASCII para a placa de vídeo, para
o canal serial, ou para qualquer função definida na sua chamada. A função
scanf é uma função padronizada de leitura formatada de caracteres ASCII de
um dispositivo de entrada de dados, tipicamente o teclado ou um canal serial.
5.11.1. Caracteres
Os caracteres são um tipo de dado: o char. As variáveis char são
armazenadas em memórias de um byte. Os inteiros (int) podem possuir um
número maior de bytes. Dependendo da implementação do compilador e do
microprocessador alvo, eles podem ter 1 byte (8 bits), 2 bytes (16 bits), 4
bytes (32 bits) ou mais.
De forma geral, também se pode usar uma variável do tipo char para
armazenar valores numéricos inteiros de 8 bits.
Para indicar um caractere de texto devem ser usadas apóstrofes. No exemplo a
seguir, pode-se visualizar o uso de variáveis do tipo char.
#include <stdio.h>
void main (void){
char car;
car = 'D'; /* car armazena o equivalente em ASCII da letra D */
printf ("%c",car);
}
Código 5-2
No programa anterior, %c indica à função printf() que deverá enviar um
caractere para o dispositivo de saída.
Como foi visto antes, uma variável char também pode ser usada para
armazenar um número inteiro de oito bits. Observar o seguinte programa.
#include <stdio.h>
void main (void){
char car;
car = 'D';
printf ("%d",car); /* Imprime o caractere como inteiro */
}
Código 5-3
93
Esse programa colocará o número 68 no dispositivo de saída (memória da
placa de vídeo ou canal serial, por exemplo), que é o código ASCII
correspondente ao caractere 'D'.
Algumas vezes, é necessário capturar um caractere único fornecido pelo
usuário do sistema ou por um dispositivo de entrada qualquer. Para efetuar
essa tarefa, existem duas funções na biblioteca, chamadas getch() e getche().
Ambas retornam o caractere pressionado, se for num teclado, ou um caractere
que chegou pelo canal serial, em várias aplicações de microcontroladores. A
função getche() imprime o caractere na tela antes de retorná-lo (num
compilador para PC) e getch() apenas retorna o caractere pressionado sem
imprimi-lo na tela. Ambas as funções são declaradas no arquivo de cabeçalho
conio.h. Geralmente, essas funções não estão disponíveis em ambiente Unix
(compiladores cc e gcc) e podem ser substituídas pela função scanf(), porém,
sem a mesma funcionalidade. A seguir, um exemplo que usa a função
getch(), e seu correspondente em ambiente Unix:
#include <stdio.h>
#include <conio.h>
void main (void){
char car;
car=getch();
printf ("Foi pressionada a tecla %c",car);
}
Equivalente para o ambiente Unix do programa acima, sem usar getch():
#include <stdio.h>
void main (void){
char Ch;
scanf("%c",&Ch);
printf ("Foi pressionada a tecla %c",Ch);
}
Código 5-4
Na maioria dos compiladores C para 8051, PIC, Motorola, etc., as funções
scanf e printf utilizam o canal serial (ou um par de pinos de I/O digital) como
dispositivo padrão para a entrada e saída de dados. Os compiladores para PC
normalmente utilizam o teclado e o monitor de vídeo como dispositivo
padrão.
A principal diferença da versão que utiliza getch(), para a versão que não a
utiliza, é que no primeiro caso, o usuário simplesmente seleciona a tecla e o
sistema a lê diretamente do buffer de teclado (compiladores para PCs[32]).
No segundo caso, é necessário pressionar também a tecla <ENTER>.
5.11.2. As Strings[33]
Para a linguagemC, uma string é definida como sendo um conjunto de
caracteres terminado com um caractere nulo (00h). O caractere nulo é um
caractere com valor inteiro igual a zero (código ASCII igual a 0). O
94
terminador nulo também pode ser representado usando a convenção de barra
invertida como sendo '\0'. Embora uma string seja um vetor de variáveis do
tipo char, e que o assunto vetores será discutido posteriormente, serão vistos
nesta seção os fundamentos necessários para que possam ser utilizadas essas
cadeias de caracteres. Para declarar uma string pode-se utilizar o seguinte
formato geral:
char identificador-da-string[tamanho];
A expressão acima declara um vetor de caracteres (uma string) com número
de posições igual a tamanho. Observar que devido ao terminador nulo,
deve-se declarar o comprimento da string como sendo, no mínimo, um
caractere maior que a maior string que se pretende armazenar. Supondo que
seja declarada uma string de 7 posições e colocando a palavra Diodo nela,
tem-se na memória:
‘D’ ‘i’ ‘o’ ‘d’ ‘o’ ‘\0’ ...
No caso acima, as células de memória posteriores não utilizadas conterão
valores indeterminados (usualmente chamado de lixo[34] de memória pelos
programadores). Isso acontece porque a linguagem C não inicializa
automaticamente as suas variáveis, cabendo ao programador essa tarefa, caso
seja necessária. Caso seja necessária a leitura de uma string fornecida por um
sistema ou pelo usuário através do teclado, pode ser usada a função gets().
Um exemplo do uso desta função é apresentado abaixo. A função gets()
coloca o terminador nulo na string, quando no final da mesma aparece o
equivalente em ASCII da tecla <ENTER>.
Uma vantagem da representação de uma string pela finalização de um
caractere nulo é que esta não precisa ter um tamanho pré-definido, e em
operações de leitura, a string pode ser lida caractere a caractere até o
terminador nulo.
#include <stdio.h>
void main (void) {
char string[100];
printf ("Digitar uma string: ");
gets(string);
printf ("\n Foi digitada %s",string);
}
Código 5-5
Neste programa o tamanho máximo da string que pode ser inserida é 99
caracteres. Caso a string inserida tiver um tamanho maior poderá levar a
resultados desastrosos.
Como as strings são basicamente vetores de caracteres, para se acessar um
determinado caractere, basta utilizar um índice relacionado ao caractere
desejado dentro da string. Supondo uma string chamada str pode-se acessar o
segundo caractere (‘t’) de str da seguinte forma:
95
str[1] = 'a';
Tanto na linguagem C como no hardware, a primeira posição de um vetor é a
posição zero. Desta forma, o primeiro caractere de uma string estará na
posição 0 do vetor; a segunda letra na posição 1, e assim sucessivamente.
Segue, um exemplo que imprimirá o segundo caractere da string "Diodo".
Em seguida, o programa troca o caractere e apresenta a string resultante.
#include <stdio.h>
void main(void) {
char str[10] = "Diodo";
printf("\n String: %s", str);
printf("\n Segundo caractere: %c", str[1]);
str[1] = 'U'; /*Troca o caractere ‘i’ por ‘U’ */
printf("\n Agora o segundo caractere é: %c", str[1]);
printf("\n A string resultante: %s", str);
}
Código 5-6
Nesta string, o terminador nulo está na posição número cinco. Das posições 0
a 5, sabe-se que existem caracteres válidos, e portanto podem ser impressos.
Deve-se notar a forma como a string str foi inicializada com os caracteres ‘D’
‘i’ ‘o’ ‘d’ ‘o’ e ‘\0’ simplesmente declarando char str[10] = "Diodo".
No programa acima, o símbolo %s indica à função printf() que deve colocar
uma string no dispositivo de saída. A continuação, será feita uma abordagem
inicial mais detalhada sobre as duas funções que já têm sido utilizadas para
implementar entradas e saídas de dados.
5.11.3. printf
A função printf() tem a seguinte forma geral:
printf (string-de-controle,lista-de-argumentos);
A string de controle serve como modelo do que vai ser enviado para o
dispositivo de saída, por exemplo, a placa de vídeo. A string de controle
contém os caracteres que devem ser colocados no dispositivo de saída e os
espaços reservados para os valores das variáveis e suas respectivas posições.
A colocação de conteúdos de variáveis é feita usando-se os códigos de
controle, que usam a uma notação especial começando com o caractere %.
Na string de controle podem ser indicadas quais variáveis terão o conteúdo
enviado, de que tipo e ainda em que posição serão enviadas. Para cada código
de controle deve-se ter um argumento na lista de argumentos. Na tabela
abaixo estão apresentados alguns códigos:
Código Significado
%d Inteiro
%f Float
%c Caractere
%s String
%x Hexadecimal
96
%% Coloca na tela um
%
Mais abaixo são mostrados alguns exemplos de uso da função printf() e o que
eles exibem:
printf ("\n Teste %% %%");
printf ("\n %f",40.345);
printf ("\n Um caractere %c e um inteiro %d",'D',120);
printf ("\n %s e um exemplo","Este");
printf ("\n %s%d%%","Tolerância de ",10);
Resultado:
Teste % %
40.345
Um caractere D e um inteiro 120
Este e um exemplo
Tolerância de 10%
Maiores detalhes sobre a função printf(), incluindo demais códigos de
controle, serão vistos posteriormente.
5.11.4. scanf
O formato geral da função scanf() é:
scanf (string-de-controle,lista-de-argumentos);
Usando a função scanf() pode-se capturar dados de um dispositivo de entrada
(canal serial ou teclado, por exemplo). O número de argumentos deve ser
igual ao de códigos de controle na string de controle. É importante lembrar o
uso do símbolo & antes das variáveis da lista de argumentos (endereço da
variável). Maiores detalhes sobre a função scanf() serão vistos
posteriormente.
Uma desvantagem do uso desta função é que o sistema para na função até que
seja inserido um caractere equivalente ao <ENTER>. Na maioria das
aplicações de engenharia esse tipo de comportamento não pode ser tolerado e
os dados devem ser capturados utilizando interrupções. O uso de interrupções
será tratado em outros capítulos.
97
5.12. Uma Introdução aos Comandos de Controle de
Fluxo[35]
A linguagem C permite uma ampla variedade de estruturas de controle de
fluxo de processamento. Essas estruturas serão vistas em detalhes nos
capítulos seguintes. Existem duas estruturas básicas (decisão e iteração) que
são similares às estruturas usadas nas pseudo-linguagens algorítmicas:
Estrutura de Tomada de Decisão: Permite direcionar o fluxo lógico para
dois blocos distintos de instruções de acordo com uma condição de controle.
Pseudo-linguagem Linguagem C
SE (condição)
então (bloco
1)
senão (bloco 2)
fim se
if(condição){
bloco 1;
} else{
bloco 2;
};
Estrutura de Iteração ou Repetição: Permite executar repetidamente um
bloco de instruções até que a condição de controle seja satisfeita.
Pseudo-linguagem Linguagem C
faça
bloco
enquanto (condição)
do{
bloco;
}while(condição);
A linguagem C é sensível a maiúsculas e minúsculas (case sensitive), assim se
for declarada uma variável chamada soma, a mesma será interpretada de
forma diferente de outras que forem declaradas com nomes parecidos, tais
como Soma, SOMA, SoMa ou sOmA. Da mesma forma, os comandos
(palavras-chave ou instruções), if e for, porexemplo, só poderão ser escritos
com letras minúsculas, caso contrário, não serão interpretados como
comandos, e sim como identificadores.
Os comandos de controle de fluxo permitem ao programador alterar a
sequência de execução de um programa. Nesta seção será feita uma breve
introdução a dois comandos de controle de fluxo mais utilizados. Outros
comandos serão estudados posteriormente.
5.12.1. if
O comando if representa uma tomada de decisão do tipo "SE (isto for
verdadeiro) ENTÃO (execute o seguinte...)". A forma geral do comando é:
if (condição) instrução;
A condição do comando if é uma expressão que será avaliada como sendo
verdadeira ou falsa. Em C, uma expressão é falsa se o seu valor é igual a
98
zero. Se o resultado for zero, a declaração não será executada. Se o resultado
for qualquer coisa diferente de zero a instrução será executada.
A instrução pode ser um bloco de código ou apenas um comando. É
interessante notar que, no caso da declaração ser um bloco de código, não é
necessário o uso do símbolo ‘;’ no final do bloco. Isso é uma regra geral para
blocos de código. Abaixo é mostrado um exemplo de utilização.
#include <stdio.h>
void main (void){
int res;
printf ("\nDigite o valor do resistor: ");
scanf("%d",&res);
if (res>100) printf ("\nO resistor é maior que 100 ohms");
if (res==100){
printf ("\n\nO resistor é de 100 ohms\n");
printf ("O numero e igual a 100.");
}
if (res<100) printf ("\n\nO resistor tem valor menor que 100 ohms");
}
Código 5-7
No programa acima, a expressão res>100 é previamente avaliada e retornará
um valor diferente de zero se verdadeira, ou zero se for falsa. Deve ser
observada a utilização do símbolo composto ‘==’ dentro da condição do
segundo if. Esse é um operador de comparação cujo resultado é verdadeiro,
se ambos operandos forem iguais, ou zero se forem diferentes. O símbolo
simples ‘=’ é um operador de atribuição, onde o operando esquerdo recebe
uma cópia de um valor relacionado com o operando colocado à direita. Caso
for colocada a seguinte condição:
if (res = 10) ... /* Isto não é uma comparação */
Nesse caso o compilador gerará um código que atribuirá a quantidade 10 à
variável res e a expressão res = 10 retornará 10 (diferente de zero, portanto
verdadeiro), fazendo com que a declaração fosse executada sempre. Esse
problema gera erros lógicos frequentes, mas geralmente fáceis de serem
detectados durante a depuração.
Deve-se evitar efetuar uma comparação de igualdade entre dois números em
ponto flutuante. Observar o seguinte exemplo.
#include<stdio.h>
void main(void){
float pi = 3.1415926536;
if( pi == 3.1415926506 ){
printf(“Os valores são diferentes, mas foram considerados iguais ”);
printf(“por causa da precisão”);
}
}
Código 5-8
Os operadores de comparação ou relacionais são:
99
== igual
!= diferente de
> maior que
< menor que
>= maior ou igual
<= menor ou igual
5.12.2. for
O comando for é utilizado para repetir a execução de um comando, ou bloco
de comandos, de forma iterativa. O uso deste comando é essencial para
executar tarefas de cálculo numérico.
Sua forma de utilização é:
for (inicialização ; condição ; incremento) instrução;
A instrução no comando for também pode ser um bloco ‘{}’ e neste caso o ;
pode ser omitido. O algoritmo equivalente de funcionamento pode ser
colocado como:
inicialização;
se (condição for verdadeira){
instrução;
incremento;
"Voltar para o comando se e comparar novamente a condição"
}
“continuar daqui para frente quando a condição for falsa”
...
Pode-se observar que o for executará a inicialização incondicionalmente e
testará a condição. Se a condição for falsa, ele não faz mais nada e o
programa continua. Se a condição for verdadeira, ele executará a declaração,
incrementará uma variável e voltará novamente a testar a condição. A
declaração será executada em laços (loops), até que a condição imposta dê um
valor falso. A seguir, é mostrado um programa que coloca os primeiros 100
números num dispositivo de saída:
#include <stdio.h>
void main (void){
int count;
for (count=0 ; count<100 ; count = count + 1) {
printf ("\n%d ",count);
}
}
Código 5-9
Outro exemplo interessante é mostrado a seguir: o programa lê uma string e
conta quantos dos caracteres desta string são iguais à letra 'c' . Essa é uma
tarefa comum quando se trabalha com drivers de comunicação com
protocolos em ASCII. Este exemplo utiliza a função gets()[36] que recebe
uma string como parâmetro, armazenando-a no endereço de memória
100
indicado pelo argumento.
#include <stdio.h>
void main (void){
char string[101]; /* string de 100 caracteres */
int i, cont;
printf("\n\n Digite uma frase de código: ");
gets(string); /* Le a string */
printf("\n\n Frase digitada:\n%s", string);
cont = 0;
for (i = 0 ; string[i] != '\0' ; i = i + 1) {
if (string[i] == 'c' ) { /* Se for a letra 'c' */
cont = cont + 1; /* Incrementa o contador de caracteres */
}
}
printf("\nNumero de caracteres iguais a ‘c’ = %d", cont);
}
Código 5-10
Deve ser notado que na condição imposta à instrução for, o caractere
armazenado em string[i] é comparado com '\0' (final da string). Caso o
caractere seja diferente de '\0', a condição é verdadeira e o bloco de instruções
da declaração será executado. Dentro do bloco existe um comando if que testa
se o caractere é igual a 'c'. Caso esta condição seja verdadeira, o contador de
caracteres c (cont) será incrementado.
101
5.13. Uma Breve Introdução às Funções
Uma função é um bloco de código de programa que pode ser usado diversas
vezes em sua execução. O uso de funções permite que o programa torne-se
estruturado. Um programa em C consiste basicamente de uma coleção de
funções, onde umas chamam as outras para obter um determinado resultado
ou para efetuar alguma tarefa específica.
Exemplo de função:
#include <stdio.h>
void ImprimeMensagem (void); /* Declaração da função criada ImprimeMensagem */
void main (void){
ImprimeMensagem(); /* chamada da função*/
}
/* Função que imprime uma mensagem */
void ImprimeMensagem (void){ /* Definição da função */
printf ("Filtro Passa-Baixas");
}
Código 5-11
Este programa terá o mesmo resultado que o primeiro exemplo da seção
anterior. O que ele faz é definir uma função ImprimeMensagem() que coloca
uma string na tela e não retorna nada (void). Em seguida, essa função é
chamada a partir de main() (que também é uma função).
Da mesma maneira que as variáveis, as funções devem ser declaradas antes de
serem utilizadas na sequência do código fonte. Isso é feito colocando o tipo
de retorno da função, o nome e os seus tipos de parâmetros entre parênteses e
separados por vírgulas (quando existirem), finalizando com o símbolo ‘;’.
Assim como nas variáveis, os nomes das funções armazenam o endereço de
memória onde existe a primeira instruçãode execução de um bloco de
instruções delimitado no código fonte pelos símbolos ‘{}’.
5.13.1. Os Argumentos[37]
Argumentos são as informações que uma função pode receber. É através dos
argumentos que são repassados os parâmetros para uma função, de forma
a poder executar algum tipo de tarefa. Nos exemplos anteriores, já foram
mostradas algumas funções com argumentos, tais como printf() e scanf(). A
seguir, é mostrado um exemplo de uma função que calcula e imprime o
quadrado de um valor enviado como parâmetro:
#include <stdio.h>
void ImprimeQuadrado (int); /* Declaração da função que calcula e imprime o quadrado de
um valor passado como parâmetro */
void main (){
int num;
102
printf ("Entre com um numero: ");
scanf ("%d",&num);
ImprimeQuadrado(num);
}
/* Função que calcula e imprime o quadrado de um valor passado como parâmetro */
void ImprimeQuadrado (int x){
int quadrado;
quadrado = x * x;
printf ("\n O quadrado de %d = %d",x,quadrado);
}
Código 5-12
Na definição de ImprimeQuadrado() é especificado que a função deverá
receber um argumento inteiro. Quando acontece a chamada à função, o inteiro
num é passado como argumento, através da cópia da variável numa área
específica da memória de dados, normalmente denominada de pilha ou
stack. Neste caso particular, o conteúdo da variável num é copiado para a
pilha, e após o salvamento do conteúdo dos registradores internos da CPU e o
salto da execução para a função chamada, será novamente copiada numa nova
posição da memória de dados identificada pelo nome da variável local x,
nesse caso.
Alguns pontos devem ser observados: em primeiro lugar deve-se satisfazer os
requisitos da função quanto ao tipo e à quantidade de argumentos de
chamada. Apesar de existirem algumas conversões de tipo, que o C faz
automaticamente, é importante ficar atento. Em segundo lugar, não é
importante o nome da variável que se passa como argumento, ou seja, a
variável num, ao ser passada como argumento para ImprimeQuadrado() é
copiada para a variável x. Dentro da função ImprimeQuadrado() trabalha-se
apenas com x, já que num é uma variável local da função main() e portanto,
não pode ser acessada pela função ImprimeQuadrado(). Se mudarmos o
valor da variável x dentro da função ImprimeQuadrado(), o valor de num
na função main() permanece inalterado.
A seguir, é mostrado o uso de uma função, que possui mais de um parâmetro.
Deve-se observar que os parâmetros do argumento devem ser separados por
vírgula, sendo que cada um deve ter um tipo explicitamente declarado. Deve-
se notar também, que os argumentos passados para a função não precisam
necessariamente ser variáveis porque mesmo sendo constantes, serão
copiados para a pilha, e daí, para a variável de entrada da função.
#include <stdio.h>
void multiplica (float a, float b, float c); /* Declaração da função que multiplica 3 números */
void main (void){
float x,y;
x=23.5;
y=12.9;
multiplica(x,y,3.87);
}
103
void multiplica(float a, float b,float c) { /* Multiplica 3 números */
printf ("%f",a*b*c);
}
Código 5-13
A função multiplica(x,y,3.87) recebe três parâmetros. Observar o seguinte
código:
#include <stdio.h>
void multiplica (float a, float b, float c); /* Declaração da função que multiplica 3 números */
void main (){
float a=2.0,b=3.5,c=4.3;
multiplica(a,b,c);
printf("main.a = %f main.b = %f main.c = %f",a,b,c);
}
void multiplica(float a, float b,float c) { /* Multiplica 3 números */
printf ("a*b*c = %f \n",a*b*c);
a = 0.0;
b = 0.0;
c = 0.0;
printf("mult.a = %f mult.b = %f mult.c = %f \n",a,b,c);
}
Código 5-14
O exemplo anterior mostra que as funções locais podem ter o mesmo nome
mas terão endereços de memória diferentes, i.e., a variável a da função
main() (main.a) tem um endereço de memória diferente da variável a da
função mult() (mult.a).
5.13.2. O Retorno de Valores
Por definição, uma função deve retornar um valor. As funções vistas
anteriormente não retornam nada, pois foi especificado um retorno void,
assim elas somente servem para executar uma tarefa.
Para retornar um valor (diferente de void), deve-se especificar o tipo antes do
nome da função. Para executar o retorno de um valor deve-se usar a função
pré-definida chamada return(). No exemplo a seguir, foi definida uma
função que calcula e retorna o quadrado de uma variável ou número passado
como parâmetro.
#include <stdio.h>
int CalculaQuadrado (int x); /* Declaração da função que calcula x^2 */
void main (){
int num;
int res;
printf ("Entre com um numero: ");
scanf ("%d",&num);
res = CalculaQuadrado(num);
printf ("\n O quadrado de %d = %d",num,res);
}
104
/* Função que calcula e retorna o quadrado de um valor passado como parâmetro */
int CalculaQuadrado (int x){ /* Calcula o quadrado de x */
int quadrado;
quadrado = x * x;
return (quadrado);
}
Código 5-15
Deve-se observar que a função CalculaQuadrado() retornará a cópia do
valor armazenado temporariamente na variável local quadrado para qualquer
função que a chame. No programa é feita a atribuição do resultado à variável
res, que posteriormente foi enviada a um dispositivo de saída através da
função printf(). Caso o retorno não seja especificado, o tipo de retorno
default[38] para uma função é o tipo inteiro. Porém, não é uma boa prática
não se especificar o valor de retorno.
Mais um exemplo de função, que agora recebe dois float e retorna um valor
float. Deve-se observar que neste exemplo foi especificado um valor de
retorno para a função main(int), retornando zero pelo programa.
Normalmente, é isso que se faz com a função main, que retornará zero
quando tiver sido executada sem qualquer tipo de erro.
#include <stdio.h>
float prod (float x,float y);
int main (void){
float saida;
saida = prod(45.2,0.0067);
printf ("A saída e: %f\n",saida);
return(0);
}
float prod (float x,float y){
return (x*y);
}
Código 5-16
A forma geral de uma função:
tipo-de-retorno identificador-da-função (lista-de-
argumentos){
código-da-função
}
105
5.14. Primeiros Passos
Veja o menor programa em C que pode gerar um executável.
main(){}
Código 5-17
É um código fonte que não executa absolutamente nada. Ao compilar este
programa provavelmente aparecerá uma mensagem de aviso[39]:
Compiling...
main.c
d:\FONTES\Nada\main.c(1) : warning C4035: 'main' : no return value
Linking...
MainProg.exe - 0 error(s), 1 warning(s)
Os warnings indicam possíveis erros lógicos na implementação, mas não é
necessariamente um erro de programa. Um programa profissional não deverá
conter nenhum warning. O warning deste exemplo, aparece do fato de que
na linguagem C qualquer função deve retornar algum valor. Se o tipo de
retorno não for definido explicitamente, o default é o retorno de um valor
inteiro. Todo programa em C possui pelo menos uma função, chamada de
main (principal), e qualquer programa começa e termina nessa função.
A estrutura de um programa em C é formada por funções, onde a primeira
função a ser chamada é afunção main, e a partir desta são chamadas as outras
componentes do código, que na sua vez podem chamar outras, e assim
sucessivamente, executando tarefas específicas e retornando valores para a
função que a ativou. O programa finaliza na função main. Na Figura 5-1 é
ilustrada a estruturação de um programa C em relação às funções
componentes.
Figura 5-1 – Estruturação de um programa em C
Voltando ao exemplo anterior, para remover o warning pode-se modificar o
código fonte para:
main(){
106
return (0);
}
Código 5-18
Obtendo do compilador
Compiling...
main.c
Linking...
MainProg.exe - 0 error(s), 0 warning(s)
Aqui foi forçado o retorno de um valor através de outra função padronizada
chamada return. Usualmente, o valor de retorno da função main pode ser
usado para indicar o status da saída, retornando códigos para erros ou eventos
que aconteceram durante a execução, por exemplo. Outra solução é a de
indicar de forma explícita que a função não retorna nada. O nada em C é
chamado de void (vazio).
void main(){}
Código 5-19
Compilando obtém-se:
Compiling...
main.c
Linking...
MainProg.exe - 0 error(s), 0 warning(s)
Seria, por exemplo, uma incoerência definir a função maincomo
retornando void e ainda utilizar a função return;
void main(){
return(0);
}
Código 5-20
Alguns compiladores mostram incoerências como estas na forma de avisos
(warnings).
Compiling...
main.c
d:\FONTES\Nada\main.c(2) : warning C4098: 'main' : 'void' function returning a value
Linking...
MainProg.exe - 0 error(s), 1 warning(s)
É interessante notar que o símbolo “;” indica fim de instrução ou de conjunto
de instruções e deve ser obrigatoriamente colocado no final de cada instrução,
exceto, quando são utilizados delimitadores de bloco “{}”. É importante
também, notar que os compiladores C ignoram os espaços ou linhas deixadas
em branco, ou tabulações entre operandos.
Os símbolos “{}” delimitam um bloco de instruções, e os símbolos “( )”
delimitam operações lógicas e aritméticas, definindo prioridades, e nas
funções delimitam os parâmetros a serem enviados para as mesmas.
Considere o programa que calcula a variação da resistência elétrica de um
107
componente, com a variação da temperatura, segundo a seguinte regra:
R = Ro [1 + (T - To)]
onde Ro = 1K ; To = 25 Celsius; T = 75 Celsius; = 0.0035 .Celsius-1
void main(void){
float Ro = 1000.0;
float R;
float alfa = 0.0035;
float To = 25.0;
float T = 75.0;
R = Ro * (1 + alfa * (T - To) );
}
Código 5-21
Esse programa efetua um cálculo aritmético com valores em ponto flutuante.
Os dados de entrada para o cálculo estão armazenados na memória, e o
resultado de saída também será armazenado na memória. Esses dados são
válidos enquanto o programa estiver em execução (alguns microssegundos
neste programa). Pode-se perceber a necessidade de ter outro meio de
armazenamento, que comporte os dados quando o programa não estiver mais
em execução. Uma forma de armazenar os dados seria apresentando-os na
tela do monitor de vídeo de forma que possam ser visualizados e
memorizados; outra alternativa é a do armazenamento em meio magnético,
por exemplo.
Observar abaixo o código em assembly gerado a partir do código C pelo
Microsoft Visual C++.
1: void main(void){
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,14h
2: float Ro = 1000.0;
00401016 mov dword ptr [Ro],447A0000h
3: float R;
4: float alfa = 0.0035;
0040101D mov dword ptr [alfa],3B656042h
5: float To = 25.0;
00401024 mov dword ptr [To],41C80000h
6: float T = 75.0;
0040102B mov dword ptr [T],42960000h
7:
8: R = Ro * (1 + alfa * (T - To) );
00401032 fld dword ptr [T]
00401035 fsub dword ptr [To]
00401038 fmul dword ptr [alfa]
0040103B fadd dword ptr
00401041 fmul dword ptr [Ro]
00401044 fstp dword ptr [R]
9: }
00401047 mov esp,ebp
00401049 pop ebp
108
0040104A ret
Código 5-22
Observe a linha de assembly abaixo da instrução C identificada com o
símbolo 2:,
00401016 mov dword ptr [Ro],447A0000h
O primeiro número que aparece é o endereço de memória de programa que foi
alocado para a instrução. O símbolo mov é uma instrução em assembly para
a família 8x86 que copia dados ou constantes de um lugar da memória para
outra. A instrução assembly precisa dos parâmetros de origem e destino.
Nessa linha, a origem é o número 447A0000h que é a representação do valor
1000 em ponto flutuante, e o destino é a posição apontada por um apontador
chamado ptr com offset de Ro unidades de memória, como sendo o índice de
um vetor. Na realidade, Ro em si não armazena um valor, mas o offset de
endereços de memória onde está armazenado o valor.
Nesse exemplo pode-se observar também a ordem com que são executadas as
operações aritméticas de uma expressão complexa. Como primeiro passo, é
efetuada a subtração entre T e To (fsub), a seguir a multiplicação do resultado
parcial vezes alfa (fmul), depois a soma (fadd) com 1 e finalmente a
multiplicação vezes Ro (fmul); somente, então, o resultado é copiado para R
(fstp). Os operadores que são executados inicialmente, são os que estiverem
entre parênteses, começando pelos mais internos dentro da expressão
complexa, continuando com a multiplicação e a divisão, soma e a subtração,
nesta ordem.
É importante ressaltar, que nem todos os processadores possuem operações de
ponto flutuante integrado no seu hardware, por exemplo, o 8051 e
microcontroladores em geral, sendo que as rotinas de cálculo devem ser
implementadas por software. Em geral, para pequenos sistemas de hardware
dedicados, deve ser evitada a utilização de número em ponto flutuante, devido
ao grande tamanho das bibliotecas necessárias. Por exemplo, uma biblioteca
com operações de ponto flutuante, para um microcontrolador 8051 com 8
kbytes de memória de programa, pode ocupar até 1KB (mais de 10% do
recurso disponível), sem contar com o código.
Nos casos em que é necessária a utilização de números decimais por causa da
exatidão, costuma-se utilizar o ponto fixo flutuante, ou seja, trabalha-se com
todos os números multiplicados por 100 para obter 2 casas decimais de
precisão, 1000 para 3 casas, e assim sucessivamente. O tempo de
processamento das funções de ponto flutuante deve ser levado em conta,
especialmente nas aplicações onde a temporização é crítica na faixa de
dezenas de microssegundos.
Uma forma de colocar o resultado num dispositivo de saída é a função padrão
109
ANSI chamada printf. Essa função é útil para enviar caracteres ASCII para a
memória de vídeo nos PCs, usando o canal serial e usando compiladores para
microcontroladores. Essa função é normalmente distribuída junto com o
pacote do compilador, numa biblioteca de funções sendo declarada junto com
outras, no arquivo stdio.h.
Considerar o seguinte programa C:
#include <stdio.h>
void main () { /* Um primeiro programa com saída de dados */
printf ("Teste do dispositivo de saída \n");
}
Código 5-23
Compilando e executando esse programa, você verá que ele coloca a string
Teste do dispositivo de saída no monitor de vídeo, se estiver compilando um
programa para PC. O mesmo código compilado para um microcontrolador
8051 ou PIC, poderia enviar a mesma string (cadeia de caracteres) pelo canal
serial.Esse programa pode ser analisado por partes, como descrito a seguir.
A linha #include <stdio.h> indica ao pré-processador que deverá ser incluído
o arquivo de cabeçalho (header file) stdio.h. Nesse arquivo existem
declarações de funções úteis para entrada e saída de dados padronizados (std
= standard, padrão em inglês; io = Input/Output, entrada e saída). Toda vez
que você quiser usar uma destas funções no código, deve-se incluir esse
comando. Essa linha é uma diretiva de pré-compilador. A linguagem C
possui diversos arquivos de cabeçalho que definem as diversas funções
disponíveis na biblioteca.
Durante a codificação de um programa devem ser colocados os comentários
que ajudem a entender a lógica do programador. No último exemplo foi
colocado um comentário: /* Um Primeiro programa com saída de dados
*/. O compilador C desconsidera qualquer string que comece com os
símbolos /* e termine com */. Um comentário poderá ter mais de uma linha.
A linha void main() define uma função de nome main. Todos os programas
em C têm que ter uma função main, pois essa é a função que será chamada
quando o programa for executado. O conteúdo de uma função (conjunto de
instruções, variáveis, etc.) é delimitado por chaves { }. O código que estiver
dentro das chaves será executado sequencialmente quando a função for
chamada. A palavra void indica que a função não retorna nenhum valor, isto
é, seu retorno é vazio, como foi visto anteriormente.
A única coisa que esse programa realmente executa é chamar a função
printf(), passando a string "Teste do dispositivo de saída \n" como
argumento. É por causa do uso da função printf() pelo programa, que se deve
incluir o arquivo-cabeçalho stdio.h . A função printf(), nesse caso, irá apenas
copiar a string na memória de vídeo do sistema e na próxima varredura, a
110
mesma aparecerá no monitor de vídeo. O conjunto de símbolos \n é um
caractere especial de controle comumente chamado de LF ou Nova Linha.
Alguns compiladores geram o código para esse símbolo como uma
combinação dos caracteres de controle LF e CR (carriage return ou retorno
de carro), outros não. Assim, por exemplo, nos compiladores para PC ele será
interpretado como um comando de quebra de linha, isto é, após imprimir
Teste do dispositivo de saída o cursor passará para a próxima linha.
A função printf pode ser também utilizada para o envio de valores de
variáveis. Assim, no programa abaixo pode ser visualizado o resultado de um
cálculo.
#include <stdio.h>
void main(void){
float Ro = 1000.0;
float R;
float alfa = 0.0035;
float To = 25.0;
float T = 75.0;
R = Ro * (1 + alfa * (T - To) );
printf(“R = %f”,R);
}
Código 5-24
Dentro da string a ser enviada para o dispositivo de saída, pode ser reservado
um espaço para o envio de um valor armazenado na memória de forma
binária, e que deverá ser convertido pela própria função para o seu
equivalente em ASCII. Para a função executar isso corretamente, deverá ser
informada do tipo de dado e o valor do mesmo. O caractere ‘ %’ indica que o
que vem a continuação é um valor armazenado em memória. Após esse
caractere, a função espera que seja definido qual o tipo para efetuar a
conversão, f para float, d para inteiro, por exemplo. Após a finalização da
string deverá ser repassado como parâmetro a variável que contém o valor a
ser previamente convertido para ASCII e posteriormente enviado ao
dispositivo de saída. Maiores detalhes sobre a função printf serão vistos mais
adiante.
Tendo um mecanismo básico de saída, será necessário um mecanismo básico
de entrada de dados. Uma função básica é scanf. Nos compiladores para PC,
essa função executa a leitura do buffer dedicado ao teclado, esperando que o
usuário digite algumas teclas mais um ENTER. Os parâmetros necessários
para essa função capturar os dados do buffer de forma adequada são o tipo de
dado e o lugar da memória onde será armazenado.
Observe o programa anterior modificado, onde é permitido carregar os
valores das variáveis durante a execução.
#include <stdio.h>
void main(void){
111
float Ro;
float R;
float alfa;
float To;
float T;
printf(“\n Entre com Ro: ”);
scanf(“%f”,&Ro);
printf(“\n Entre com a temperatura atual T: ”);
scanf(“%f”,&T);
printf(“\n Entre com To: ”);
scanf(“%f”,&To);
printf(“\n Entre com o coeficiente térmico alfa: ”);
scanf(“%f”,&alfa);
R = Ro * (1 + alfa * (T - To) ); /* calcula o valor da resistência */
printf(“O valor da resistência R = %f a uma temperatura de %f C”,R,T);
}
Código 5-25
O primeiro parâmetro da função scanf, "%f", refere-se à informação
necessária para a interpretação sobre o tipo de dado. O segundo parâmetro
passado à função indica que o dado lido deverá ser armazenado na variável
Ro. Como cada função poderá possuir as suas próprias variáveis, a função
scanf poderá não visualizar a variável Ro, no caso de uso de variáveis locais.
A solução para isso é enviar como parâmetro o endereço da própria variável
ou do local onde existe uma cópia da mesma. Isso é feito através do operador
&, que quando colocado antes do nome de uma variável, transforma-a no seu
endereço de memória.
112
5.15. Exercícios
1. Escreva um programa que leia um caractere digitado pelo usuário, imprima o
caractere digitado e o código ASCII correspondente a esse caractere em representação
decimal e hexadecimal.
2. Escreva um programa que leia duas strings e as coloque na tela, e que imprima
o segundo caractere de cada string.
3. Modifique o programa que calcula as raízes de uma equação de segundo grau,
usando a fórmula de Báskara, para que seja capaz de calcular e mostrar raízes complexas.
4. Escreva uma função que dado um horário em horas, minutos e segundos,
retorne o valor equivalente em segundos a partir da meia noite. Ex. 20:10:15. O
equivalente em segundos será: 72615 segundos.
5. Baseado no exemplo do cálculo da resistência com a variação da temperatura,
implementar um programa que calcule as raízes de um polinômio de grau dois pela
fórmula de Báskara. Dado um polinômio de segundo grau, sendo a variável independente s
a, b e c são constantes: tendo as duas soluções dadas pela fórmula:
. Sugestão: Utilizar a função sqrt (raiz quadrada) ou pow
(potência), da biblioteca de funções definida no arquivo math.h. Caso o seu compilador
não possua funções equivalentes na sua biblioteca, as mesmas deverão ser implementadas
pela utilização de algoritmos numéricos. Como os microprocessadores somente executam
funções aritméticas simples com números inteiros, tais como soma, subtração,
multiplicação e divisão de números, as funções mais complexas deverão ser
implementadas utilizando funções simples, usando séries de Taylor ou tabelas
normalizadas de dados[40].
6. Escreva um programa que coloque os números de 0 a 100 num dispositivo de
saída na ordem inversa (começando em 100 e terminando em 0).
7. Escreva um programa que leia uma string, conte quantos caracteres possui e
quantos são iguais ao caractere 'a'. Ainda, substitua os caracteres que forem iguais a 'a' por
'A'. O programa deverá imprimir o número de caracteres totais da string, o número de
caracteres modificados e a string modificada.
113
6. ELEMENTOS DA LINGUAGEM
Este capítulo descreve os elementosda linguagem de programação C,
incluindo os nomes, números, e caracteres utilizados para implementar um
programa em C.
A sintaxe ANSI C denomina os elementos componentes de "tokens". Este
capítulo explica como são definidos os tokens e como o compilador os avalia.
Os seguintes tópicos serão discutidos nesta seção:
Tokens
Comentários
Keywords[41]
Identificadores
Constantes
Strings literais
Pontuação e caracteres especiais
Este capítulo inclui tabelas de referência para trigraphs, constantes de ponto
flutuante, constantes inteiras e sequências de escape.
Os operadores são símbolos (caracteres simples ou combinações destes) que
especificam a forma em que os dados serão manipulados. Cada símbolo é
interpretado como uma única unidade, denominado token.
114
6.1. Tokens da Linguagem C
Num programa fonte em C, o elemento básico reconhecido pelo compilador é
o “token”. Um token é um pedaço de texto que o compilador não consegue
dividir em outros elementos menores. Os tokens podem ser divididos em :
Keywords
Identificadores
Constantes
Strings literais
Pontuações
As palavras-chave, identificadores, constantes, strings e operadores descritos
neste capítulo são exemplos de tokens. Caracteres de pontuação, tais como,
colchetes ([ ]), parênteses (( )), chaves ({ }), e vírgulas (,) são também tokens.
6.1.1. Caracteres de Espaço em Branco
Os caracteres espaço, tabulação, retorno de carro, nova página e nova linha
são chamados de “caracteres de espaço em branco”, por servir ao mesmo
propósito que os espaços entre palavras e linhas em uma página impressa,
facilitando a leitura. Os tokens são delimitados por espaços em branco e por
outros tokens, tais como, operadores e pontuação. Durante a compilação, o
compilador C ignora os caracteres de espaços em branco, a não ser que sejam
utilizados como separadores ou como componentes de caracteres constantes
ou strings literais. O uso de espaços em branco torna o programa mais
legível. Deve ser notado que o compilador trata os comentários como espaços
em branco.
6.1.2. Comentários em C
Um comentário é uma sequência de caracteres que começa com a combinação
dos caracteres barra e asterisco (/*), que são tratados pelo compilador como
um único caractere, espaço em branco, e portanto ignorado. Um comentário
pode incluir qualquer combinação de caracteres do grupo representável,
incluindo caracteres de nova linha, mas excluindo o delimitador de “final de
comentário” (*/). Os comentários podem ocupar mais que uma linha, mas
não podem estar aninhados (comentário dentro de comentário).
Os comentários podem aparecer em qualquer lugar onde um caractere espaço
em branco for permitido. Desde que o compilador trata um comentário como
um único caractere espaço em branco, não podem ser incluídos comentários
dentro de tokens. O compilador ignora os caracteres nos comentários.
Os comentários devem ser utilizados para documentar o código. A seguir, um
exemplo de comentário aceito pelo compilador.
/* Os comentários podem conter keywords tais como
for e while sem gerar erros. */
115
Os comentários podem aparecer na mesma linha de uma instrução de código:
printf( "Inicializando\n" ); /* Comentários podem ser colocados aqui */
Os comentários podem ser usados precedendo funções ou módulos de
programa contendo um bloco de descrição:
/* MATHERR.C Mostra os códigos de erro para
* funções matemáticas.
*/
Uma vez que os comentários não podem ser aninhados, tem-se um exemplo
que gera um erro de compilação:
/* Comentário da rotina de teste
/* Abre arquivo */
fh = _open( "myfile.c", _O_RDONLY );
.
.
.
*/
O erro ocorre porque o compilador reconhecerá o primeiro */, depois das
palavras Abre arquivo, com o final do comentário. Então tenta processar o
texto restante, produzindo um erro quando encontra o conjunto */ fora do
comentário.
Podem ser utilizados os comentários para deixar certas linhas de código
inativas para propósitos de teste. Uma alternativa muito utilizada para essa
tarefa é a utilização das diretivas de pré-processador #if e #endif como
métodos mais adequados. Maiores informações podem ser vistas no capítulo
14.
Alguns compiladores suportam o comentário de linha única, que é precedido
de duas barras (//). Os comentários não podem se estender até a segunda
linha.
// Este é um comentário válido em alguns compiladores C
Os comentários que começam com duas barras (//) são terminados pelo
seguinte caractere de nova linha. No exemplo abaixo, o caractere nova linha
é precedido de uma barra invertida (\), criando uma “sequência de escape”.
Essa sequência de escape faz com que o compilador trate a seguinte linha
como sendo parte da primeira.
// my comment \
i++;
Assim, a instrução i++ também está comentada.
6.1.3. Avaliação dos Tokens
Quando o compilador interpreta os tokens, o compilador tenta incluir tantos
caracteres quantos forem possíveis num único token, antes de continuar para
o próximo. Devido a essa característica, o compilador pode não interpretar os
tokens corretamente se não estiverem separados apropriadamente por espaços
116
em branco. Considerar a seguinte expressão:
i+++j;
Nesse exemplo, o compilador primeiro tenta localizar o maior operador
possível (++)[42] a partir dos três sinais mais, então processa o símbolo
restante como um operador de soma (+). Desta maneira a expressão é
interpretada como
(i++) + (j) – caso 1
e não
(i) + (++j) – caso 2
No primeiro caso, será efetuado o incremento em uma unidade na variável i e
o resultado será somado ao valor da variável j. Já no segundo caso, o valor
contido em i é somado ao valor contido em j. Após a soma, o valor de j será
incrementado em uma unidade. A seguir são mostrados os códigos referentes
aos dois casos.
void main(void){
int i=5,j=1,z=0;
z = i+++j;
}
Código 6-1
O código acima resulta em i=6, j=1 e z=6. Esse código é equivalente ao
primeiro caso.
void main(void){
int i=5,j=1,z=0;
z = i+(++j);
}
Código 6-2
Já o código anterior resulta em i=5, j=2 e z=7. Esse código é equivalente ao
segundo caso.
Nesses casos e em similares, é recomendado utilizar espaços em branco e
parênteses para evitar ambiguidades e assegurar uma avaliação adequada para
a expressão.
117
6.2. Keywords
As "keywords" ou palavras-chaves são palavras que possuem significados
especiais para o compilador C. A linguagem C utiliza as seguintes keywords:
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
As keywords não podem ser redefinidas. Ainda pode ser especificado um
determinado texto para substituir as keywords antes da compilação, utilizando
diretivas de pré-processador.
118
6.3. Identificadores
Os “identificadores” ou “símbolos” são os nomes definidos para variáveis,
tipos, funções e labels (ou rótulos) do programa. Os nomes dos
identificadores devem ser diferentes entre si e das keywords. As keywords
não podem ser utilizadas como identificadores, já que elas são reservadas para
usos especiais. O programador pode criar um identificador especificando-o
na declaração da variável, tipo ou função. No exemplo a seguir, result é um
identificador de uma variável inteira, main e printf são identificadores de
funções.
void main(){
int result;
if ( result != 0 )
printf( "Arquivo corrupto\n" );
}
Código 6-3
Umavez declarado, o programador pode utilizar o identificador nas seguintes
instruções do programa para se referir ao valor associado.
Um tipo especial de identificador , chamado de instrução label, pode ser
utilizado com a instrução goto. Recomenda-se a não utilização dessa
instrução por motivos estruturais.
O primeiro caractere de um nome de identificador deve ser um não-dígito (i.e.
o primeiro caractere deve ser um símbolo “_” – underscore [43]– ou uma
letra). O padrão ANSI permite seis caracteres significativos para nomes
identificadores externos, e 31 para nomes identificadores internos (dentro de
uma função). Os identificadores externos (declarados com escopo global ou
declarados com a classe de armazenamento extern) podem estar sujeitos a
restrições adicionais porque esses identificadores devem ser processados por
outros programas, tais como, os linkers.
O compilador C considera as letras maiúsculas e minúsculas como sendo
caracteres diferentes. Essa característica é chamada de case sensitive, e
possibilita a criar diferentes identificadores que tenham a mesma fonética,
mas diferentes nos seus conteúdos. Por exemplo, cada um dos seguintes
identificadores é único:
add
ADD
Add
aDD
A seguir, exemplos de identificadores válidos.
j
119
count
temp1
top_of_page
skip12
LastNum
Um dado identificador tem um “escopo”, que é a região do programa na qual
é conhecida ou relacionada. Maiores informações serão vistas mais adiante.
6.3.1. Caracteres Multibyte e Caracteres Longos
Um caractere multibyte é um caractere composto de uma sequência de um ou
mais bytes. Cada sequência de byte representa um caractere único no grupo
de caracteres estendido. Os caracteres multibyte são utilizados em grupos de
caracteres como o Kanji.
Os caracteres longos são códigos de caracteres multilíngues que possuem 16
bits de largura.
6.3.2. Trigraphs
O grupo de caracteres de um programa em C está descrito dentro do grupo de
caracteres ASCII representados por 7 bits, mas é um supergrupo do padrão
ISO 646-1983 Invariant Code Set. As sequências trigraph[44] permitem que
os programas em C possam ser escritos utilizando somente a norma ISO[45]
Invariant Code Set. Os trigraphs são sequências de três caracteres (dois
caracteres de interrogação) que o compilador substitui pelo seu caractere
correspondente de pontuação. Assim, podem ser usados os caracteres
trigraphs em arquivos fonte, quando o grupo de caracteres não contêm as
representações gráficas convenientes para alguns caracteres de pontuação.
A tabela a seguir mostra as nove sequências de trigraphs. Todas as
ocorrências no arquivo fonte da primeira coluna, serão substituídas pelo
caractere correspondente na segunda coluna. É importante ressaltar, que nem
todos os compiladores aceitam os trigraphs.
Trigraph Caracteres de pontuação
??= #
??( [
??/ \
??) ]
??' ^
??< {
??! |
??> }
??- ~
Tabela 6-1 – Exemplos de trigraphs
120
121
6.4. Constantes
Uma “constante” é um número, caractere, ou string que pode ser usada como
um valor em um programa. As constantes são utilizadas para representar
valores em ponto flutuante, inteiros, enumerações ou caracteres que não
podem ser modificados. As constantes são caracterizadas por ter um valor e
um tipo.
6.4.1. Constantes de Ponto Flutuante
Uma constante de ponto flutuante é um número decimal que represente um
número real com sinal. A representação de um número real com sinal inclui
uma porção inteira, uma porção fracionária e um expoente. Deve-se utilizar
constantes de ponto flutuante para representar valores de ponto flutuante que
não devem ser modificados.
Podem ser omitidos os dígitos antes do ponto decimal (a parte inteira do
valor) ou os dígitos depois do ponto decimal (parte fracionária), mas não
ambos. O ponto decimal pode ser ignorado somente se for colocado o
expoente. Não é permitido o uso de caractere espaços em branco separando
os dígitos ou caracteres da constante.
O seguinte exemplo mostra algumas formas de constantes do tipo ponto
flutuante e expressões:
15.75
1.575E1 /* = 15.75 */
1575e-2 /* = 15.75 */
-2.5e-3 /* = -0.0025 */
25E-4 /* = 0.0025 */
As constantes de ponto flutuante são positivas, a menos que, sejam precedidas
pelo símbolo menos (-). Nesse caso o símbolo menos é tratado como um
operador negação aritmético unário. As constantes de ponto flutuante podem
ser do tipo float, double ou long double.
Em geral, uma constante de ponto flutuante que não tenha o sufixo f, F, l ou L
será do tipo double. Se for utilizada a letra F ou f como sufixo, a constante
será do tipo float.
Por exemplo:
100L /* tem o tipo long double */
100F /* é do tipo float */
100D /* é do tipo double */
Como foi visto, pode-se omitir uma parte da constante de ponto flutuante,
como mostra o seguinte exemplo. O número 75 pode ser expresso de várias
formas, incluindo as seguintes:
122
.0075e2
0.075e1
.075e1
75e-2
6.4.2. Constantes Inteiras
Uma "constante inteira" é um número decimal (base 10), octal (base 8), ou
hexadecimal (base 16) que representa um valor inteiro. As constantes inteiras
podem ser usadas para representar valores de inteiro que não podem ser
mudados.
As constantes inteiras são positivas, a menos que, sejam precedidas por um
sinal de menos (-). O sinal de menos é interpretado como um operador
negação aritmético unário. Os operadores aritméticos unários serão discutidos
mais para frente.
Se uma constante inteira começar com os caracteres 0x ou 0X, o sistema
assumirá que está representada no sistema hexadecimal. Se começar com o
dígito 0, será considerada como uma representação no sistema octal. Caso
contrário, é assumido que o valor está sendo representado no sistema decimal.
As linhas seguintes são equivalentes:
0x1C /* = representação hexadecimal para o valor 28 decimal */
034 /* = representação octal para o valor 28 decimal */
Nenhum caráter de espaço em branco pode separar os dígitos de uma
constante de inteira. Os exemplos a continuação mostram constantes
decimais, octais e hexadecimais válidas.
Constantes Decimais
10
132
32179
Constantes Octais
012
0204
076663
Constantes Hexadecimais
0xa ou 0xA
0x84
0x7dB3 ou 0X7DB3
Tipos de Inteiros
Toda constante inteira é determinada por um tipo, baseado no seu valor e na
forma em que for expressa. Pode-se forçar qualquer constante inteira a ser do
123
tipo long adicionando a letra l ou L no final da constante; também se pode
forçar um tipo unsigned, adicionando a letra u, ou U, ao valor. O caractere
minúsculo l pode ser confundido com o dígito 1 e por esse motivo deve ser
evitado. Algumas formas de constantes de inteiro longas são mostradas a
seguir.
Constantes Decimais Unsigned
99U
Constantes Decimais Long
10L
79L
Constantes Octais Long
012L
0115L
Constantes Hexadecimais Long
0xaL ou 0xAL
0X4fL ou 0x4FL
Constantes Decimais Unsigned Long
776745UL
778866LU
O tipo que for designado a uma constante depende do valor que esta
representa. O valor de uma constante deve estar na faixa de valores
representáveis para seu tipo. O tipo de uma constante determina quais
conversões serão executadas quando ela for usada em uma expressão, ou
quando osinal de menos (-) for aplicado. A lista a seguir resume as regras de
conversão para constantes inteiras.
O tipo para uma constante decimal sem um sufixo é um int, long int, ou
unsigned long int. O primeiro desses três tipos, nos quais o valor da constante
pode ser representado, é o tipo nomeado para a constante.
O tipo designado para as constantes octal e hexadecimal sem sufixos são int,
unsigned int, long int, ou unsigned long int dependendo do tamanho da
constante.
O tipo designado para constantes com o sufixo u ou U é unsigned int ou
unsigned long int dependendo do seu tamanho.
O tipo designado para as constantes com sufixo l ou L pode ser long int ou
unsigned long int dependendo do seu tamanho.
O tipo designado com sufixos u ou U e l ou L é int longo não assinado é
unsigned long int.
6.4.3. Constantes Caractere
124
Uma "constante caractere" é formada envolvendo o caractere em aspas
simples (' ').
Tipos de Caracteres
Uma constante caractere inteira não precedida pela letra L é do tipo int. O
valor de uma constante de caráter de inteiro que contém um único caractere, é
o valor numérico do caractere interpretado como um inteiro. Por exemplo, o
valor numérico do caractere 'a' é 97 em decimal e 61 em hexadecimal.
Sintaticamente, uma "constante caractere longa" é uma constante de caractere
prefixada pela letra L.
char schar = 'x'; /* Uma constante de caractere */
Conjunto de Caracteres de Execução
O conjunto de caracteres de execução não é necessariamente o mesmo que é
usado para escrever programas de C. O conjunto de caracteres de execução
inclui todos os caracteres, tais como, o caractere nulo, caractere de newline,
retrocesso, tabulação horizontal, tabulação vertical, retorno de carro, e
sequências de escape.
Sequências de Escape
As combinações de caracteres que consistem em uma barra invertida (\)
seguidos de uma letra ou uma combinação de dígitos são chamadas
"sequências de escape". Para representar um caractere de newline, aspas
simples, ou certos caracteres especiais constantes, devem ser utilizadas as
sequências de escape. Uma sequência de escape é considerada como um
único caractere e é válido como um caractere constante.
As sequências de escape são tipicamente usadas para especificar ações, tais
como, retornos de carro e tabulações em terminais e impressoras. Essas
sequências também permitem efetuar representações literais de caracteres não
imprimíveis, e de caracteres que normalmente têm significados especiais, tais
como as aspas duplas (“). A tabela a seguir mostra as sequências de escape
ANSI e o que elas representam.
Sequência de
Escape
Representa
\a Bell (alert) – Campainha
\b Backspace – Retorno
\f Formfeed – Alimentação de página
\n New line – Nova linha
\r Carriage return – Retorno de carro
\t Horizontal tab – Tabulação horizontal
\v Vertical tab – Tabulação vertical
\' Single quotation mark – Aspas simples
\" Double quotation mark – Aspas duplas
\\ Backslash – Barra invertida
\? Sinal de interrogação
125
\ooo Caractere ASCII em notação octal
\xhhh Caractere ASCII em notação
hexadecimal
Tabela 6-2 – Sequências de escape
Especificações de Caracteres em Octal e Hexadecimal
A sequência \ooo permite especificar qualquer caractere do grupo de
caracteres ASCII como um código de caractere de três dígitos em notação
octal. O valor numérico do inteiro em notação octal especifica o valor do
caractere desejado.
De forma análoga, a sequência \xhhh permite especificar qualquer caractere
ASCII como um código de caractere em notação hexadecimal. Por exemplo,
para caractere de retrocesso (backspace) pode ser utilizada a sequência de
escape (\b), ou codificar como \010 (octal) ou \x008 (hexadecimal).
Numa sequência de escape em octal só podem ser usados os dígitos 0 a 7.
Uma sequência de escape octal não pode ter mais três dígitos. Quando não é
necessário usar todos os três dígitos, deve-se usar pelo menos um. Por
exemplo, a representação em octal para o caractere backspace é \10 e \101
para a letra A.
De forma análoga deve-se usar um dígito pelo menos para uma sequência de
escape hexadecimal, mas poderão ser omitidos o segundo e terceiros dígitos.
Dessa maneira, poderia especificar a sequência de escape hexadecimal para o
caractere backspace como[46] \x8, \x08, ou \x008.
De forma diferente das sequências de escape octais, o número de dígitos
hexadecimais em uma sequência de escape não é limitado. Uma sequência de
escape hexadecimal terminará com o primeiro caráter que não é um dígito
hexadecimal. Uma vez que os dígitos hexadecimais incluem as letras a até f,
deve-se ter cuidado para assegurar que a sequência de escape termina num
dígito intencional. Para evitar confusão, podem-se colocar as definições de
caracteres hexadecimais numa definição de macro:
#define Bell '\x07'
6.4.4. Strings Literais
Uma "string literal" é uma sequência de caracteres inseridos entre aspas
duplas (“ ”). As strings literais são usadas para representar uma sequência de
caracteres que, juntos, formam uma string com terminador nulo.
O exemplo a seguir é uma string literal simples:
char amessage = "Esta é uma string literal.";
Todos os códigos de escape, listados na tabela anterior, são válidos em strings
literais. Para representar as aspas simples numa string literal, deve ser usada a
sequência de escape \". As aspas simples podem ser representadas sem a
necessidade de uma sequência de escape. A barra invertida (backslash ‘\’)
126
deve ser seguida de uma segunda barra quando colocada dentro da string.
Quando aparecer uma barra invertida no fim de uma linha, será sempre
interpretada como um caractere de continuação de linha.
Tipos de Strings Literais
As strings literais são do tipo arranjo de char (char []). As strings de
caracteres longos são do tipo arranjo de wchar_t (wchar_t []). Isso indica que
uma string é um arranjo de elementos do tipo char. O número de elementos do
arranjo é igual ao número de caracteres da string mais o caractere de
terminação nulo (null).
Armazenamento de Literais
Os caracteres de uma string literal são armazenados na sequência em posições
consecutivas de memória. Uma sequência de escape (tais como \\ ou \”)
dentro de uma string literal contará como um caractere único. Um caractere
null (representado pela sequência de escape \0) será automaticamente anexado
e indicará o final da string literal.
Concatenação de Strings Literais
Para formar strings literais de mais de uma linha, podem ser concatenadas
duas strings. Para fazer isso, deve ser digitada uma barra invertida, e então
pressionada a tecla ENTER. A barra invertida instrui o compilador a ignorar o
caractere newline. Por exemplo, a string literal
"Strings longas podem ser queb\
radas em duas ou mais partes."
é idêntica à string
“Strings longas podem ser quebradas em duas ou mais partes."
A concatenação de strings pode ser usada em qualquer lugar, sempre que
seguido de um caractere de nova linha (newline) para colocar strings maiores
que uma linha.
Para forçar uma nova linha dentro de uma string literal, entre a sequência de
escape de nova linha (\n) no ponto da string onde haja uma quebra de linha:
"Entre em um número entre 1 e 100\n ou pressione ENTER"
Uma vez que as strings podem começar em qualquer coluna do código fonte e
as strings longas podem continuar em qualquer coluna da linha seguinte, estas
poderão ser posicionadas para melhorar a legibilidade do código fonte. De
qualquer forma, a representação no dispositivo de saídanão permanecerá
inalterada. Por exemplo:
printf ( "Esta é a primeira metade da string, "
"esta é a segunda metade") ;
Contanto que cada parte da string seja incluída entre aspas duplas, as partes
serão concatenadas produzindo uma única string no dispositivo de saída.
"Esta é a primeira metade da string, esta é a segunda metade”
127
Um ponteiro para string, inicializado como dois strings literais, separados
somente por espaços em branco, será armazenado como uma única string[47].
Quando corretamente referenciado, como no exemplo seguinte, o resultado é
idêntico ao exemplo anterior
char *string = "Esta é a primeira metade da string, "
"esta é a segunda metade";
printf("%s" , string ) ;
128
6.5. Caracteres Especiais e Pontuação
A pontuação e os caracteres especiais da linguagem C possuem vários usos,
desde organizar o texto de programa, até definir as tarefas que o compilador
ou o programa compilado deverão efetuar. Eles não especificam uma
operação a ser executada. Alguns símbolos de pontuação também são
operadores. O compilador determinará o seu uso de acordo com o contexto.
[ ] ( ) { } * , : = ; ... #
Esses caracteres possuem significados especiais em C. O seu uso é descrito
ao longo deste documento. O caractere cardinal (#) pode ser usado somente
nas diretivas de pré-processador.
129
6.6. Exercícios
1. Se dois fios metálicos de materiais diferentes forem conectados em uma das
suas extremidades, tendo uma temperatura T1 na junção e T2 nas extremidades livres, é
gerada uma força eletromotriz (efeito Seebeck), cujo valor é dado pela expressão:
[V]
Os sensores de temperatura que fazem uso deste efeito são chamados de Termopares ou Pares
Termoelétricos. Para termopares do tipo T (Cobre-Constantan) por exemplo, C1 = 62.1 e C2 = -0.045.
Faça um programa que :
a. Pergunte o número de dados a serem entrados via teclado (valores de T1). Utilize esse valor
para fazer a alocação dinâmica de memória (usar ponteiros). Além disso, o programa deverá
permitir a entrada dos valores dos parâmetros C1, C2, T2.
b. Calcule a tensão E, para cada valor de T1.
c. Calcule quantos valores calculados de E estão acima da média.
d. Permita a gravação e leitura dos dados em arquivo, com nome e caminho definido pelo usuário
via teclado. O programa deverá armazenar também os parâmetros.
2. Faça um programa que permita a entrada de um número inteiro (32 bits); uma
função que permita setar ou resetar um bit, do número entrado via teclado. Ex.: unsigned
int set_bit (unsigned int var, unsigned int bitnum, unsigned int val) onde a função
retorna o valor resultante depois da operação, e recebe os parâmetros var, que é o valor ou
variável, bitnum é o número do bit, e val define se o bit vai para zero ou um. Se
chamarmos a função fazendo (sabendo que y = 0) x = setbit (y,4,1) vai setar o bit 4 (quinto
bit porque o primeiro é o bit 0) do valor contido na variável y e colocar o resultado na
variável x, o resultado será neste caso x = 32. O valor do número deverá ser apresentado
na tela, em binário decimal e hexadecimal, antes e depois da operação. Faça também uma
função que permita a inversão de todos os bits do número inteiro, e que apresente os
resultados de maneira idêntica aos itens anteriores.
3. Converta os seguintes números para a representação nos sistemas binário,
decimal e hexadecimal. Demonstre o desenvolvimento aritmético.
552D
0000010101111B
191H
4. Um termistor é um resistor que varia a sua resistência com a variação da
temperatura segundo a relação abaixo:
Onde os valores de é o coeficiente de variação da resistência com a temperatura, Ro é a resistência a
uma determinada temperatura pré-definida, e To é a temperatura que define os dois valores anteriores.
Essas grandezas devem ser definidas pelo usuário via teclado.
Elabore um programa com duas opções de menu:
Dados n valores de T calcula n valores de R
Dados n valores de R calcula as n T
Obs. Para ambos os itens apresentar o valores máximo, mínimo e a média.
Lembrar que: ; ;
5. Faça um programa que calcule o fatorial de 10000 números armazenados num
vetor (com ponto flutuante), calcule a média do vetor, o valor máximo e o valor mínimo.
130
131
7. ESTRUTURA DE UM PROGRAMA
O objetivo deste capítulo é fornecer uma visão geral da programação em C e a
execução de programas. Também serão vistos alguns termos e características
importantes para o entendimento dos programas em C e os seus
componentes. Os tópicos que serão discutidos aqui incluem:
Arquivos fonte e programas fonte.
A função main e a execução dos programas.
Argumentos de linha de comando para verificação.
Tempo de vida, escopo, visibilidade e encadeamentos (linkage).
Espaço de Nomes
132
7.1. Arquivos Fonte e Programas Fonte
Um programa fonte pode ser dividido em um ou mais "arquivos fonte". O
arquivo de entrada para o compilador é chamado de "unidade para tradução".
Os componentes de uma unidade para tradução são declarações externas que
incluem definições de funções e declarações de identificadores. Essas
declarações e definições podem estar em arquivos de fonte, arquivos de
cabeçalho (header), bibliotecas, e outros arquivos que o programa precisar.
Cada unidade para tradução deve ser compilada. O arquivo objeto resultante
deve passar pelo processo de linker para formar um programa executável.
Um "programa fonte em C" é uma coleção de diretivas, pragmas, declarações,
definições, blocos de instruções e funções. Porém, a localização desses
componentes num programa afetará a utilização das variáveis e funções
usadas no programa.
Os arquivos fonte não precisam ter instruções executáveis. Por exemplo,
pode-se achar útil colocar definições de variáveis em um arquivo fonte e
então, declarar as referências para essas variáveis em outros arquivos fonte
que as utilizam. Essa técnica faz com que as definições sejam fáceis de serem
encontradas e atualizadas, quando necessário. Pela mesma razão, as
constantes e macros[48] são organizadas em arquivos separados chamados
"include files" ou "header files" (arquivos de cabeçalho), que podem ser
referenciados no arquivo fonte quando requerido.
7.1.1. Diretivas de Pré-Processador
A "diretiva" instrui o pré-processador C a executar uma ação específica no
texto do programa, antes de compilação. Esse exemplo usa a diretiva de pré-
processador #define:
#define MAX 100
Essa declaração diz para o compilador, que substitua cada ocorrência de
MAX por 100 antes de compilação. As diretivas[49] de pré-processador em C
são mostradas a seguir como exemplo.
#define #endif #ifdef #line
#elif #error #ifndef #pragma
#else #if #include #undef
Tabela 7-1– Exemplo de diretivas de pré-processador.
7.1.2. Declarações e Definições
Uma "declaração" estabelece uma associação entre uma variável particular,
função, ou tipo e os seus atributos. . Uma declaração também especifica onde
e quando um identificador pode ser acessado. .
Uma "definição" de uma variável estabelece as mesmas associações, que uma
133
declaração, mas também define a alocação de memória para essa variável.
Por exemplo, as funções main, find e count, e as variáveis var e val são
definidas num arquivo fonte nesta ordem:
void main(){
...
}
int var = 0;
double val[MAXVAL];
char find( fileptr ){
...
}
int count( double f ){
...
}
Código 7-1
As funções var e val podem ser usadas nas funções find e count; não são
necessárias declarações posteriores. Essas variáveis não são visíveis (nãopodem ser acessadas) pela função main por estarem declaradas após.
7.1.3. Declarações e Definições de Funções
Os protótipos de funções estabelecem o nome da função, seu tipo de retorno,
o tipo e número de parâmetros formais. Uma definição de função inclui o
corpo de função.
Declarações de funções e variáveis podem aparecer dentro ou fora de uma
definição de função. Qualquer declaração dentro de uma definição de função
é dita como sendo de nível "interno" ou "local ". Qualquer declaração
colocada fora das definições da função, é dita de nível "externo", "global", ou
de "escopo de arquivo". As definições das variáveis, como nas declarações,
podem aparecer em nível interno (dentro de uma definição de função) ou num
nível externo (fora de todas as definições de função). Definições de função
sempre deverão ocorrer no nível externo.
7.1.4. Blocos
Uma sucessão de declarações, definições e instruções encerradas dentro de
chaves ({...}) são chamadas de "bloco". Existem dois tipos de blocos em C.
Uma "instrução composta" de uma ou mais declarações, é um tipo de bloco.
O outro bloco, a "definição de função", consiste em um conjunto de
instruções (o corpo da função) mais o cabeçalho associado à função (nome de
função, tipo de retorno e seus parâmetros). Um bloco que está dentro de outro
é dito ser aninhado.
O leitor deve notar, que nem tudo o que está entre chaves constitui um
conjunto de instruções. Por exemplo, uma estrutura (struct), ou os elementos
134
de enumeração (enum), podem aparecer dentro de chaves, porém não são um
conjunto de instruções[50].
135
7.2. A Função main e a Execução do Programa
Todo programa em C tem uma função principal que deve ser nomeada com o
nome de main. . A função principal serve como o ponto de partida para
execução do programa. Normalmente, ela controla a execução do programa
direcionando as chamadas para outras funções no programa. Um programa
normalmente para a execução no final da função main, embora possa
terminar em outros pontos no programa, devido a uma variedade de razões.
Às vezes, quando certo erro é detectado durante a execução, pode-se querer
forçar a terminação de um programa. Para fazer isso, pode ser utilizada a
função exit.
As funções dentro de um programa fonte executam uma ou mais tarefas
específicas. A função main pode chamar essas funções para executar as suas
tarefas respectivas. Quando a função main chama outra função, ela passa o
controle da execução para a função, de forma que execução começa na
primeira instrução da mesma. Uma função retorna o controle para a função
main quando for executada uma função return ou quando o fim da função é
encontrado.
Qualquer função, incluindo a main, pode ser declarada como tendo
parâmetros. O termo "parâmetro" ou "parâmetro formal" se refere ao
identificador que recebe um valor passado para a função. Quando uma função
chama outra, a função chamada recebe valores por seus parâmetros da função
que a chamou. Esses valores são chamados "argumentos". Pode-se declarar
parâmetros formais para a função main de forma que possa receber
argumentos através da linha de comando, usando o seguinte formato:
main( int argc, char *argv[ ], char *envp[ ] )
Para passar informações para a função main, os parâmetros são
tradicionalmente nomeados como argc e argv, embora o compilador C não
requer esses nomes. Os tipos para argc e argv são definidos pela linguagem C.
Tradicionalmente, se um terceiro parâmetro é passado para a função main,
será chamado envp. O tipo para o parâmetro envp é padronizado pelo ANSI,
mas não o seu nome. As seções seguintes explicam esses parâmetros com
maiores detalhes.
7.2.1. Os Argumentos argc e argv
A função main() pode ter parâmetros formais. Mas o programador não pode
escolher quais serão eles. A declaração mais geral da função main() é:
int main (int argc,char *argv[]);
Os parâmetros argc e argv dão ao programador acesso à linha de comando
com a qual o programa foi chamado.
O argc (argument count) é um inteiro e possui o número de argumentos com
136
os quais a função main() foi chamada na linha de comando. Ele é, no mínimo
1, pois o nome do programa é contado como sendo o primeiro argumento.
O argv (argument values) é um ponteiro para uma matriz de strings. Cada
string dessa matriz é um dos parâmetros da linha de comando. O argv[0]
sempre aponta para o nome do programa (que, como já foi dito, é considerado
o primeiro argumento). É para saber quantos elementos temos em argv que
temos argc.
Exemplo: Escreva um programa que faça uso dos parâmetros argv e argc. O
programa deverá receber da linha de comando o dia, mês e ano correntes, e
imprimir a data em formato apropriado. Veja o exemplo, supondo que o
executável se chame data:
data 19 04 99
O programa deverá imprimir:
19 de abril de 1999
#include <stdio.h>
#include <stdlib.h>
void main(int argc, char *argv[]){
int mes;
char *nomemes [] = {"Janeiro", "Fevereiro", "Março", "Abril", "Maio",
"Junho", "Julho", "Agosto", "Setembro", "Outubro",
"Novembro", "Dezembro"};
if(argc == 4){ /* Testa se o numero de parametros fornecidos esta' correto
o primeiro parametro e' o nome do programa, o segundo o dia
o terceiro o mes e o quarto os dois ultimos algarismos do ano */
mes = atoi(argv[2]); /* argv contem strings. A string referente ao mes
deve ser transformada em um numero inteiro.
A funcao atoi esta sendo usada para isto:
recebe a string e transforma no
inteiro equivalente */
if (mes<1 || mes>12) /* Testa se o mes e' valido */
printf("Erro!\nUso: data dia mes ano, todos inteiros");
else
printf("\n%s de %s de 19%s", argv[1], nomemes[mes-1], argv[3]);
}
else printf("Erro!\nUso: data dia mes ano, todos inteiros");
}
Código 7-2
7.2.2. Descrição dos Argumentos
O parâmetro argc da função main[51] é um inteiro que especifica quantos
argumentos são passados ao programa a partir da linha de comando. Uma vez
que o nome do programa é considerado um argumento, o valor de argc deve
ser pelo menos um.
O parâmetro argv é um arranjo de ponteiros para strings terminadas em nulo,
que representam os argumentos de programa. Cada elemento do array aponta
a uma string de um argumento passado para a função main (ou wmain). O
137
parâmetro argv pode ser declarado como um array de ponteiros para os dados
do tipo char (char *argv []) ou como um ponteiro para ponteiros do tipo char
(char **argv). . A primeira string (argv[0]) é o nome do programa. O último
ponteiro (argv[argc]) é NULL.
O parâmetro envp é um ponteiro para um array de strings terminadas em null
que representam os valores colocados pelo usuário nas variáveis do ambiente.
O parâmetro envp pode ser declarado como um array de ponteiros para char
(char *envp []) ou como um ponteiro para ponteiros para char (char * *envp).
. O final do array é indicado por um ponteiro NULL. Notar que o bloco de
ambiente passado para a função main ou wmain será uma "cópia instantânea"
do ambiente atual.A seguir um exemplo[52].
#include <stdio.h>
#include <string.h>
void main( int argc, char *argv[], char *envp[] ){
int iNumberLines=0,i; /* O padrao é sem número de linhas. */
/* Se forem colocados mais dados além do nome do arquivo executavel e se */
/* foi especificado o comando de linha /n, a lista de variaveis do ambiente */
/* será enumerada por linha. */
if( argc == 2 && stricmp( argv[1], "/n" ) == 0 )
iNumberLines = 1;
/* Move-se atraves da lista de strings até encontrar um caractere NULL. */
for(i = 0; envp[i] != NULL; ++i ){
if( iNumberLines ) {
printf(“%d : %s \n”,i,envp[i]);
}
}
}
Código 7-3
138
7.3. Tempo de Vida, Escopo, Visibilidade e Conexões
Para entender como um programa em C trabalha, deve-se entender as regras
que determinam como podem ser usadas as variáveis e funções dentro do
programa. Vários conceitos são cruciais para o entendimento dessas regras:
Tempo de vida
Escopo e visibilidade
Conexões
7.3.1. Tempo de Vida
O "tempo de vida" é o período durante execução de um programa no qual
uma variável ou função existe. A duração do armazenamento do identificador
determina o seu tempo de vida.
Um identificador declarado como o especificador de classe de
armazenamento static, tem duração armazenamento estática. Os
identificadores com duração de armazenamento estática (também chamados
"globais") têm o seu local de armazenamento e valor definido, preservado
durante a execução do programa. O local de armazenamento é reservado e o
valor armazenado do identificador é inicializado uma vez, antes do início do
programa. Um identificador declarado como tendo conexões externas ou
internas também tem duração estática.
Um identificador declarado sem o especificador de classe de armazenamento
static, tem duração de armazenamento automatic se for declarado dentro de
uma função. Um identificador que possui duração de armazenamento
automática (identificador local), tem local de armazenamento e valor
definido, somente dentro do bloco onde o mesmo está definido ou declarado.
Para um identificador automático será alocado um novo local de
armazenamento cada vez que entra no bloco, e perde seu local de
armazenamento (e o seu valor) quando o programa encerra o bloco. Os
identificadores declarados numa função sem conexões, também têm duração
de armazenamento automática.
As regras seguintes especificam se um identificador tem tempo de vida global
(estático) ou local (automático):
Todas as funções têm tempo de vida estático. Portanto, elas existem
durante toda a execução do programa. Os identificadores declarados no
nível externo (i.e., fora de todos os blocos no programa, no mesmo nível
de definições de funções) sempre terão tempo de vida global (estático).
Se uma variável local possuir um inicializador, a mesma será
inicializada toda vez que for criada (a menos que seja declarada como
static). Os parâmetros de função também têm tempo de vida local. É
possível especificar um tempo de vida global para um identificador
139
dentro de um bloco, incluindo o especificador de classe de
armazenamento static na sua declaração. Se uma variável for declarada
estática, a mesma reterá seu valor, desde uma entrada de bloco até a
próxima.
Mesmo que um identificador ou variável, com tempo de vida global, exista ao
longo da execução do programa fonte (por exemplo, uma variável
externamente declarada ou uma variável local declarada com a palavra-chave
static), o mesmo poderá não ser visível em todas as partes do programa.
A memória pode ser alocada, quando necessário (alocação dinâmica),
utilizando rotinas específicas da biblioteca de funções, tais como a função
malloc. Considerando que a alocação dinâmica de memória deverá utilizar
rotinas da biblioteca, não é considerada parte da linguagem. .
7.3.2. Escopo e Visibilidade
A "visibilidade" de um identificador determina as porções do programa no
qual pode ser referenciado; o seu escopo. Um identificador é visível (i.e.,
usado de ser de pode) somente em porções de um programa incluídas no seu
escopo, que pode ser limitado (de maneira a aumentar as suas restrições) para
o arquivo, função, bloco, ou protótipo de função no qual ele aparece. O
escopo de um identificador é a parte do programa no qual o seu nome pode
ser usado. Há quatro tipos de escopo: função, arquivo, bloco e protótipo de
função.
Todos os identificadores têm o seu escopo determinado pelo nível no qual a
declaração ocorre. As regras para cada tipo de escopo que controla a
visibilidade dos identificadores dentro de um programa são citadas a seguir:
Escopo de Arquivo: A declaração ou especificação do tipo para um
identificador com escopo de arquivo, aparece fora de qualquer bloco ou
lista de parâmetros, e estará acessível de qualquer lugar na unidade de
tradução depois da sua declaração. Os nomes de identificadores com
escopo de arquivo são chamados frequentemente "globais" ou
"externos". O escopo de um identificador global começa no ponto de sua
definição ou declaração e termina no final unidade de tradução.
Escopo de função: Um label é um identificador único no escopo de
uma função. Um label é implicitamente declarado pelo seu uso numa
instrução. Os labels devem ser únicos dentro de uma função.
Escopo de bloco: O declarador ou especificador de tipo para um
identificador com escopo de bloco aparece dentro de um bloco ou uma
lista de declarações de parâmetros formais numa definição de função. Só
é visível do ponto de sua declaração ou definição, até o final do bloco
que contém sua declaração ou definição. Seu escopo é limitado àquele
bloco e para qualquer bloco aninhado naquele bloco, finalizando no
140
Nível
Item
Especificador
de Classe de
Armazenamento
Resultado:
Tempo de
Vida
Visibilidade
(escopo)
Escopo
de
Arquivo
Definição
de variável
static Global Resto do
arquivo
fonte no
qual ela
ocorre.
Declaração
de variável
extern Global Resto do
arquivo
fonte na
qual ela
ocorre
Protótipo
de função
ou
definição
static Global Num único
arquivo
fonte
Protótipo
de função
extern Global Resto do
arquivo
fonte
Escopo
de
Bloco
Declaração
de variável
extern Global Bloco
Definição
de variável
static Global Bloco
Definição
de variável
auto ou register Local Bloco
fechamento das chaves que delimitam o bloco associado. Tais
identificadores são chamados de "variáveis locais".
Escopo de protótipo de função: O declarador ou especificador de
tipo para o identificador com escopo de protótipo de função aparece
dentro da lista de declarações de parâmetro em um protótipo de função
(não na declaração de função). Seu escopo termina no final do
declarador da função.
As variáveis e
funções declaradas
no nível externo
com o
especificador de
classe de
armazenamento
static são visíveis
somente dentro do
arquivo de fonte no
qual elas são
definidas. Todas as
outras funções são
globalmente
visíveis.
Resumo de Tempo
de Vida e
Visibilidade
A tabela a seguir
resume as
características de
visibilidade
(escopo) e tempo de vida para a maioria dos identificadores. As primeiras três
colunas mostram os atributos que definem vida e visibilidade. Um
identificador com os atributos dados pelas primeiras três colunas tem um
tempo de vida e visibilidade mostrada na quarta e quinta coluna. Porém, esta
tabela não cobre todos os casospossíveis. .
Tabela 7-2 – Tempo de vida e escopo
No seguinte exemplo, são mostrados blocos, aninhamentos e visibilidade de
variáveis.
#include <stdio.h> /* Biblioteca padrão de funções de entrada e saída */
int i = 1; /* i é definida no nível externo */
int main(){ /* a função main é definida no nível externo */
printf( "%d\n", i ); /* Imprime 1 (valor de nível externo i) */
{ /* Início do primeiro bloco aninhado */
141
int i = 2, j = 3; /* i e j definidas em nível interno */
printf( "%d %d\n", i, j ); /* Imprime 2, 3 */
{ /* Inicio do segundo bloco aninhado */
int i = 0; /* i é redefinido */
printf( "%d %d\n", i, j ); /* Imprime 0, 3 */
} /* Fim do segundo bloco aninhado */
printf( "%d\n", i ); /* Imprime 2 (a outra definição */
/* é restaurada) */
} /* Fim do primeiro bloco aninhado */
printf( "%d\n", i ); /* Imprime 1(a definição de nível externo*/
/* é restaurada) */
return 0;
}
Código 7-4
Nesse exemplo existem quatro níveis de visibilidade: o nível externo e três
níveis de bloco. Os valores são enviados para o dispositivo de saída como
indicado nos comentários que acompanham a cada linha de instrução.
7.3.3. Encadeamentos
Os nomes dos identificadores podem se referir a diferentes identificadores em
diferentes escopos. Um identificador declarado em diferentes escopos, ou no
mesmo, mais de uma vez, pode se referir ao identificador ou função por um
processo chamado "encadeamento". O encadeamento determina as porções do
programa no qual um identificador pode ser referenciado (visível). Existem
três tipos de encadeamento: interno, externo, e sem encadeamento.
Encadeamento Interno
Se a declaração de um identificador de escopo de arquivo para um objeto ou
função contém o especificador de classe de armazenamento static, o
identificador terá encadeamento interno. Caso contrário, o identificador tem
encadeamento externo. .
Dentro de uma unidade de tradução, cada instância de um identificador com
encadeamento interno, denota o mesmo identificador ou função. Os
identificadores conectados internamente são únicos para a unidade de
tradução.
Encadeamento Externo
Se a primeira declaração em nível de escopo de arquivo, para um
identificador não usa o especificador de classe de armazenamento static, o
objeto terá encadeamento externo.
Se a declaração de um identificador para uma função não tem nenhum
especificador de classe de armazenamento, seu encadeamento será
exatamente determinado como se fosse declarada com o especificador de
classe de armazenamento extern. Se a declaração de um identificador para um
objeto tem escopo e nenhum especificador de classe de armazenamento, seu
encadeamento será externo.
142
O nome de um identificador com encadeamento externo designa a mesma
função ou o mesmo objeto de dados, como faz qualquer outra declaração para
o mesmo nome com encadeamento extern. As duas declarações podem estar
na mesma unidade de tradução ou em unidades de tradução diferentes. Se o
objeto ou função também tem tempo de vida global, o objeto ou função será
compartilhado pelo programa inteiro.
Sem Encadeamento
Se uma declaração para um identificador dentro de um bloco não inclui o
especificador de classe de armazenamento externo, o identificador não terá
nenhum encadeamento e será único para a função.
Os seguintes identificadores não possuem nenhum encadeamento:
Um identificador declarado para ser qualquer coisa diferente de um
objeto ou uma função
Um identificador declarado para ser um parâmetro de função
Um identificador de escopo de bloco para um objeto declarado sem
o especificador de classe armazenamento extern.
Se um identificador não tem nenhum encadeamento, e for declarado o
mesmo nome novamente (em um declarador ou especificador de tipo), no
mesmo nível de escopo, será gerado um erro de redefinição de símbolo.
143
7.4. Espaços de Nomes
O compilador define um espaço de nomes para poder distinguir os
identificadores usados em diferentes itens. Os nomes dentro de cada espaço
de nomes, devem ser únicos para evitar conflitos, mas um nome idêntico pode
aparecer em mais de um espaço de nomes. Isso permite que possa ser
utilizado o identificador para dois ou mais itens diferentes, contanto que os
itens estejam em espaços diferentes. O compilador pode definir as referências
baseado no contexto sintático do identificador dentro do programa.
A lista a seguir descreve os espaços de nomes usados em C.
Labels: Os labels fazem parte das instruções. A definição dos labels· é
seguida de dois pontos (:) mas este não faz parte do mesmo, exceto no label
pré-definido case. O uso do label já declarado deve acontecer sempre
imediatamente seguido da instrução goto. Os labels devem usar nomes
diferentes daqueles já utilizados como identificadores, ou de outros labels em
outras funções. Por exemplo, um trecho de programa:
...
FasePrincipal:
.....
FaseSecundaria:
...
if(fase == 2) goto FasePrincipal;
....
Estruturas, uniões e variáveis de enumeração: Esses labels são parte de
estrutura, uniões e especificadores do tipo enumeração e, se presentes, sempre
imediatamente seguidos das palavras reservadas struct, union, ou enum. Os
nomes dos labels devem ser distintos de todos os que definem uma estrutura,
enumeração, ou union com a mesma visibilidade.
Membros de estruturas ou unions: São alocados no mesmo espaço de
nomes associados com cada tipo de estrutura e union. I.e., o mesmo
identificador pode ser, ao mesmo tempo, um nome de componente em
qualquer número de estruturas ou unions. As definições de nomes de
componente sempre acontecem dentro dos especificadores de tipo para as
estruturas ou unions. Sempre devem ser usados os nomes de componentes
imediatamente seguidos do operador de seleção de membro (-> e .). O nome
de um membro deve ser único dentro da struct ou union, mas pode não ser
diferente de outros nomes no programa, inclusive os nomes de membros de
diferentes structs e unions, ou o nome da própria struct em si.
Identificadores ordinários: Todos os outros nomes entram em um espaço de
nomes que inclui variáveis, funções (incluindo parâmetros formais e variáveis
locais), e constantes de enumeração. Os nomes dos identificadores possuem
visibilidade aninhada, de forma que possam ser redefinidos dentro de blocos
144
de instruções.
Nomes typedef: Os nomes do tipo typedef não podem ser usados como
identificadores no mesmo escopo.
Por exemplo, uma vez que os labels de structs, membros de structs, e nomes
variáveis estão distribuídos em três espaços de nomes diferentes, os três itens
nomeados como resistor neste exemplo não conflitam entre si.. O contexto de
cada item permite a interpretação correta de cada ocorrência de resistor no
programa[53].
struct resistor {
char referencia[20];
int tolerancia;
int resistor;
} resistor;
Quando resistor aparece depois do keyword struct, o compilador reconhece-o
como sendo o label da struct. Quando resistor aparece depois de um operadorde seleção de membro (-> ou.), o nome se refere ao membro da struct. Em
outros contextos, resistor se refere à variável struct. Porém, não é
recomendado sobrecarregar o espaço de nomes já que isso dificulta o
discernimento do programa.
145
7.5. Exercícios
1. O que é um programa fonte?
2. Qual a utilidade das diretivas de pré-processamento?
3. Qual a importância da função main?
4. Por que foram implementados o tempo de vida, escopo e visibilidade para as
variáveis e identificadores da linguagem C?
146
8. TIPOS E DECLARAÇÕES
Este capítulo descreve a declaração e inicialização de variáveis, funções e
tipos. A linguagem C inclui um grupo padrão de tipos de dados. O
programador também pode adicionar os seus próprios tipos de dados,
chamados de "tipos derivados", pela declaração de novos tipos baseados nos
já existentes. Os seguintes tópicos serão discutidos:
Declarações
Classes de armazenamento
Especificadores de tipo
Qualificadores de tipo
Declaradores e declarações de variáveis.
Interpretação de declaradores mais complexos.
Inicialização
Tipos básicos de armazenamento.
Tipos incompletos
Declarações typedef
Atributos de classes estendidas de armazenamento.
147
8.1. Declarações
Uma "declaração" especifica a interpretação e os atributos de um grupo de
identificadores. A declaração que ocasiona o armazenamento de dados (sendo
estes reservados para o objeto ou função nomeada pelo identificador), é
chamada de "definição". As declarações em C para variáveis, funções e tipos,
apresentam a seguinte sintaxe:
Na forma geral de uma declaração de variável, o especificador de tipo indica
o tipo de dado que será armazenado pela variável. O especificador de tipo
pode ser um composto, como quando o tipo é modificado pelas keywords
const ou volatile. O declarador fornece o nome da variável, possivelmente,
modificado para declarar uma ordem ou um tipo de ponteiro. Por exemplo,
int const *fp;
declara uma variável chamada fp como um ponteiro para um valor int não
modificável (const). Podem ser definidas várias variáveis numa mesma
declaração, usando declaradores múltiplos separados por vírgulas.
Uma declaração deve ter pelo menos declarador, ou seu especificador de tipo
deve declarar um label de uma struct, union, ou membros de um objeto
enum[54]. Os declaradores proveem qualquer informação restante sobre um
identificador. Um declarador é um identificador que pode ser modificado com
colchetes ([ ]), asteriscos (*), ou parênteses (( )) para declarar um array,
ponteiro, ou tipo de função, respectivamente. Quando forem declaradas
variáveis simples (tais como caracteres, inteiro, e de ponto flutuante), ou
structs e unions de variáveis simples, o declarador é o próprio identificador. .
Todas as definições são declarações implícitas, mas não todas as declarações
são definições. Por exemplo, as declarações de variáveis que começam com o
especificador de classe de armazenamento extern estão "referenciando", e
não "definindo". Se uma variável externa é referenciada antes de ser definida,
ou se ela estiver definida em outro arquivo fonte, será necessária uma
declaração extern. O local de armazenamento não será alocado somente
através de "referências", nem há inicializações de variáveis nas declarações.
Uma classe de armazenamento ou um tipo (ou ambos) é requerido nas
declarações variáveis. Em geral, somente um tipo de classe de
armazenamento será permitido em uma declaração, e não todos o
especificadores de classe de armazenamento, serão permitidos em todo
contexto. O especificador de uma classe de armazenamento de uma
declaração, afeta a forma em que o item declarado, armazenado e inicializado,
e define quais as partes do programa que poderão referenciá-la.
Os especificadores de classe de armazenamento incluem: auto, extern,
register, static e typedef.
148
A localização da declaração dentro do programa fonte e a presença ou
ausência de outras declarações da variável, são fatores importantes na
determinação do tempo de vida das variáveis. Poderá haver múltiplas
redeclarações, mas somente uma definição. Porém, uma definição pode
aparecer em mais de uma unidade de tradução. Para objetos com
encadeamento interno, essa regra se aplica separadamente para cada unidade
de tradução, porque os objetos encadeados internamente são únicos para a
unidade de tradução. Para objetos com encadeamento externo, essa regra se
aplica ao programa inteiro.
Os especificadores de tipo fornecem alguma informação sobre os tipos de
dados dos identificadores. O especificador de tipo default é int. Os
especificadores de tipo também podem definir rótulos de tipo, nomes de
componentes de structs e unions, e constantes de enumeração[55].
Existem dois tipos de qualificadores de tipo: const e volatile. Esses
qualificadores especificam propriedades adicionais dos tipos que só são
relevantes, quando forem acessados objetos deste tipo por l-values[56].
149
8.2. Classes de Armazenamento
A "classe de armazenamento" de uma variável determina se o item tem tempo
de vida "global" ou "local". A linguagem C chama esses dois tempos de vida
como "static" e "automatic". Uma variável com tempo de vida global, existe
e tem valor ao longo da execução do todo o programa. Todas as funções têm
tempo de vida global.
As variáveis automáticas, ou variáveis com tempo de vida local, têm o seus
locais de memória alocados cada vez que o controle da execução passa para o
bloco no qual elas estão definidas. Quando a execução retorna, as variáveis já
não existirão.
A linguagem C possui os seguintes especificadores de classes de
armazenamento:
auto
register
static
extern
typedef
Somente pode ser utilizado um especificador de classe de armazenamento,
como declarador, em uma declaração. Se nenhum especificador de classe de
armazenamento for utilizado, as declarações dentro de um bloco criarão
objetos automáticos.
Os itens declarados com os especificadores auto ou register têm tempo de
vida local. Os itens declarados com os especificadores static ou extern têm
tempo de vida global.
O lugar de colocação das declarações de variáveis e funções dentro de
arquivos fonte também afeta a classe de armazenamento e a visibilidade. As
declarações que ficam fora das definições de função são ditas de "nível
externo". As declarações dentro de definições de função são de "nível
interno".
O significado exato de cada especificador de classe de armazenamento,
depende de dois fatores:
Se a declaração aparece no nível externo ou interno
Se o item que está sendo declarado é uma variável ou uma função
8.2.1. Especificadores de Classe de Armazenamento para Declarações de
Nível Externo
Variáveis externas são variáveis de escopo de arquivo. Elas são definidas fora
de qualquer função, e estão potencialmente disponíveis para muitas funções.
As funções podem somente ser definidas em nível externo e, portanto, não
150
podem ser aninhadas. Por default, todas as referências para variáveis externas
e funções do mesmo nome são referenciadas ao mesmo objeto, o que permite
ter um "encadeamento" externo.
As declarações de variáveis no nível externo são definições de variáveis
(declarações que definem), ou referências para variáveis definidas em outro
lugar (declarações que referenciam).
Uma declaraçãode variável externa, que também inicializa a mesma
(implicitamente ou explicitamente), é uma declaração de definição da
variável. Uma definição no nível externo pode tomar várias formas:
Uma variável declarada com o especificador de armazenamento
static. Uma variável static pode ser explicitamente inicializada com uma
expressão constante, como descrito na seção 8.7 Inicialização. Caso o
inicializador fosse omitido, a variável é inicializada com 0 por default.
Por exemplo, estas duas declarações são ambas consideradas definições
da variável k.
static int k = 16;
static int k;
Uma variável que é inicializada explicitamente de nível externo. Por
exemplo, int j = 3; é uma definição para a variável j.
Em declarações variáveis em nível externo (quer dizer, fora de todas as
funções), podem ser usados os especificadores de classes de armazenamento
extern ou static, ou serem omitidos completamente. Os especificadores auto e
register não podem ser utilizados em nível externo.
Uma vez que, uma variável é definida em nível externo, ela será visível ao
longo do resto da unidade de tradução. Essa variável não será visível antes da
sua declaração, no mesmo arquivo fonte. Também, não será visível em outros
arquivos fonte do programa, a menos que uma declaração de referência a faça
visível, como descrito a seguir.
As regras relativas a static incluem:
As variáveis declaradas fora de todos os blocos, e sem a keyword
static, retêm os seus valores durante toda a execução do programa. Para
restringir o acesso a uma unidade de tradução particular, a keyword
static deve ser utilizada. Isso fornece um "encadeamento interno". Para
deixá-las globais para o programa inteiro, deve-se omitir a classe de
armazenamento explícita, ou usar a keyword extern. Isso resultará num
"encadeamento externo".
O programador poderá definir uma variável de nível externo apenas
uma vez dentro do programa. Poderão ser definidas outras variáveis com
o mesmo nome, usando o especificador de classe de armazenamento
static em uma unidade de tradução diferente. Considerando que cada
definição estática somente será visível dentro de sua própria unidade de
151
tradução, nenhum conflito acontecerá. Isso fornece um modo útil para
esconder nomes de identificadores que devem ser compartilhados entre
funções de uma única unidade de tradução, mas que não serão visíveis a
outras unidades de tradução.
O especificador de classe de armazenamento static também pode ser
aplicado a funções. Caso uma função for declarada static, o seu nome
será invisível fora do arquivo no qual é declarada.
As regras para usar extern são:
O especificador de classe de armazenamento extern declara uma
referência a uma variável definida em outro lugar. A declaração extern
pode ser utilizada para fazer uma definição de outro arquivo de fonte,
visível, ou fazer uma variável antes da sua definição no mesmo arquivo
fonte. Uma vez declarada uma referência para a variável em nível
externo, esta ficará visível ao restante da unidade de tradução, na qual
ocorreu a declaração de referência.
Para uma referência extern ser válida, a variável a que se refere,
deve estar definida uma vez, e só uma vez, no nível externo. Essa
definição (sem a classe de armazenamento extern) pode estar em
quaisquer das unidades de tradução que compõem o programa
O exemplo a seguir mostra as declarações externas.
/******************************************************************
Arquivo Fonte UM
*******************************************************************/
#include<stdio.h>
extern int i; /* Referencia a i, definida mais abaixo */
void next( void ); /* Prototipo de funcao */
extern void other(void); /* Referencia de funcao em outro fonte */
void main(){
i++;
printf( "%d\n", i ); /* i igual a 4 */
next();
}
int i = 3; /* Definicao de i */
void next( void ){
i++;
printf( "%d\n", i ); /* i igual a 5 */
other();
}
Código 8-1
/******************************************************************
Arquivo Fonte DOIS
*******************************************************************/
#include<stdio.h>
extern int i; /* Referencia a i do */
152
/* primeiro arquivo fonte */
void other(void){
i++;
printf( "%d\n", i ); /* i igual a 6 */
}
Código 8-2
Os dois arquivos fonte deste exemplo contêm um total de três declarações
externas de i. Somente uma declaração é uma "declaração de definição". A
declaração:
int i = 3;
define a variável global i e a inicializa com valor inicial 3. A "declaração de
referência" de i no topo do primeiro arquivo fonte, usando extern, faz a
variável global visível antes da sua definição no arquivo. A declaração de
referência de i no segundo arquivo fonte também a faz visível naquele
arquivo fonte. Se numa instância a definição de uma variável não for
fornecida na unidade de tradução, o compilador assume o tipo,
extern int x;
como declaração de referência e ainda a define como,
int x = 0;
sendo visível em outra unidade de tradução do programa.
Todas as três funções, main, next e other, executam a mesma tarefa:
incrementam i e a enviam para o dispositivo de saída. Os valores 4, 5, e 6
serão enviados.
Caso a variável i não tivesse sido inicializada explicitamente, teria sido fixada
automaticamente com valor 0. Neste caso, os valores 1, 2, e 3 seriam
enviados.
8.2.2. Especificadores de Classe de Armazenamento para Nível Interno
No nível interno, pode ser usado qualquer um dos quatro especificadores de
classe de armazenamento para a declaração de variáveis. Quando é omitido o
especificador da classe de armazenamento de uma declaração, o especificador
default é auto. Por isso, a keyword auto raramente é vista em um programa
em C.
O Especificador de Classe de Armazenamento auto
O especificador de classe de armazenamento auto declara uma variável
automática, i.e. uma variável com tempo de vida local. Uma variável auto
somente será visível no bloco na qual é declarada. As declarações de variáveis
auto podem incluir inicializadores, como será visto seção 8.7 Inicialização.
Uma vez que as variáveis com classe de armazenamento auto não são
inicializadas automaticamente, pode-se inicializá-la explicitamente na própria
declaração, ou designar valores iniciais em instruções dentro do bloco. Os
valores de variáveis auto não inicializadas serão indefinidos. (Uma variável
153
local auto ou register é inicializada cada vez que se encontra no escopo em
que o inicializador é definido).
Uma variável interna static (com escopo local ou de bloco) pode ser
inicializada com o endereço de qualquer item externo ou static, mas não com
o endereço de um item auto, já que o endereço desse tipo não é constante.
O Especificador de Classe de Armazenamento register
As variáveis (assim como o programa) são armazenadas na memória. O
modificador register indica ao compilador que a variável em questão deve
ser, se possível armazenada num registrador interno da CPU.
As variáveis nos registradores da CPU são acessadas em um tempo muito
menor, pois o acesso aos registradores é mais rápido que o acesso à memória.
Uma consideração importante é que o especificador não pode ser usado em
variáveis globais, já que isso implicaria que um registrador da CPU ficaria o
tempotodo reservado para uma variável. Os tipos de dados, onde é mais
apropriado o uso do especificador register, são os tipos char e int, mas
pode-se usá-lo em qualquer tipo de dado. Observar o exemplo a seguir:
void main (void){
register int count;
for (count = 0;count<10;count++){
...
}
}
Código 8-3
Nesse exemplo, o loop do for será executado mais rapidamente do que seria
se não usássemos o especificador register. Esse é o uso mais recomendável
para o register, no caso de uma variável que será usada muitas vezes de
forma seguida.
A Classe de Armazenamento static
Uma variável declarada em nível interno, com o especificador de classe de
armazenamento static, tem um tempo de vida global, mas somente será visível
dentro do bloco no qual é declarada. Para strings constantes, o uso de static é
adequado porque alivia a sobrecarga das frequentes inicializações em funções
que são frequentemente chamadas.
Se uma variável static não for inicializada explicitamente, será inicializada
para 0 por default. Dentro de uma função, a classe static ocasiona a alocação
de memória e serve como definição. As variáveis internas static, proveem
armazenamento privado permanente e visibilidade para uma única função.
O funcionamento das variáveis declaradas como static depende se estas
foram declaradas como globais ou como locais.
As variáveis globais static funcionam como variáveis globais dentro de um
módulo, ou seja, são variáveis globais que não serão conhecidas em outros
154
módulos. Esse modificador é útil para isolar trechos de um programa,
evitando mudanças acidentais em variáveis globais.
As variáveis locais static conservam o seu valor entre chamadas da mesma
função. Observar o exemplo a seguir:
int count (void){
static int num = 0;
num++;
return num;
}
A função count() retorna o número de vezes que ela foi chamada. Pode-se
observar que a variável local num é inicializada. Essa inicialização só é
efetuada na primeira vez que a função é chamada, pois num deverá manter o
seu valor de uma chamada para a outra. O que a função executa é o
incremento de num a cada chamada, retornando o seu valor atual.
A Classe de Armazenamento extern
Uma variável declarada com o especificador de classe de armazenamento
extern, é uma referência para uma variável com o mesmo nome definido em
nível externo, em qualquer arquivo fonte do programa. A declaração interna
extern é usada para fazer a definição da variável de nível externo, visível
dentro do bloco. A menos que a variável seja declarada a nível externo, a
variável declarada com a keyword extern somente será visível no bloco no
qual está declarada.
Este exemplo ilustra as declarações de nível interno e externo:
#include <stdio.h> /* biblioteca padrão de funções de entrada e saída de dados */
int i = 1; /* declaração e definição da variável i */
void other( void ); /* declaração de função a ser definida posteriormente */
void main(){
extern int i; /* Referencia à variável i, definida acima: */
static int a; /* O valor inicial é zero; a é visível somente dentro da main: */
register int b = 0; /* b é armazenada em um registrador, se possível: */
int c = 0; /* A classe de armazenamento default é auto: */
printf( "%d\n%d\n%d\n%d\n", i, a, b, c );
/* Os valores impressos serão 1, 0, 0, 0: */
other();
return;
}
void other( void ){
static int *external_i = &i; /* O endereço da variável global i é designado a uma
variável ponteiro: */
155
int i = 16; /* i é redefinida; a variável i global não é mais visível: */
static int a = 2; /* Esta variável a é visível somente dentro desta função: */
a += 2;
printf( "%d\n%d\n%d\n", i, a, *external_i ); /* O valore impresso serão 16, 4, e
1: */
}
Código 8-4
Nesse exemplo, a variável i é definida em nível externo com valor inicial 1.
Uma declaração extern na função main é usada para declarar a referência à
variável de nível externo i. A variável static é inicializada para 0 por default,
uma vez que o inicializador foi omitido. A chamada de printf imprime os
valores 1, 0, 0, e 0.
Na função other, o endereço da variável global i é usado para inicializar o
ponteiro static para a variável externa i. Isso funciona porque a variável
global tem tempo de vida static, o que significa que seu endereço não muda
durante a execução do programa. Logo após, a variável i é redefinida como
uma variável local, com valor inicial igual a 16. Essa redefinição não afeta o
valor da variável de nível externo i, que fica escondida para o uso de seu
nome pela variável local. O valor da i global fica acessível somente de forma
indireta dentro desse bloco, através do ponteiro externo external_i. Caso se
tente designar um endereço para a variável auto i para um ponteiro, isso não
funcionará, uma vez que este endereço pode ser diferente, cada vez que o
programa inicia a execução do bloco. A variável a está declarada como uma
variável static e inicializado com valor igual a 2. Esta variável a não conflita
com a variável a da função main, uma vez que variáveis static de nível
interno são visíveis somente dentro do bloco no qual são declaradas.
A variável a é incrementada de 2, dando o valor 4 como resultado. Se a
função other fosse chamada novamente no mesmo programa, o valor inicial
de a seria 4. As variáveis internas static mantêm os seus valores quando o
programa sai e novamente entra no bloco no qual estão declaradas.
O especificador extern define variáveis que serão usadas em um arquivo
fonte, apesar de terem sido declaradas em outro. Programas grandes podem
ser divididos em vários arquivos (módulos) que serão compilados
separadamente. Diga-se por exemplo, que para um programa grande existam
duas variáveis globais: um inteiro count e um float sum. Estas variáveis são
declaradas normalmente em um dos módulos do programa. Por exemplo:
/*Módulo 1*/
int count;
float sum;
main (void){
...
return 0;
156
}
Código 8-5
Num outro módulo do mesmo programa, há uma rotina que deve usar as
variáveis globais do módulo acima. Por exemplo, a rotina RetornaCount()
retorna o valor atual de count. Para o segundo módulo poder acessar as
variáveis do primeiro, estas deverão ser declaradas com o modificador de
armazenamento extern.
/*Módulo 2*/
extern int count; /* variável do módulo 1 */
extern float sum; /* variável do módulo 1 */
int RetornaCount (void){
return count;
}
Código 8-6
Dessa forma, o compilador saberá que count e sum estão sendo usados neste
módulo, mas foram declarados em outro.
157
8.3. Especificadores de Tipo
Os especificadores de tipo nas declarações definem o tipo da variável ou
declaração de função. Os especificadores de tipo são listados a seguir.
void
char
short
int
long
float
double
signed
unsigned
especificador-de-struct
especificador-de-union
especificador-de-enumnome-de-typedef
Os tipos char, signed int, signed short int e signed long int, junto com as suas
contrapartidas unsigned, são chamados "tipos inteiros". Os especificadores de
tipo float, double e long double são referenciados como "flutuantes" ou de
"ponto flutuante". Pode-se utilizar qualquer especificador inteiro ou de ponto
flutuante em uma variável, ou na declaração de uma função. Se o
especificador de tipo não for definido na sua declaração, o tipo default é o int.
As keywords signed e unsigned são opcionais e podem preceder qualquer um
dos tipos inteiros, exceto enum, e podem também ser usados como único tipo
de especificador e neste caso fica subentendido como um unsigned int e
signed int respectivamente. Quando usado só o especificador int, é assumido
sendo signed. Quando usados os especificadores long e short somente, o
compilador entenderá como sendo long int e short int.
Os tipos enum são considerados tipos básicos. Os especificadores para o tipo
enum serão discutidos no capítulo 16.
A keyword void tem três usos: especificar um tipo de retorno de função,
especificar uma lista de tipos de argumentos para uma função que não possui
argumentos, e especificar um ponteiro para um tipo de dado não especificado.
Pode-se usar o tipo void para declarar funções que não retornam nenhum
valor, ou para declarar um ponteiro para um tipo não especificado. .
Podem ser criados especificadores de tipo adicionais, utilizando a declaração
typedef, como será visto mais adiante.
8.3.1. Especificadores de Tipos de Dados e seus Equivalentes
158
Na tabela a seguir são listados os especificadores de tipos de dados e seus
equivalentes.
Especificador de
Tipo
Equivalente(s)
signed char char
signed int signed, int
signed short int short, signed short
signed long int long, signed long
unsigned char —
unsigned int unsigned
unsigned short int unsigned short
unsigned long int unsigned long
float —
double —
long double Double
Tabela 8-1 – Especificadores de Tipo
Alguns compiladores para microcontroladores, tais como o PCW da CCS para
microcontroladores PIC, não seguem esse padrão. Por exemplo, no
compilador citado acima, o tipo default é unsigned, ou seja, se for declarada
uma variável ou função sem colocar explicitamente o tipo signed, o default
será unsigned, ao contrário dos compiladores para PC.
Os tipos double e long double também podem não existir em compiladores
para microcontroladores, ou simplesmente são tratados como sendo do tipo
float.
159
8.4. Qualificadores de Tipo
Os qualificadores atribuem uma de duas propriedades para um identificador.
O qualificador de tipo const declara um objeto que não pode ser modificado.
O qualificador de tipo volatile declara um item cujo valor pode ser mudado
legitimamente, por qualquer elemento além do controle do programa no qual
ele aparece, tal como uma tarefa concorrente de execução.
Os dois qualificadores de tipo, const e volatile, só podem aparecer uma vez
em uma declaração. Os qualificadores de tipo podem aparecer juntamente
com qualquer outro especificador de tipo; porém, eles não podem aparecer
depois da primeira vírgula em uma declaração múltipla de variáveis. Por
exemplo, as seguintes declarações estão corretas:
typedef volatile int VI;
const int ci;
As seguintes declarações são incorretas:
typedef int *i, volatile *vi;
float f, const cf;
Os qualificadores de tipo são os seguintes:
const
volatile
As declarações seguintes são válidas:
int const *p_ci; /* Ponteiro para uma constante int */
int const (*p_ci); /* Ponteiro para uma constante int */
int *const cp_i; /* Ponteiro constante para um int */
int (*const cp_i); /* Ponteiro constante para um int */
int volatile vint; /* Inteiro volátil */
Se a especificação de um tipo array inclui qualificadores de tipo, o elemento
será qualificado, não o tipo de array. Se a especificação do tipo de função
incluir qualificadores, o comportamento é indefinido. Nem volatile, nem
const afetarão a faixa valores ou propriedades aritméticas do objeto. A lista a
seguir descreve como usar const e volatile.
A keyword const poderá ser usada para modificar qualquer tipo
fundamental ou agregado, ou um ponteiro para um objeto de qualquer
tipo, ou um typedef. Se uma variável for declarada com qualificador
const, seu tipo será considerado como sendo const int. Uma variável
const pode ser inicializada ou pode ser colocada numa região de
memória de armazenamento, somente de leitura. A keyword const é útil
para declarar ponteiros para const, já que, isso evitará que a função
consiga mudar o ponteiro.
O compilador assume que, para qualquer ponto no programa, uma
variável volátil pode ser acessada por um processo desconhecido, que
usa ou modifica seu valor.
160
Se a keyword volátil for usada sem o especificador de tipo, será
assumido o tipo int. O especificador volatile pode ser usado para prover
acesso adequado a posições especiais de memória. Deve-se utilizar
volatile em objetos de dados que podem ser acessados ou alterados por
manipuladores (handlers), programas concorrentes, ou por um hardware
especial tal como registradores de controle de I/O mapeados em
memória. Uma variável pode ser declarada como volatile para o seu
tempo de vida, ou pode ser convertida (cast) uma única referência para
ser volatile.
Uma variável pode ser const e volátil, neste caso, a variável não
poderá ser modificada legitimamente por seu próprio programa, mas
poderá ser modificada por algum outro processo assíncrono.
Observar o seguinte exemplo:
const float PI = 3.1415926536;
Pode-se observar no exemplo que as variáveis com o modificador const
podem ser inicializadas, mas não alteradas. Neste caso, a variável PI não
poderia ser alterada em qualquer outra parte do programa. Se o programador
tentar modificar PI, o compilador gerará um erro de compilação.
A utilidade mais importante de const, não é a de declarar variáveis constantes
no programa. Seu uso mais comum é evitar que um parâmetro de função seja
alterado pela mesma. Isso é muito útil no caso de um ponteiro, pois o
conteúdo de um ponteiro pode ser alterado por uma função. Para tanto, basta
declarar o parâmetro como const. Observar o seguinte exemplo:
#include <stdio.h>
void sqr (const int *num);
main (void){
int a = 10;
int b;
b = sqr(&a);
}
int sqr (const int *num){
return ((*num)*(*num));
}
Código 8-7
Nesse exemplo, num está protegida contra alterações. Isso quer dizer que,
caso se tente fazer
*num=10;
dentro da função sqr(), o compilador geraria uma mensagem de erro.
O modificador volatile indica ao compilador que a variável em questão pode
ser alterada por outros aplicativos, sem que o programa seja avisado. Por
exemplo, no caso de uma variável que o BIOS do computador altera de
minuto em minuto (um relógio). Seria apropriado que esta variável seja
declarada com o especificador de acesso volatile.
161
162
8.5. Declarações de Variáveis
O resto deste capítulo descreve a forma e significado das declarações para
tipos variáveis resumidos na Tabela 8-2. Em particular, as seções seguintes
explicam como declarar os itens que seguem:
Tipo de
Variável
Descrição
Variáveis
simples
Variáveis de valor único de tipo inteiro ou
de ponto flutuante
Arrays Variáveis compostas de coleção de
elementos do mesmo tipo
Ponteiros Variáveis que apontam para outras
variáveis e armazenam a localização das
variáveis(na forma de endereços) em vez
dos valores destas.
Variáveis de
enumeração
(enum)
Variáveis simples do tipo inteiro que
armazenam um valor de um grupo de
constantes de inteiro em sequência
Estruturas
(structs)
Variáveis compostas de uma coleção de
valores que podem ter tipos diferentes
Unions Variáveis compostas de vários valores de
tipos diferentes que ocupam o mesmo
espaço de memória de armazenamento
Tabela 8-2 – Tipos de variáveis
O declarador é a parte de uma declaração que especifica o nome que será
introduzido no programa.
Os declaradores podem ser usados para declarar arrays de valores, ponteiros
para valores, e funções que devolvem valores de um tipo especificado. .
Quando um declarador consiste em um identificador que não pode ser
modificado, o item sendo declarado terá um tipo de base. Se um asterisco (*)
aparece à esquerda de um identificador, o tipo é modificado para ser um tipo
de ponteiro. Se o identificador é seguido de colchetes ([ ]), o tipo é
modificado para um tipo array. Se o identificador é seguido de parênteses, o
tipo é modificado para um tipo função.
Cada declarador declara pelo menos um identificador. Um declarador deve
incluir um especificador de tipo para formar uma declaração completa. O tipo
de especificador dá o tipo de elementos para um array, o tipo de objeto
endereçado por um ponteiro, ou o tipo de retorno de uma função.
Os exemplos seguintes ilustram algumas formas simples de declaradores:
int list[20]; / * Declara um array de 20 valores do tipo int chamado list * /
char *cp; /* Declara um ponteiro para um valor do tipo char */
double func(void); /* Declara uma função chamada func, sem argumentos,
que retorna um valor do tipo double */
int *aptr[10] /* Declara um array de 10 ponteiros */
163
8.5.1. Declaração de Variáveis Simples
A declaração de uma variável simples especifica o nome da variável e o tipo.
Isso também especifica a classe de armazenamento da variável e o tipo de
dado.
As classes de armazenamento ou o tipo (ou ambos) são requeridos nas
declarações de variáveis. Se não for declarado o tipo de variável, o tipo
default é o int.
Pode ser utilizada uma lista de identificadores separados por vírgulas (,) para
especificar várias variáveis na mesma declaração. Todas as variáveis
definidas nesta declaração devem ser do mesmo tipo. Por exemplo:
int x, y; /* Declara duas variáveis simples do tipo int */
int const z = 1; /* Declara um valor constante do tipo int com nome de z */
As variáveis x e y podem armazenar qualquer valor no grupo definido para o
tipo int. A variável simples z é inicializada com o valor 1 e não pode ser
modificada pelo programa por causa da classe de armazenamento const.
Se a declaração de z for para uma variável estática não inicializada ou com
escopo de arquivo, receberá o valor inicial de 0, e depois não poderá ser
modificada.
unsigned long reply, flag; /* Declara duas variáveis chamadas reply e flag */
Nesse exemplo, ambas variáveis, reply e flag, são do tipo unsigned long e
armazenam valores inteiros sem sinal.
8.5.2. Declarações de Enumeração[57]
Uma enumeração consiste em um grupo de constantes inteiras nomeadas.
Uma declaração de tipo enumeração dá o nome do (opcional) label de
enumeração e define o grupo de identificadores inteiros nomeados (chamado
de "grupo de enumeração", "constantes de enumeração", "enumeradores", ou
"membros").
Uma variável do tipo enumeração armazena um dos valores do grupo de
enumeração, definido por aquele tipo. As variáveis do tipo enum podem ser
usadas em expressões indexadas e como operandos de qualquer operador
aritmético ou relacional.
As enumerações proporcionam uma alternativa para a diretiva de pré-
processador #define, com a vantagem de que os valores podem ser gerados
pelo programador, obedecendo às regras de normais de escopo.
No padrão ANSI C, as expressões que definem o valor de uma constante
enum sempre serão do tipo int; assim, o espaço de armazenamento associado
com uma variável de enumeração é o espaço requerido para um único valor
de int. Uma constante de enumeração ou um valor de um tipo enumerado
pode ser usado em qualquer lugar da linguagem C que permita uma expressão
164
inteira.
O identificador (que é opcional) nomeia o tipo de enumeração definido pela
lista de enumeração. Este identificador é chamado frequentemente de "tag" da
enumeração especificada pela lista. Um especificador da forma
enum identificador { lista-de-enumeração }
declara o identificador como sendo o tag[58] da enumeração, definida pela
lista de enumeração. A lista de enumeração define o "conteúdo enumerador".
A lista de enumeração é descrita em detalhes a seguir.
Se a declaração de um tag é visível, as declarações subsequentes que usem o
tag, mas omitam a lista de enumeração, especificarão o tipo enum
previamente declarado. O tag tem que se referir ao tipo de enumeração
definido, o mesmo deve estar no escopo. Considerando que o tipo de
enumeração possa ser definido em outro lugar, a lista de enumeração não
aparece nesta declaração. As declarações de tipos derivadas de enumerações,
e declarações typedef para tipos de enumeração podem usar os tags de
enumeração, antes que o tipo de enumeração seja definido.
Cada constante de enumeração, de uma lista de enumeração, nomeia um valor
do conjunto de valores. Por default, à primeira constante de enumeração é
associado o valor 0. A próxima constante de enumeração da lista será
associada como valores consecutivos, a menos que seja explicitamente
associada com outro valor. O nome de uma constante de enumeração é
equivalente a seu valor.
Pode ser usado constantes-de-enumeração = expressão-constante para
sobrescrever a sequência padrão de valores. Assim, se a constante-de-
enumeração = expressão-constante aparecer na lista de enumeradores, a
constante-de-enumeração será associada com o valor dado pela expressão-
constante. A expressão-constante deve ser do tipo int e não pode ser um
número negativo.
As regras seguintes se aplicam aos membros de um grupo de enumeração:
Um conjunto de enumerações pode conter valores constantes
duplicados. Por exemplo, se poderia associar o valor 0 com dois
identificadores diferentes, talvez nomeados nulo e zero, no mesmo
conjunto.
Os identificadores na lista de enumeração devem ser distintos de
outros identificadores no mesmo escopo e com a mesma visibilidade,
inclusive nomes de variáveis ordinárias e identificadores de outras listas
de enumeração.
Os tags de enumeração obedecem as regras de escopo normais. Eles
devem ser diferentes de qualquer outra enumeração, tag de estrutura ou
de união que tenham a mesma visibilidade.
165
Estes exemplos ilustram declarações de enumeração:
enum DIA{ /* Define um tipo de enumeração */
Sábado, /* chamado DIA e declara uma */
Domingo = 0, /* variável chamada dias_uteis com */
Segunda_feira, /* este tipo */
Terça_feira,
Quarta_feira, /* Quarta_feira é associada ao valor 3 */
Quinta_feira,
Sexta_feira
} dias_uteis;
O valor 0 é associado com Sabado por default. O identificador Domingo é
fixado explicitamente em 0. Para os identificadores restantes são
determinados os valores de 1 até 5 por default.
Neste exemplo, um valor do grupo DIA é designado para a variável hoje.
enum DIA hoje = Quarta_feira;
Notar que,o nome da constante de enumeração é utilizado para designar o
valor. Considerando que o tipo de enumeração DIA foi previamente
declarado, somente será necessário o label DIA para a utilização.
Para designar um valor inteiro explícito para uma variável de um tipo de dado
enumerado, pode ser usado o seguinte cast: [59]
dia_util = ( enum DIA ) ( valor_do_dia - 1 );
Este cast é recomendado, mas não é necessário.
enum BOOLEAN{ /* Declara tipo de dado de enumeração chamado BOOLEAN */
false, /* false = 0, true = 1 */
true
};
enum BOOLEAN end_flag, match_flag; /* Duas variáveis do tipo BOOLEAN */
Esta declaração também pode ser especificada como
enum BOOLEAN { false, true } end_flag, match_flag;
ou
enum BOOLEAN { false, true } end_flag;
enum BOOLEAN match_flag;
Um exemplo que usa estas variáveis pode ser visto a seguir.
if ( match_flag == false ) {
.
. /* instruções */
.
}
end_flag = true;
Também podem ser declarados tipos de dados enum sem nome. O nome do
tipo de dado é omitido, mas as variáveis devem ser declaradas. A variável
resposta é uma variável do tipo definido:
enum { sim, nao } resposta;
8.5.3. Declarações de Estruturas[60]
166
Uma declaração de estrutura nomeia um tipo e especifica uma sucessão de
valores variáveis (chamados de membros, atributos ou campos da estrutura),
que podem ser de tipos diferentes. Um identificador opcional, também
chamado tag, dá o nome ao tipo de estrutura e pode ser usado em referências
subsequentes para o tipo. Uma variável daquele tipo de estrutura armazenará
uma sequência inteira definida pelo próprio tipo. As estruturas em C são
semelhantes aos tipos conhecidos como "registros", em outras linguagens de
programação e em bancos de dados.
A declaração de um tipo de estrutura não aloca espaço de memória para a
estrutura. Esta define somente um modelo para declarações posteriores de
variáveis deste tipo de estrutura.
Pode ser utilizado um identificador previamente definido (tag) para fazer
referência ao tipo de estrutura definida. Nesse caso, a declaração da lista da
estrutura não pode ser repetida enquanto a sua definição seja visível. As
declarações de ponteiros para estruturas e os typedefs para tipos de estrutura
podem ser usados antes da definição do tipo de estrutura. Porém, a definição
de estrutura deve ser encontrada antes de qualquer uso. Essa é uma definição
incompleta do tipo e do tag. Para que a definição seja completa, a definição
do tipo deverá aparecer mais tarde no mesmo escopo.
Cada variável declarada na lista é definida como uma membro do tipo de
estrutura. As declarações de variáveis dentro da lista têm a mesma forma que
as outras declarações de variáveis discutidas neste capítulo, exceto que, as
declarações não podem conter especificadores de classe de armazenamento,
nem inicializadores. Os membros da estrutura podem ser qualquer tipo
variável exceto o tipo void, um tipo incompleto, ou um tipo função.
Um membro não pode ser declarado para ser do tipo da estrutura na qual ele
aparece. Porém, um membro pode ser declarado como um ponteiro ao tipo de
estrutura no qual aparece, contanto que o tipo de estrutura tenha um tag. Isso
permite criar listas encadeadas de estruturas.
As estruturas seguem o mesmo escopo que os outros identificadores. Os
identificadores da estrutura devem ser diferentes de outros tags de structs,
unions, e enums com a mesma visibilidade.
Cada declaração struct em uma lista deve ser única dentro dela. Porém, os
nomes dos identificadores numa declaração de struct não precisam ser
diferentes dos nomes de variáveis comuns ou de identificadores de outras
listas de declaração de estrutura.
Estruturas aninhadas podem ser acessadas como se elas fossem declaradas em
nível de escopo de arquivo. Por exemplo, colocando esta declaração:
struct a{
int x;
struct b {
167
int y;
} var2;
} var1;
estas declarações serão válidas:
struct a var3;
struct b var4;
O exemplo a seguir ilustra uma declaração de struct:
struct CanalSerial{ /* Define uma variável estrutura chamada com1 */
char nome[20];
long baudrate;
int bits;
} com1;
A estrutura CanalSerial possui três membros: nome, baudrate e bits. O nome
é um membro do tipo array com vinte elementos char; baudrate e bits são
variáveis simples do tipo long int e int, respectivamente. O identificador
CanalSerial é o identificador da estrutura.
struct CanalSerial com2,com3,com4;
O exemplo acima define três variáveis struct: com2, com3 e com4. Cada
estrutura tem a mesma lista de três membros. Os membros são declarados
para ter o tipo de struct CanalSerial definido no exemplo anterior.
struct { /* Define uma struct anônima e uma */
/* variável struct chamada complex */
float x, y;
} complex;
A estrutura complex possui dois membros com tipo float, x e y. O tipo de
estrutura não possui um tag e portanto, é anônima.
struct sample { /* Define uma variável de estrutura chamada x */
char c;
float *pf;
struct sample *next;
} x;
Os primeiros dois membros da estrutura são uma variável char e um ponteiro
para um valor float. O terceiro membro é declarado como sendo um ponteiro
para uma estrutura do mesmo tipo no qual está sendo definido (sample). Isso
pode ser interessante, já que permitiria que uma variável do tipo sample,
armazene o seu próprio endereço de armazenamento, por exemplo. Isso é
extremamente útil, quando se trabalha com listas encadeadas ou na criação
dinâmica de objetos, que é a base da programação orientada a objetos.
As estruturas anônimas são usuais quando não é necessária a utilização de um
tag. Isto acontece quando uma única declaração define todas as instâncias da
estrutura. Por exemplo:
struct{
int x;
int y;
} mystruct;
168
Estruturas integradas são frequentemente anônimas.
struct somestruct {
struct{ /* estrutura anónima */
int x, y;
} point;
int type;
} w;
Campos de Bits
Além dos declaradores para os membros de uma estrutura ou union, um
declarador de estrutura pode também especificar um número de bits, chamado
de "campos de bits". O seu comprimento é definido do declarador para o
nome do campo seguido do símbolo de dois pontos (:). O campo de bits será
um tipo inteiro.
A expressão constante especifica a largura do campo em bits. O especificador
de tipo do declarador deve ser unsigned int, signed int, ou int, e a expressão
constante deve ser um valor positivo. Se o valor é zero, a declaração não
possuirá declarador. Não é permitido na maioria dos compiladores arrays de
campos de bits, ponteiros para campos de bits e funções que retornem campos
de bits. O declarador opcional nomeia o campo de bits. Os campos de bits só
podem ser declarados como parte de uma estrutura. O operador endereço (&)
não pode ser aplicado a componentes de campos de bit.
Campos de bits sem nome não poderão ser referenciados, e o seu conteúdo
em tempo de execução é impossível de predizer. Os campos de bits podem ser
usados como "campos reservados" para propósitos de alinhamento. Um
campo de bits anônimo, cuja largura é especificada como sendo 0, garante que
o espaço de armazenamento para o membro seguinte da lista começará no
início de um int.
Os campos de bits serão tão longos quanto for suficiente para conter o
conjunto de bits.
Por exemplo,estas duas declarações são incorretas:
short a:17; /* incorreto! */
int long y:33; /* incorreto! */
O exemplo a seguir define um array de duas dimensões chamado screen.
struct{
unsigned short icon : 8;
unsigned short color : 4;
unsigned short underline : 1;
unsigned short blink : 1;
} screen[25][80];
O array contém 2000 elementos. Cada elemento é uma estrutura individual
contendo quatro membros de campos de bits: icon, color, underline e blink.
O tamanho de cada estrutura é de dois bytes.
Os campos de bits têm a mesma semântica que os tipos integrais. Isso
169
permite que um campo de bit possa ser usado em expressões da mesma forma
que uma variável do mesmo tipo, com a diferença de quantos bits estarão no
campo de bits.
Em geral, os campos de bits são definidos como inteiros e tratados como
unsigned, mas podem mudar de compilador para compilador. Alguns
compiladores permitem o uso dos tipos char e long (signed ou unsigned) para
os campos de bits.
Na maioria dos compiladores, os campos de bits são alocados começando
como bit menos significativo até o bit mais significativo, como mostra o
seguinte código:
struct mybitfields{
unsigned short a : 4;
unsigned short b : 5;
unsigned short c : 7;
} test;
void main( void ){
test.a = 2;
test.b = 31;
test.c = 0;
}
Código 8-8
Os bits serão arranjados da seguinte maneira:
00000001 11110010
cccccccb bbbbaaaa
Na família de processadores 80x86, o armazenamento de um valor inteiro é
feito de forma que o byte menos significativo é armazenado antes do mais
significativo, ou seja, o inteiro 0x01F2 acima será armazenado na memória
física como 0xF2 seguido de 0x01.
Os campos de bits são muito úteis quando se trabalha com sistemas onde
várias informações diferentes são codificadas num único byte, como por
exemplo, em protocolos de comunicação (CAN, Profibus e outros), em
palavras de status, etc..
Em geral, os membros da estrutura são armazenados sequencialmente na
ordem em que eles são declarados: o primeiro membro tem um endereço de
memória menor, e o último membro o maior.
8.5.4. Declarações de Unions[61]
Uma declaração de union especifica um conjunto de valores e,
opcionalmente, um tag que nomeia a mesma. Os valores variáveis são
chamados de membros da union e podem ter diferentes tipos.
Uma variável do tipo union armazena um dos valores definidos pelo seu
170
tipo. As mesmas regras que regem as estruturas, são aplicadas às declarações
das unions. As unions também podem ter campos de bits.
Os membros das unions não podem ser do tipo incompleto, void e nem tipo
função. Assim os membros não podem ser uma instância da própria union,
mas podem ser ponteiros para o tipo union no qual são declarados.
A declaração do tipo union é somente um modelo. Não será reservada
memória até que uma variável seja declarada.
Os membros de uma variável union, compartilham o mesmo espaço de
memória. Se for declarada uma union com dois tipos, e um valor for
armazenado, mas a union for acessada pelo outro tipo, o resultado
armazenado será incorreto. Por exemplo, considerar a declaração da union de
um float e um int. Um valor float é armazenado, mas o programa depois
acessa este valor como um int. Neste tipo de situações, o valor dependerá da
forma de armazenamento interno do valor float. O valor inteiro não será o
correto. Ver o exemplo a seguir.
union uni{
int i;
float f;
};
void main(void){
union uni x;
x.f = 6000.0;
printf("Valor de uni.f = %d",x.i);
}
Código 8-9
Usando o compilador BC3.1 (16 bits) o valor na tela será –32768. Veja a
seguir mais um exemplo.
union sign { /* Uma definição e uma declaração*/
int svar;
unsigned uvar;
} number;
void main (void){
number.svar = -66;
printf("\n O valor de number.svar = %d",number.svar);
printf("\n O valor de number.uvar = %u",number.uvar);
}
Código 8-10
Nesse exemplo é definida a variável union com o tipo sign e declarada uma
variável chamada number que possui dois membros: svar, um signed int, e
uvar, que é um unsigned int. Esta declaração permite que o valor corrente
do número seja armazenado como signed ou como unsigned. O tag
associado a este tipo de union é sign.
union { /* Define um array de duas dimensões chamado screen */
struct {
171
unsigned int icon : 8;
unsigned color : 4;
} window1;
int screenval;
} screen[25][80];
O array screen contém 2000 elementos. Cada elemento do array é uma union
individual de dois membros: window1 e screenval. O membro window1 é
uma estrutura com dois membros com campos de bits, icon e color. O
membro screenval é um int. Em qualquer momento, cada elemento da union
armazena um inteiro representado por screenval ou uma estrutura
representada por window1.
Armazenamento das Unions
O espaço de armazenamento associado com uma variável union é o espaço
requerido para o maior membro da mesma. Quando o menor dos membros
for armazenado, a variável union poderá conter espaço de memória não
utilizado. Todos os membros são armazenados no mesmo espaço de memória
e começam no mesmo endereço. O valor armazenado é sobrescrito cada vez
que um valor é designado para membros diferentes. Por exemplo:
union { /* Define uma union chamada x */
char *a, b;
float f[20];
} x;
Os membros da union x são, na ordem da sua declaração, um ponteiro para
um valor char, um valor char, e um array para vinte valores do tipo float. O
espaço alocado para x é o espaço requerido para o array de 20 elementos f, já
que f é o maior membro da union. Nesse exemplo não foi associado nenhum
tag para a union, e portanto, será anônima.
8.5.5. Declarações de Arrays[62]
Uma declaração de array identifica o array e especifica o tipo dos seus
elementos. Também pode definir o número de elementos componentes do
mesmo. Uma variável do tipo array é considerada como um ponteiro para o
tipo dos elementos do array.
A sintaxe tem duas formas:
A primeira forma define uma variável array. O argumento da
expressão constante, se presente, deve ser do tipo inteira, maior que
zero. Cada elemento tem o tipo dado pelo especificador de tipo, que
pode ser de qualquer tipo, exceto void. Um elemento de um array não
pode ser do tipo função. A sintaxe básica pode ser:
especificador-de-tipo declarador [expressão-constante]
A segunda forma declara uma variável que tem sido definida em
outro lugar do código. Esta forma omite o argumento da expressão
172
constante entre colchetes, mas não os colchetes. Essa forma pode ser
usada somente se o array foi previamente inicializado, declarado como
parâmetro ou declarado com referência para um array explicitamente
definido em outro lugar do programa.
especificador-de-tipo declarador [ ]
Em ambas as formas, os declaradores diretos nomeiam a variável, podendo
modificar o seu tipo. Os colchetes que seguem o declarador direto modificam
o declarador para um tipo array.
Os qualificadores de tipo podem aparecer na declaração de um objeto do tipo
array, mas os qualificadores se aplicam aos elemento no lugar do array em si.
Pode-se declarar um array de arrays (array multidimensional) pela colocação
no declarador de array de uma lista de constantes encerradas em colchetes da
forma:
especificador-de-tipo declarador[expressão-constante] [expressão-
constante] ...
Cada expressão constante entre colchetes define o número de elementos em
uma dada dimensão: arrays de duas dimensões têm duas expressões entre
colchetes; arrays em três dimensões têm três; e assim sucessivamente. A
primeira expressão constante pode ser omitida se o array foi previamente
inicializado, declarado como um parâmetro, ou declara -do como uma
referência para um array explicitamente definido em alguma parte do
programa.
Podem ser definidos arrays de ponteiros para vários tipos de objetos pelo uso
de declarações mais complexas como descrito em outras seções.
Os arrays são armazenados em linhas. Por exemplo, o seguinte array consiste
de duas linhas com três colunas cada:
char A[2][3];
As três colunas da primeira linha são armazenadas inicialmente, seguidas
pelas três colunas da segunda linha. Isso permite que o último subscrito varie
de forma mais rápida.
Os exemplos a seguir ilustram algumas declarações de arrays.
float matrix[10][15];
Este array bidimensional nomeado matrix possui 150 elementos do tipo
float. O primeiro elemento do array seria matrix[0][0], e o ultimo seria
matrix[9][14].
struct {
float x, y;
} complex[100];
Esta é uma declaração de um array de estruturas. Este array possui 100
elementos (complex[0] a complex[99]); cada elemento é uma estrutura
173
contendo dois membros do tipo float.[63]
extern char *name[];
Essa instrução declara o tipo e o nome de um array de ponteiros para char. A
definição do array ocorre em outro lugar.
Armazenamento de Arrays
O espaço de armazenamento associado a um tipo array é o espaço requerido
para armazenar todos os seus elementos. Os elementos de uma array são
armazenados em posições de memória contígua e crescente, a partir do
primeiro elemento até o último.
8.5.6. Declarações de Ponteiros
Uma declaração de ponteiro define uma variável do tipo ponteiro, e especifica
o tipo de objeto para o qual a mesma aponta. Uma variável declarada como
ponteiro armazena um endereço de memória.
A sintaxe básica é a seguinte:
especificador-de-tipo * declarador;
O especificador de tipo indica o tipo de objeto que será apontado, que pode
ser um tipo de variável simples, estrutura ou union. As variáveis do tipo
ponteiro podem apontar para funções, arrays e outros ponteiros.
Fazendo o especificador de tipo void, pode-se deixar em aberto a
especificação do tipo para a qual o ponteiro se refere. Este tipo de
especificação é referido com o nome de “ponteiro para void” e é escrito como
void *. Uma variável declarada como ponteiro para void, pode ser usada para
apontar para qualquer tipo de objeto. Porém, a execução da maioria das
operações no ponteiro ou no objeto para o qual este aponta e o tipo apontado,
devem ser especificados explicitamente para cada operação (variáveis do tipo
char* e tipo void* são geralmente compatíveis sem necessidade de casts).
Tais conversões podem ser feitas pela utilização de casts, que serão vistos na
seção 9.4.
O qualificador de tipo pode ser const, volatile, ou ambos. Estes especificam,
respectivamente, que o ponteiro não pode ser modificado pelo próprio
programa (const), ou que o ponteiro pode ser modificado por algum processo
externo ao controle do programa (volatile).
O declarador nomeia a variável e pode incluir um modificador de tipo. Por
exemplo, se o declarador representa um array, o tipo do ponteiro é modificado
para ser um ponteiro a um array.
Podem ser declarados ponteiros para tipos struct, union e enum, antes desses
serem definidos. Pode-se declarar o ponteiro usando o tag da estrutura ou
union como mostrado nos exemplos a seguir. Tais declarações são permitidas
porque o compilador não precisa saber o tamanho da estrutura ou union para
174
alocar espaço para a variável ponteiro
Os seguintes exemplos ilustram algumas declarações para ponteiros.
char *message; /* Declara uma variável ponteiro chamda message */
O ponteiro message aponta para uma variável do tipo char.
int *pointers[10]; /* Declara um array de ponteiros*/
O array de ponteiros possui 10 elementos, cada elemento é um ponteiro para
uma variável do tipo int.
int (*pointer)[10]; /* Declara um ponteiro para um array de 10 elementos */
A variável pointer aponta para um array com 10 elementos. Cada elemento
do array é do tipo int.
int const *x; /* Declara a variável ponteiro x,
para um valor constante */
O ponteiro x pode ser modificado para apontar um valor int diferente, mas o
valor para o qual aponta não pode ser modificado.
const int some_object = 5 ;
int other_object = 37;
int *const y = &fixed_object;
const volatile *const z = &some_object;
int *const volatile w = &some_object;
A variável y é declarada como um ponteiro constante para um valor int. O
valor para o qual aponta pode ser modificado, mas o próprio ponteiro em si,
sempre tem que apontar para a mesma localização: o endereço de
fixed_object. Analogamente z é um ponteiro constante, mas também é
declarado para apontar a um int cujo valor não pode ser modificado pelo
programa. O especificador adicional volatile indica que, embora o valor do
const int apontado por z não pode ser modificado pelo programa, ele poderia
ser modificado por um processo concorrente com o programa. A declaração
de w especifica que o programa não pode mudar o valor apontado e que o
programa não pode modificar o ponteiro.
struct list *next, *previous; /* Usa o tag da estrutura chamada list */
Esse exemplo declara duas variáveis ponteiro, next e previous, que apontam
para uma tipo de estrutura chamado list. Esta declaração pode aparecer antes
da definição do tipo de estrutura, sempre que a definição do tipo tenha a
mesma visibilidade que a própria declaração. Observar o exemplo a seguir:
struct list {
char *token;
int count;
struct list *next;
} line;
A variável line é do tipo struct chamada list. A estrutura list possui três
membros: o primeiro membro é um ponteiro para valores do tipo char, o
segundo é um valor int, e o terceiro é um ponteiro para uma outra estrutura do
tipo list.
175
struct id {
unsigned int id_no;
struct name *pname;
} record;
No exemplo acima, a variável record é do tipo estrutura chamado id. Deve-
se notar que pname é declarada como um ponteiro para um outro tipo de
estrutura chamado name. Essa declaração pode aparecer antes que o tipo
name seja definido.
Armazenamento de Endereços
O espaço de memória necessário para o armazenamento de um endereço e o
significado do mesmo, depende da implementação do compilador. Não se
pode garantir que os ponteiros para tipos diferentes de dados tenham a mesma
largura em bits. Desta forma, sizeof(char*) não é necessariamente igual a
sizeof(int *), embora sejam iguais na maioria dos compiladores.
8.5.7. Declarações Abstratas
Um declarador abstrato é um declarador sem identificador, que consiste de
um ou mais ponteiros, arrays ou modificadores de função. O modificador
ponteiro (*) sempre precede o identificador em um declarador; os
modificadores array ([ ]) e função (( )) são colocados após o identificador.
Conhecendo isso, pode-se determinar se o identificador tem que aparecer em
um declarador abstrato de forma a interpretá-lo de forma correta.
Os declaradores abstratos podem ser complexos. Os parênteses em um
declarador abstrato complexo especificam uma interpretação particular, da
mesma forma que especificam como funcionam.
Osexemplos a seguir ilustram algumas declarações abstratas[64].
int * /* O nome do tipo para um ponteiro para o tipo int */
int *[3] /* Um array de três ponteiros para int */
int (*) [5] /* Um ponteiro para um array de cinco inteiros */
int *() /* Uma função sem especificação de parâmetros que
retorna um ponteiro para um int */
int (*) ( void ) /* Um ponteiro para uma função sem argumentos
e que retorna um int */
int (*const []) ( unsigned int, ... ) /* Um array de ponteiros constantes
de número não especificado , para funções cada uma das quais como um
parâmetro que tem o tipo unsigned int, e um número não especificado de
outros parâmetros, que retornam um int */
176
177
8.6. Interpretando Declaradores Complexos
Qualquer declarador pode ser incluído entre parênteses para especificar uma
interpretação particular de um "declarador complexo". Um declarador
complexo é um identificador qualificado por mais de um modificador de
array, ponteiro, ou função. Pode-se aplicar várias combinações de
modificadores de arrays, ponteiros, e funções para um único identificador.
Geralmente, pode ser usada a keyword typedef, para simplificar as
declarações. .
Na interpretação de declaradores complexos, os colchetes e parênteses (i.e.,
os modificadores à direita do identificador) tomam precedência sobre os
asteriscos (i.e., modificadores à esquerda do identificador). Os colchetes e
parênteses têm a mesma precedência e são associados da esquerda para a
direita. Depois que o declarador tenha sido interpretado completamente, o
tipo de especificador é aplicado como o último passo. Usando parênteses
pode-se sobrescrever a ordem de associação default e forçar uma
interpretação particular. Nunca devem ser usados parênteses ao redor do nome
do próprio identificador. Isso poderia ser mal interpretado como uma lista de
parâmetros.
Um modo simples para interpretar declaradores complexos é lendo-os de
dentro para fora, usando os quatro passos a seguir:
1. Começar com o identificador e procurar diretamente, para a direita
por colchetes ou parênteses (se houverem).
2. Interpretar estes colchetes ou parênteses, então procurar no lado
esquerdo por asteriscos.
3. Se for encontrado um parêntesis de fechamento ‘)’, voltar aos passos
1 e 2 para tudo o que for encontrado dentro dos parênteses.
4. Aplicar o especificador de tipo.
char *( *(*var)() )[10];
^ ^ ^ ^ ^ ^ ^
7 6 4 2 1 3 5
Nesse exemplo, os passos foram numerados na ordem e podem ser
interpretados como segue:
1. O identificador var é declarado como ....
2. um ponteiro para ....
3. uma função que retorna ...
4. um ponteiro para ...
5. um array de 10 elementos que são do tipo ...
6. ponteiros para valores do tipo ...
178
7. char.
Os seguintes exemplos ilustram outras declarações complexas e mostram
como os parênteses podem afetar o significado de uma declaração.
int *var[5]; /* Array de ponteiros para valores do tipo int */
O modificador de array ([ ]) tem uma prioridade maior que o modificador
ponteiro (*), de forma que var é declarada como sendo um array. O
modificador de ponteiro é aplicado ao tipo dos elementos do array; assim, os
elementos do array são ponteiros para valores int.
int (*var)[5]; /* Ponteiro para um array de valores do tipo int */
Nesta declaração para var, os parênteses dão ao modificador ponteiro uma
prioridade maior que o modificador array, e var é declarada como sendo um
ponteiro para um array de cinco valores int.
long *var( long, long ); /* Função que retorna um ponteiro para um valor long */
O modificador função também tem prioridade maior que o modificador
ponteiro, de forma que esta declaração para var declara que é uma função que
retorna um ponteiro para um valor long. A função é declarada como tendo
dois argumentos do tipo long.
long (*var)( long, long ); /* Ponteiro para uma função que retorna uma long */
Nesse exemplo, os parênteses dão ao modificador ponteiro maior prioridade
que para o modificador função, e var é declarada como sendo um ponteiro
para uma função que retorna um valor long. A função possui dois argumentos
do tipo long.
struct both{ /* Array de ponteiros para funções */
/* que retornam uma tipo de estrutura */
int a;
char b;
} ( *var[5] )( struct both, struct both );
Os elementos de um array não podem ser funções, mas essa declaração
demonstra como declarar um array de ponteiros para funções no lugar de um
array de funções. Nesse exemplo, a variável var é declarada como sendo um
array de cinco ponteiros para função que retorna uma estrutura de dois
membros. Os argumentos da função são declarados como sendo duas
estruturas com o mesmo tipo de estrutura. Pode-se notar que os parênteses ao
redor de *var[5] são requeridos, já que sem eles, a declaração não será
correta, tentando declarar um array de funções como mostrado a seguir.
/* Declaração incorreta */
struct both *var[5]( struct both, struct both );
A seguinte instrução declara um array de ponteiros.
unsigned int *(* const *name[5][10] ) ( void );
O array name tem 50 elementos organizados num array multidimensional.
Os elementos são ponteiros para um ponteiro que é constante. Este ponteiro
constante aponta para uma função que não tem parâmetros e retorna um
179
ponteiro para um tipo unsigned int.
O exemplo a seguir é a declaração de uma função que retorna um ponteiro
para um array de três valores double.
double ( *var( double (*)[3] ) )[3];
Nessa declaração, a função retorna um ponteiro para um array. Funções que
retornam arrays não são permitidas. Aqui var é declarada como sendo um
ponteiro para o retorno da função, que aponta para um array de três elementos
double. O tipo de argumento é dado pelo declarador abstrato complexo. Os
parênteses ao redor do asterisco, no tipo de argumento são necessários; sem
eles, o tipo de argumento deverá ser um array de três ponteiros para valores
double.
union sign { /* Array de arrays de ponteiros */
/* para ponteiros para unions */
int x;
unsigned y;
} **var[5][5];
Como mostra o exemplo abaixo, um ponteiro pode apontar para outro
ponteiro, e um array pode conter arrays como elementos. Aqui var é um
array de cinco elementos. Cada elemento é um array de ponteiros para
ponteiros para unions com dois membros.
union sign *(*var[5])[5]; /* Array de ponteiros para arrays
de ponteiros para unions */
Esse exemplo mostra como a localização dos parênteses muda o significado
da declaração. Nesse exemplo, var é um array de cinco elementos de
ponteiros para arrays de cinco elementos para ponteiros para unions.
180
8.7. Inicialização
Um inicializador é um valor ou sequência de valores a serem atribuídos para
variáveis que estão sendo declaradas. Pode-se setar uma variável para um
valor inicial, aplicando inicializador no declarador, na declaração da variável.
O valor ou valores do inicializador será atribuído à variável.
As seções seguintes descrevem como inicializar variáveis de tipo escalares,
compostos e strings. Os tipos escalares incluem todos os tipos aritméticos,
mais os ponteiros. Os tipos compostos incluem arrays, estruturas e unions.
8.7.1. Inicializando Tipos Escalares
Quando for inicializadoum valor de tipo escalar, o valor da expressão será
atribuído à variável. Existem regras de conversão de tipos de dados que
poderão ser aplicadas, como será visto mais adiante.
A sintaxe básica de uma inicialização para o tipo escalar é:
especificador-de-tipo identificador = inicializador;
Qualquer tipo de variável pode ser inicializada, sempre que obedecidas as
seguintes regras:
As variáveis declaradas com nível de escopo de arquivo podem ser
inicializadas. Caso uma variável de nível externo não seja inicializada
explicitamente, ela será inicializada com 0 por default.
Uma expressão constante pode ser usada para inicializar qualquer
variável global declarada com o especificador de classe de
armazenamento static. As variáveis declaradas para ser static são
inicializadas quando começar a execução do programa. Caso estas não
forem explicitamente inicializadas, estas também serão inicializadas em
0 por default, e para cada membro que seja do tipo ponteiro, será
atribuído um ponteiro null.
As variáveis declaradas com a classe de armazenamento auto ou
register, são inicializadas cada vez que o controle da execução passa para
o bloco no qual estão declaradas. Se for omitido o inicializador na
declaração das variáveis auto ou register, o seu valor inicial será
indefinido. Para este tipo de variáveis, o inicializador não é restringido a
ser um valor constante; também pode ser uma expressão envolvendo
valores previamente definidos, assim como chamadas de função.
Os valores iniciais para declarações de variáveis externas (extern) e
para todas as variáveis estáticas (static), devem ser expressões
constantes. Uma vez que o endereço de qualquer variável declarada
externamente ou como static é constante, ele pode ser utilizado para
inicializar uma variável ponteiro static declarada internamente.
Entretanto, o endereço de uma variável auto não pode ser usado como
181
um inicializador static, porque ele pode ser diferente para cada execução
do bloco. Podem ser usados valores constantes ou variáveis para
inicializar variáveis do tipo auto e register.
Se a declaração de um identificador tem escopo de bloco, e o
identificador tem ligação externa, a declaração não pode ter uma
inicialização.
Os seguintes exemplos ilustram algumas inicializações:
int x = 10;
A variável inteira x é inicializada com a expressão constante 10.
register int *px = 0;
O ponteiro px é inicializado para 0, produzindo um ponteiro null.
const int c = (3 * 1024);
No exemplo, foi usada a expressão constante (3 * 1024) para inicializar c para
um valor constante que não poderá ser modificado devido a keyword const.
int *b = &x;
Esta instrução inicializa o ponteiro b com o endereço de uma outra variável,
x.
int *const a = &z;
O ponteiro a é inicializado com o endereço da variável z. Entretanto, uma
vez que ele foi especificado como sendo classe const, a variável a pode
somente ser inicializada, mas nunca modificada. Desta forma, o ponteiro
apontará sempre para o mesmo endereço.
int GLOBAL ;
int function( void ){
int LOCAL ;
static int *lp = &LOCAL; /* Delcaração ilegal */
static int *gp = &GLOBAL; /* Declaração legal */
register int *rp = &LOCAL; /* Declaração legal */
}
A variável global chamada GLOBAL, é declarada de nível externo, de forma
que possui tempo de vida global. A variável local chamada LOCAL, tem
classe de armazenamento auto e somente possui um endereço durante a
execução da função na qual é declarada. Desta maneira, ao tentar inicializar o
ponteiro static lp com o endereço de LOCAL, haverá a geração de um erro.
O ponteiro static gp pode ser inicializado com o endereço de GLOBAL
porque o endereço dele é sempre o mesmo. De forma análoga, *rp pode ser
inicializado porque rp é uma variável local e pode ter um inicializador não
constante. Cada vez que o bloco é executado, a variável LOCAL tem um
novo endereço, que será então designado a rp.
8.7.2. Inicializando Tipos Compostos
Um tipo composto é um tipo struct, union ou array. Se um tipo composto
contém membros de tipos também compostos, as regras de inicialização se
182
aplicam recursivamente.
A sintaxe básica da inicialização é:
especificador-de-tipo identificador = { inicializador ou lista-de-
inicializadores,...}
A lista de inicializadores é um conjunto de inicializadores separados por
vírgulas. Cada inicializador do conjunto é uma expressão constante ou uma
lista de inicializadores. Assim, as listas de inicializadores podem ser
aninhadas. Esta forma é muito usada para inicializar membros compostos de
um tipo composto como mostram os exemplos desta seção.
Entretanto, se o inicializador para um identificador automático é uma única
expressão, não necessita ser uma expressão constante; necessita meramente
ter o tipo apropriado para a atribuição ao identificador.
Para cada lista de inicializadores, os valores das expressões constantes são
atribuídos, em ordem, aos membros correspondentes da variável composta.
Se a lista de inicializadores tiver menos valores que o tipo composto, os
membros ou elementos restantes do tipo composto serão inicializados a 0,
para variáveis externas e static. O valor inicial de um identificador
automático, não explicitamente inicializado, será indefinido. Se a lista de
inicializadores tiver mais valores do que o tipo composto, resultará em um
erro de compilação. Essas regras aplicam-se a cada lista embutida de
inicializadores, como para os tipos compostos em si.
Um inicializador de estrutura pode ser uma expressão do mesmo tipo, ou uma
lista de inicializadores para os seus membros fechados entre chaves ({ }).
Campos de bits não nomeados, não serão inicializados.
Quando uma union é inicializada, a lista de inicializadores deve ser uma única
expressão constante. O valor da expressão constante é atribuído ao primeiro
membro da union.
Se um array tem tamanho desconhecido, o número de inicializadores
determina o tamanho do mesmo. Não há forma de especificar uma repetição
de um inicializador na linguagem C, nem como inicializar um elemento na
metade de um array, sem prover todos os valores precedentes.
É importante notar, que o número de inicializadores pode definir o tamanho
do array.:
int x[ ] = { 0, 1, 2 }
Caso seja especificado o tamanho, e dado um número errado de
inicializadores (a mais), o compilador gerará um erro.
Segue um exemplo de inicialização para um array:
int P[4][3] = {
{ 1, 1, 1 },
{ 2, 2, 2 },
183
{ 3, 3, 3,},
{ 4, 4, 4,},
};
Essa instrução declara P como um array quatro por três, e inicializa os
elementos da primeira linha com 1, a segunda linha com 2, e assim
sucessivamente até a quarta linha. Deve-se notar que a lista de inicialização
para a terceira e quarta linhas contém vírgulas, após a última expressão
constante. A última lista de inicialização ({4, 4, 4,},) também acaba numa
vírgula. Essas vírgulas a mais são permitidas, mas não são necessárias;
somente são necessárias as vírgulas que separam expressões constantes umas
de outras, e aquelas que separam uma lista de inicializadores de outra.
Se um membro composto não possuir uma lista de inicializadores agrupada,
os valores serão simplesmente atribuídos, na ordem em que aparecem, para
cada membro do subconjunto.Desta forma, a inicialização do exemplo
anterior é equivalente à seguinte:
int P[4][3] = {
1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4
};
Podem ser colocadas chaves em torno de cada inicializador individual da
lista.
Quando forem inicializadas variáveis compostas, deve se ter cuidado ao usar
as chaves de modo a colocar a lista de inicializadores de forma apropriada. O
seguinte exemplo ilustra a interpretação do compilador, com respeito às
chaves, com mais detalhes:
typedef struct {
int n1, n2, n3;
} triplet;
triplet nlist[2][3] = {
{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }, /* Linha 1 */
{ { 10,11,12 }, { 13,14,15 }, { 16,17,18 } } /* Linha 2 */
};
No exemplo, nlist é declarada como um array 2x3 de estruturas, cada uma
delas tendo três valores. A linha 1 da inicialização atribui valores para a
primeira linha de nlist, como segue:
1. A abertura de chaves na linha 1 sinaliza ao compilador que a
inicialização do primeiro membro composto de nlist (que é, nlist[0])
está começando.
2. A segunda abertura de chaves, à esquerda, indica que a inicialização
do primeiro membro composto de nlist[0] (que é, a estrutura em
nlist[0][0]) está começando.
3. O primeiro fechamento de chaves finaliza a inicialização da
estrutura nlist[0][0]; o próximo abre chaves começa a inicialização
de nlist[0][1].
184
4. O processo continua até o final da linha, onde o fechamento de
chaves finaliza a inicialização de nlist[0].
A linha 2 atribui valores para a segunda linha de nlist de forma análoga.
Deve-se notar no código, que as chaves externas que envolvem os
inicializadores das linhas 1 e 2 são necessárias. Na seguinte construção, onde
são omitidas as chaves externas, haverá erro:
triplet nlist[2][3] = /* Isto causa um erro de compilação */
{
{ 1, 2, 3 },{ 4, 5, 6 },{ 7, 8, 9 }, /* Linha 1 */
{ 10,11,12 },{ 13,14,15 },{ 16,17,18 } /* Linha 2 */
};
Nessa construção, o primeiro abre chaves na linha 1 começa o inicializador de
nlist[0], que é um array de três estruturas. Os valores 1, 2 e 3 serão atribuídos
aos três membros da primeira estrutura. Quando for encontrado o próximo
fecha chaves (depois do valor 3), a inicialização de nlist[0] estará completa, e
as duas estruturas restantes do array de três estruturas serão automaticamente
inicializadas em 0. De forma análoga, o conjunto { 4,5,6 } inicializa a
primeira estrutura da segunda linha de nlist. As duas estruturas restantes de
nlist[1] serão setadas a 0. Quando o compilador encontrar a seguinte lista de
inicialização ({ 7,8,9 }), tentará inicializar nlist[2]. Já que nlist possui
somente duas linhas, será originado um erro de compilação.
No exemplo seguinte, os três membros inteiros de x serão inicializados para
1, 2 e 3 respectivamente.
struct list {
int i, j, k;
float m[2][3];
} x = {
1,
2,
3,
{4.0, 4.0, 4.0}
};
Na lista de inicializadores de estrutura acima, os três elementos da primeira
linha de m são inicializados para 4.0; os elementos restantes da linha de m
serão zerados por default.
union{
char x[2][3];
int i, j, k;
} y = { {
{'1'},
{'4'}
}
};
A variável union y, do exemplo, é inicializada. O primeiro elemento da union
é um array, de forma que o inicializador é composto. A lista de inicialização
{‘1’} atribui o valor à primeira linha do array. Uma vez que, somente um
185
valor aparece na lista, o elemento na primeira coluna é inicializado com o
caractere 1, e os dois elementos restantes da linha são inicializados com valor
0. De forma similar, o primeiro elemento da segunda linha de x é inicializado
com o caractere 4, sendo que os dois elementos restantes da linha serão
inicializados em zero.
8.7.3. Inicializando Strings
Os arrays de caracteres (strings) podem ser inicializados com uma string
literal. Por exemplo:
char code[ ] = "abc";
inicializa a variável code como um array de quatro elementos do tipo
caractere. O quarto elemento é o caractere null, que finaliza uma string
literal.
Uma lista de inicializadores deve ser do mesmo tamanho que o número de
elementos a serem inicializados. Caso seja especificado o tamanho para o
array menor que o tamanho da string de inicialização, os caracteres extras da
string serão ignorados. Por exemplo, a seguinte declaração inicializa um
array de três elementos do tipo caractere:
char code[3] = "abcd";
Somente os três primeiros caracteres do inicializador serão atribuídos a code.
O caractere d e o caractere null de terminação serão descartados. Deve-se
notar que isso cria uma string indeterminada (i.e., sem o null que indica o seu
final) e, provavelmente, o compilador gerará uma mensagem indicando essa
condição.
A declaração
char s[] = "abc", t[3] = "abc";
é idêntica a
char s[] = {'a', 'b', 'c', '\0'},
t[3] = {'a', 'b', 'c' };
Se a string é menor que o tamanho especificado para o array, os elementos
restantes serão inicializados em 0.
Dependendo do compilador, a inicialização de strings poderá ser limitada em
tamanho. Por exemplo no Microsoft C, as strings literais podem ter até 2048
bytes. No compilador PCW para microcontroladores PIC (Microchip), até 16
bytes.
186
8.8. Armazenamento de Tipos Básicos
As tabelas a seguir mostram o espaço de armazenamento associado a cada
tipo básico.
Tipo Espaço de
Armazenamento (bytes)
char, unsigned char, signed char 1
short, unsigned short 2
int, unsigned int 4
long, unsigned long 4
Float 4
Double 8
long double 10
Tabela 8-3 – Tipos de dados de um compilador para processadores da família 8x86 de 32 bits (Visual
C++ e Borland C++ 4.0):
Tipo Espaço de
Armazenamento (bytes)
char, unsigned char, signed char 1
short, unsigned short 1 bit
int, unsigned int 1
long, unsigned long 2
Float 4
Double não
Long double não
Tabela 8-4 – Tipos de dados de um compilador para processador PIC de 8 bits CCS PCW:
Tipo Espaço de
Armazenamento (bytes)
Char, unsigned char, signed char 1
short, unsigned short 1
int, unsigned int 2
long, unsigned long 4
Float 4
Double 4
long double 4
Tabela 8-5 - Tipos de dados de um compilador para processador da família 8051 de 8 bits (ex. Franklin,
Keil e Archimedes):
Tipo Espaço de
Armazenamento (bytes)
char, unsigned char, signed char 1
short, unsigned short 1
int, unsigned int 2
long, unsigned long 4
Float 4
Double 8
long double 10
187
Tabela 8-6 – Tipos de dados de um compilador para processador da família 8x86 de 16 bits (ex. Borland
Turbo C++ 3.x e Microsoft C):
Os tipos de dados em C podem ser agrupados em categorias gerais: os tipos
“inteiros” que incluem char, int, short, long, signed, unsigned e enum. A
segunda categoria é a dos tipos “ponto flutuante”, que incluem o float,
double e long double. Os tipos “aritméticos” incluem todos os tipos inteiros
e de ponto flutuante.
8.8.1. Tipo char
O tipo char é usado para armazenar o valor inteiro de um membro de um
grupo de caracteres representáveis. O valor inteiro é o código em ASCII
correspondente ao caractere especificado.
Esse tipo é bastante usado também para armazenar números de 8 bits para
manipulação de portas de comunicação
8.8.2. Tipo int
O tamanho de um item do tipo int, com sinal ou sem sinal, é o tamanho
padrão de um inteiro num processador ou sistema operacional particular. Por
exemplo, em sistemas operacionais de 16bits, o tipo int é usualmente de 16
bits, ou dois bytes. Em sistemas operacionais de 32 bits, o tipo int é
usualmente de 32 bits, ou quatro bytes. O mesmo acontece com
processadores de tamanho de palavra diferente. Assim, o tipo int num
sistema de 32 bits pode ser equivalente ao long int de um sistema de 16 bits, e
um short int de um sistema de 32 bits pode ser equivalente ao int de um
sistema de 16 bits, dependendo do ambiente alvo. Os tipos int, em geral,
representam valores com sinal (signed), a menos que, seja especificado de
outra forma (unsigned). No compilador PCW da CCS para PIC o padrão é
unsigned.
Os especificadores de tipo int e unsigned int (ou simplesmente unsigned)
definem certas características da linguagem C (por exemplo, o tipo enum).
Nestes casos, as definições de int e unsigned int para uma implementação
particular, determinarão o atual espaço de armazenamento[65].
Em geral, os inteiros com sinal são representados e tratados na forma de
complemento de dois. O bit mais significativo armazena o sinal: 1 se for
negativo, 0 para positivos[66].
8.8.3. Tipo float
Para representar os números de ponto flutuante, a maioria dos compiladores
usa o formato IEEE[67], que consiste em um bit de sinal, um expoente de 8
bits (excesso 127) e uma mantissa de 23 bits. A mantissa representa um
número entre 1.0 e 2.0. Desde que, o bit mais significativo da mantissa é
sempre 1, este não é armazenado no número. Esta representação permite uma
188
faixa aproximada de valores que vai de 3.4E–38 a 3.4E+38 para o tipo float.
Uma variável pode ser declarada como float ou double, dependendo das
necessidades da aplicação. A principal diferença entre esses dois tipos, é a
significância que esses podem representar, o espaço de memória necessário, a
faixa de valores e a velocidade de processamento. A tabela a seguir permite
observar algumas relações entre esses dois tipos.
Tipo Dígitos
Significativos
Número de bytes
float 6 – 7 4
double 15 – 16 8
Tabela 8-7 – Características dos tipos float e double
As variáveis de ponto flutuante são representadas pela mantissa, que contém o
valor do número; e o expoente, que contém a ordem de grandeza do número.
A tabela a seguir mostra o número de bits alocados para a mantissa e o
expoente para cada tipo de dado de ponto flutuante. O bit mais significativo
de qualquer float ou double é sempre o bit de sinal. Se for igual a 1, o
número é considerado negativo; caso contrário, será considerado um número
positivo.
Tipo Comprimento do Expoente Comprimento da Mantissa
float 8 bits 23 bits
double 11 bits 52 bits
Tabela 8-8 - Comprimento do expoente e mantissa
Devido a que o expoente é armazenado na forma unsigned, é colocado um
offset da metade do seu valor máximo. Para o tipo float, o offset é de 127
unidades; para o tipo double é 1023. O valor atual do expoente pode ser
calculado subtraindo o valor do offset, do valor do expoente. O valor de
offset permite a representação de expoentes negativos, que são necessários
para representar números menores que a unidade.
A mantissa é armazenada como sendo uma fração binária maior ou igual a 1,
e menor que 2. Para os tipos float e double, existe um bit 1 implícito na
posição mais significativa, de forma que a mantissa possui efetivamente 24 e
53 bits de comprimento respectivamente. O bit mais significativo da mantissa
não é armazenado na memória.
A tabela a seguir mostra os valores máximos e mínimos que podem ser
armazenados em variáveis de cada tipo. Os valores listados nesta tabela se
aplicam a números de ponto flutuante normalizado.
Os números retidos nos registradores de um coprocessador matemático
80x87, são sempre representados na forma normalizada de 80 bits.
Tipo Valor Mínimo Valor Máximo
float 1.175494351 E – 38 3.402823466 E + 38
189
double 2.2250738585072014 E – 308 1.7976931348623158 E + 308
Tabela 8-9 - Valores máximos e mínimos em ponto flutuante
Se a precisão necessária for mais baixa que o float pode representar,
considere a utilização deste tipo de variável. Caso a precisão elevada for o
critério mais importante, deve-se utilizar o tipo double.
As variáveis de ponto flutuante podem ser convertidas para um tipo com
significância maior (do tipo float para o tipo double). Essas conversões
ocorrem frequentemente quando são executadas operações aritméticas em
variáveis de ponto flutuante. A aritmética é sempre executada num grau de
precisão mais elevado que a variável com o mais alto grau de precisão. Por
exemplo, considerar as seguintes declarações:
float f_short;
double f_long;
f_short = f_short * f_long;
No exemplo acima, a variável f_short é convertida para o tipo double e
multiplicada pela f_long; então, o resultado é arredondado para o tipo float
antes de ser atribuída a f_short.
No exemplo a seguir, a aritmética é feita nas variáveis com precisão de float
(32 bits); o resultado, então, é convertido para o tipo double.
float f_short;
double f_longer;
f_longer = f_short * f_short;
8.8.4. Tipo double
Os valores do tipo double (dupla precisão) são representados e armazenados
usando 8 bytes. O formato é similar ao formato float, exceto que, possui um
expoente de 11 bits (com excesso 1023) e uma mantissa de 52 bits, mais o bit
mais significativo implícito em 1. O formato permite uma faixa aproximada
de valores que vão de 1.7E–308 a 1.7E+308 para esse tipo.
Poucos compiladores para microcontroladores suportam esse formato, devido
ao tamanho de código necessário para fazer aritmética, e ao tamanho da
memória de dados necessária para armazená-los, já que, estes dispositivos
possuem recursos bastante limitados, quando comparados com um
computador desktop.
8.8.5. Tipo long double
A faixa de valores para uma variável é limitada pelos valores máximos e
mínimos que podem ser representados internamente, com um determinado
número de bits. Dessa forma, devido às regras de conversão da linguagem C,
nem sempre pode ser utilizado o valor máximo ou mínimo para uma
constante de um tipo particular, dentro de uma expressão.
190
Por exemplo, a expressão constante –32768 que consiste do operador de
negação aritmética (-) aplicada ao valor constante 32768. Como o valor
32768 é muito grande para ser representado como um valor de 16 bits (ex.
int), a variável deverá ser do tipo de 32 bits (ex. long int).
Consequentemente, a expressão constante –32768 é do tipo long. Pode-se
representar –32768 como um tipo de 16 bits (ex. int) pela utilização de um
modelador de tipo (cast) para o tipo de 16 bits. Desta forma nenhuma
informação será perdida no tipo convertido, desde que –32768 possa ser
representado internamente em 2 bytes.
O valor 65000 em notação decimal é considerado como uma constante
signed. Será armazenado como sendo do tipo de 32 bits, porque a sua
representação não cabe em 16 bits com sinal. Um valor como 65000 pode ser
representado como um valor de 16 bits somente pelo uso de um conversor de
tipo (cast) para o tipo de 16 bits, ou pela especificação do número como
65000U. Pode-se converter este valor de 32 bits em 16 bits, sem perda de
informação devido a que o valor 65000 pode ser armazenado em 2 bytes em
representação sem sinal (unsigned).
A maioria dos compiladores para sistemas de 32 bits suporta o tipo long
double de 80 bits (10 bytes): 1 bit para o sinal, 15 para o expoente e 64 para a
mantissa. A faixapossível vai de 1.2E-4932 a 1.2E+4932 com no mínimo de
19 dígitos de precisão. Embora os tipos double e long double sejam tipos
diferentes, a representação é idêntica.
191
8.9. Tipos Incompletos
Um tipo incompleto é um tipo que descreve um identificador, mas não
fornece a informação necessária para determinar o tamanho do identificador.
Um tipo “incompleto” pode ser:
Um tipo de estrutura cujos membros não tenham ainda sido
especificados.
Um tipo union cujos membros não tenham ainda sido especificados.
Um array cujas dimensões não tenham ainda sido especificadas.
O tipo void é um tipo incompleto que não pode ser completado. Para
completar um tipo incompleto, deve ser especificada a informação faltante.
Os seguintes exemplos mostram como criar e completar tipos incompletos.
Para criar um tipo estrutura incompleta, declarar o tipo de estrutura
sem especificar os seus membros. No exemplo a seguir, o ponteiro pres
aponta para um tipo estrutura incompleto chamado resistor.
struct resistor *pres;
Para completar um tipo estrutura incompleto, declarar o mesmo tipo
de estrutura, mais tarde, no mesmo escopo com os seus membros
especificados, como em
struct resistor{
int num;
} /* estrutura resistor agora está completa */
Para criar um tipo array incompleto, declarar o array sem especificar
o seu tamanho. Por exemplo:
char a[]; /* a é um tipo incompleto */
Para completar um array incompleto, declarar o mesmo nome,
depois, no mesmo escopo com o tamanho especificado, como em:
char a[25]; /* a agora esta’completa */
192
8.10. Declarações typedef
Uma declaração typedef é uma declaração que usa a keyword typedef como
classe de armazenamento. O declarador aparecerá como sendo um novo tipo
de dado. Pode-se utilizar as declarações typedef para construir tipos com
nomes menores ou mais significativos, para tipos previamente definidos pela
linguagem ou para novos tipos que se desejam criar. Os nomes typedef
permitem encapsular detalhes de implementação que podem mudar.
Uma declaração typedef é interpretada da mesma forma que uma declaração
de variável ou função, mas o identificador, no lugar de assumir o tipo
especificado pela declaração, fica como um sinônimo para o tipo.
É importante notar, que uma declaração typedef não cria tipos. Ela cria
sinônimos para tipos existentes, ou nomes para tipos que possam ser
especificados de maneiras diferentes. Quando um nome typedef é utilizado
como um especificador de tipo, ele pode ser combinado com um certo tipo de
especificadores, mas não com todos. São aceitos os modificadores, tais como,
const e volatile.
Os nomes typedef compartilham o mesmo espaço com os identificadores
ordinários. Desta forma, um programa pode ter um nome typedef e um
identificador com escopo local do mesmo nome. Por exemplo:
typedef char Flag;
int main(){
}
int myproc( int ){
int Flag;
}
Código 8-11
Quando for declarado um identificador de escopo local com o mesmo nome
que um typedef, ou quando for declarado um membro de uma estrutura ou
union no mesmo escopo ou em um escopo interno, o especificador de tipo
deverá ser definido. Este exemplo ilustra este evento:
typedef char Flag;
const Flag x;
Para poder reutilizar o nome Flag para um identificador, membro de
estrutura, ou union, o tipo deve ser declarado.
const int Flag; /* O especificador de tipo é requerido */
Não será suficiente escrever
const Flag; /* Especificação incompleta */
já que Flag é interpretado como sendo parte do tipo, não um identificador que
está sendo redeclarado. A declaração é interpretada como sendo uma
193
declaração ilegal parecida com
int; /* Declaração ilegal */
Qualquer tipo pode ser declarado com typedef, incluindo ponteiros, funções e
tipos array. Pode-se declarar um nome typedef para um ponteiro para uma
estrutura ou union antes de definir o tipo da própria estrutura ou union, ou tão
logo a definição tenha a mesma visibilidade que a declaração.
O seguinte exemplo ilustra uma declaração typedef:
typedef int INTEIRO; /* Declara INTEIRO para ser sinônimo para int */
Deve-se notar que INTEIRO pode agora ser usado em declarações de
variáveis tais como, INTEIRO i; ou const INTEIRO i. Entretanto, a
declaração de long INTEIRO i será ilegal.
typedef struct transistor {
char partnumber[30];
int tipo, beta;
} TRANSISTOR;
Estas linhas declaram que TRANSISTOR é uma tipo de estrutura com três
membros. Desde que são especificados o tag e os membros, poderão ser
usados o nome typedef (TRANSISTOR) ou o tag da estrutura, em declarações
posteriores. Pode-se usar a keyword struct com o tag, caso não se queira usar
a keyword struct com o nome typedef.
typedef TRANSISTOR *PG; /* Usa o nome typedef declarado previamente
para declarar um ponteiro */
O tipo PG é declarado como um ponteiro para uma variável tipo
TRANSISTOR, que é definida como sendo um tipo estrutura.
typedef void DRAWF( int, int );
O exemplo a seguir define o tipo DRAWF para uma função que não retorna
valores e possui dois argumentos. Isto permitirá, por exemplo, a seguinte
declaração:
DRAWF box;
é equivalente a declarar:
void box( int, int );
194
8.11. Conversão de Tipos
Nas atribuições de valores na linguagem C, tem-se o seguinte formato:
destino = origem;
Se o destino e a origem são de tipos diferentes, o compilador fará uma
conversão entre os tipos. Nem todas as conversões são possíveis de serem
efetuadas. O primeiro ponto a ser ressaltado é que o valor de origem é
convertido para o valor de destino, antes de ser atribuído e não o contrário.
É importante lembrar, que quando se converter um tipo numérico para outro
não se melhora a precisão. Pode-se perder precisão ou no máximo mantê-la.
A seguir é mostrada uma tabela de conversões numéricas com perda de
precisão, para um compilador de 16 bits:
De Para Informação Perdida
unsigned
char char
Valores maiores que 127 são
alterados
short int char Os 8 bits de mais alta ordem
int char Os 8 bits de mais alta ordem
long int char Os 24 bits de mais alta ordem
long int short int Os 16 bits de mais alta ordem
long int int Os 16 bits de mais alta ordem
float int Precisão - resultado arredondado
double float Precisão - resultado arredondado
long double double Precisão - resultado arredondado
Tabela 8-10 - Conversões com perda de precisão
195
8.12. Exercícios
1. Que são declarações na linguagem C?
2. Descreva brevemente as classes de armazenamento existentes na linguagem C?
3. Qual a diferença entre as classes de armazenamento extern e static?
4. Citar os especificadores de tipo.
5. O que são qualificadores de tipo?
6. Interpretar as seguintes declarações:
int **x[10]
float *(*x)(void)
7. A inicialização de variáveis aumenta o tamanho do programa?
8. A inicialização de ponteiros aumenta o tamanho do programa?
196
Capítulo
9
197
9. OPERADORES E EXPRESSÕES
Este capítulo descreve como formar expressões e atribuir valores na
linguagem C. Constantes, identificador, strings e chamadas de função são os
operandos que são manipulados nas expressões. A linguagem C possui todos
os operadores habituais das linguagens de programação. Este capítulo cobre
esses operadores, assim como, também os operadores que são únicos da
linguagem C. Os tópicos que serão discutidosincluem:
Expressões l-values e r-values
Expressões constantes
Efeitos colaterais
Pontos de sequência
Operadores
Precedência dos operadores
Conversões de tipo
Casts
198
9.1. Introdução
Um "operando" é uma entidade na qual um operador pode atuar, por exemplo:
c = a + b ;
No exemplo, a, b e c são os operandos, = e + são os operadores.
Uma "expressão" é uma sequência de operadores e operandos que executam
qualquer combinação das seguintes ações:
Cálculo de um valor
Designação de um objeto ou função
Geração de efeitos colaterais
Os operandos em C incluem constantes, identificadores, strings, chamadas de
função, expressões subscritas, expressões de seleção de membros, e
expressões complexas formadas pela combinação de operandos, ou pelo
fechamento de operandos entre parênteses. A sintaxe para estes operandos é
ilustrada na seção 9.3
199
9.2. Introdução aos Operadores
9.2.1. Operadores Aritméticos e de Atribuição
Os operadores aritméticos são usados para desenvolver operações
matemáticas. A Tabela 9-1 mostra a lista dos operadores aritméticos da
linguagem C:
Operador Ação
+ Soma (inteira e ponto flutuante)
- Subtração ou Troca de sinal (inteira e ponto
flutuante)
* Multiplicação (inteira e ponto flutuante)
/ Divisão (inteira e ponto flutuante)
% Resto de divisão (inteiros)
++ Incremento (inteiro e ponto flutuante)
-- Decremento (inteiro e ponto flutuante)
Tabela 9-1 – Operadores Aritméticos
A linguagem C possui operadores unários e binários. Os operadores unários
agem sobre uma variável apenas, modificando ou não o seu valor, e retornam
o valor final da mesma. Os operadores binários utilizam duas variáveis e
retornam um terceiro valor, sem alterar os valores das variáveis originais. A
soma é um operador binário que utiliza duas variáveis como operandos, soma
seus valores sem alterar as mesmas, e retorna o resultado da operação. Outros
operadores binários são os operadores - (subtração), *, / e %. O operador ‘-‘
quando utilizado para troca de sinal, é um operador unário que não altera o
valor da variável sobre a qual é aplicado, retornando o valor da variável
multiplicado por -1.
O operador divisão (/) quando aplicado a variáveis inteiras, fornece o
resultado da divisão inteira; quando aplicado a variáveis em ponto flutuante
fornece o resultado da divisão em ponto flutuante. Examinar o seguinte
código:
int a = 17, b = 3;
int x, y;
float z = 17.0 , z1, z2;
x = a / b;
y = a % b;
z1 = z / b;
z2 = a/b;
No final da execução destas linhas, os valores calculados seriam x = 5, y = 2,
z1 = 5.666666 e z2 = 5.0 . Deve-se notar que na linha correspondente a z2,
primeiro será feita uma divisão inteira (pois os dois operandos são inteiros).
Somente depois de efetuada a divisão, o resultado será atribuído a uma
variável do tipo float.
Os operadores de incremento e decremento são unários, alterando o valor da
200
variável sobre a qual são aplicados. A função destes operadores é a de
incrementar ou decrementar o valor da variável, sobre a qual estão aplicados,
de uma unidade. Assim:
x++;
x--;
são equivalentes a
x=x+1;
x=x-1;
Estes operadores podem ser pré-fixados ou pós- fixados. A diferença é que
quando são pré-fixados eles incrementam e retornam o valor da variável já
incrementada. Quando são pós-fixados, eles retornam o valor da variável sem
o incremento e depois incrementam a variável. Assim, no exemplo a seguir:
x=23;
y=x++;
obter-se, no final, y=23 e x=24. E no exemplo seguinte:
x=23;
y=++x;
obter-se, no final, y=24 e x=24.
O operador de atribuição da linguagem C é o ‘=’. A função deste operador é a
de copiar o valor (ou resultado de uma expressão) à direita, e atribuir à
variável da sua esquerda. Além disso, ele retorna o valor atribuído. Isso faz
com que as seguintes expressões sejam válidas:
x=y=z=1.5; /* Expressao 1 */
if (k=w) ... /* Expressao 2 */
A expressão 1 é válida, pois quando é executada a instrução z=1.5, ela retorna
1.5, que é repassado para a próxima parte da expressão. A expressão 2 será
verdadeira se w for diferente de zero, pois esse será o valor retornado por
k=w. Deve-se evitar o uso de atribuições dentro de comandos, onde
usualmente existem operações de comparação, para evitar erros de
interpretação. Na expressão 2, por exemplo, não se está comparando k e w,
mas o valor de w está sendo atribuído à variável k, e o valor desta última está
sendo utilizado para tomar uma decisão.
9.2.2. Introdução aos Operadores Relacionais e Lógicos
Os operadores relacionais da linguagem C avaliam relações entre variáveis.
Esses operadores são usualmente utilizados para efetuar comparações de
igualdade e ordem de grandeza. Os operadores relacionais estão listados na
Tabela 9-2.
Operador Ação
> Maior do que
>= Maior ou iguala
< Menor do que
201
<= Menor ou igual
a
== Igual a
!= Diferente de
Tabela 9-2 -Operadores relacionais
Os operadores relacionais retornam valores lógicos binários, verdadeiro
(diferente de zero) ou falso (igual a 0). Todos os operadores relacionais são
operadores binários (operam sobre dois operandos).
Os operadores lógicos efetuam as operações AND, OR e NOT. Estes
operadores são mostrados na Tabela 9-3.
Operador Ação
&& AND (E)
|| OR (OU)
! NOT (NÃO)
Tabela 9-3 - Operadores lógicos
Os operadores AND e OR são binários (precisam de dois operandos). O
operador NOT é unário (opera sobre um operando).
Com o uso dos operadores relacionais lógicos pode-se realizar uma grande
gama de testes. A tabela-verdade destes operadores é dada na Tabela 9-4.
P q p AND q p OR q
Falso falso falso falso
Falso verdadeiro falso verdadeiro
verdadeiro falso falso verdadeiro
verdadeiro verdadeiro verdadeiro verdadeiro
Tabela 9-4 - Tabela verdade dos operadores lógicos e relacionais
No trecho de programa abaixo a instrução if será executada, já que o resultado
da expressão lógica será verdadeiro:
int i = 5, j = 7;
if ( (i > 3) && (j <= 7) && (i != j) ){
j++;
}
(verdadeiro) AND (verdadeiro) AND (verdadeiro) = verdadeiro
9.2.3. Introdução aos Operadores Lógicos Bit a Bit
A linguagem C permite que operações lógicas “bit a bit” (bitwise) em
números. Ou seja, neste caso, o número é representado por sua forma binária
e as operações são feitas em cada bit por separado.
Como exemplo, pode-se considerar um número inteiro de 16 bits, identificado
por i, armazenando o valor 2.
int i = 2;
A representação binária de i, será: 0000 0000 0000 0010. Pode-se fazer
operações em cada um dos bits desse número. Se for feita a negação do
202
número (operação binária NOT, ou operador binário ‘~’), isto é, ~i, o número
se transformará em 1111 1111 1111 1101 que equivale a 0xFFFD ou –3.
void main(void){
int i = 2;
i = ~i;
}
Código 9-1
As operações binárias ajudam programadores efetuar funções de operação
sobre bits por separado. As operações lógicas bit a bit só podem ser usadas
nos tipos inteiros. Os operadores são listados na Tabela 9-5.
Operador Ação
& AND
| OR
^ XOR (OR exclusivo)
~ NOT
>> Deslocamento de bits a direita
<< Deslocamento de bits aesquerda
Tabela 9-5 - Operadores lógico bit a bit
Os operadores &, |, ^ e ~ são operadores lógicos bit a bit. A forma geral dos
operadores de deslocamento é:
valor>>número-de-bits-de-deslocamentos
valor<<número-de-bits-de-deslocamentos
O número-de-bits-de-deslocamento indica de quantas posições de um bit o
valor da variável deverá ser deslocado. Por exemplo, para a variável i do
exemplo anterior, armazenando um valor iguala 2 e fazendo:
void main(void){
int i = 2;
i << 3;
}
Código 9-2
Este programa fará com que i agora tenha a representação binária:
0000000000010000, isto é, o valor original de i será deslocado de três bits à
esquerda, resultando no novo valor igual a 0010H ou 16.
O deslocamento para a esquerda de cada bit equivale à multiplicação do valor
original por dois, e o deslocamento para a direita equivale à divisão do valor
por dois. Isso decorre da característica do sistema de numeração binário.
203
9.3. Expressões
As expressões são combinações de variáveis, constantes e operadores.
Quando são definidas as expressões, deve ser levada em consideração, a
ordem com que os operadores serão executados, conforme a tabela de
precedências da linguagem C, mostrada mais adiante.
Alguns exemplos de expressões:
Anos = Dias/365.25;
i = i+3;
c = a * b + d / e;
c = a * (b+d)/ e;
Em operações aritméticas complexas, deve-se ter o cuidado de que o
resultado das diversas operações dentro da expressão, não ultrapasse a faixa
de valores possíveis para o maior tipo que está sendo usado, nem do tipo da
atribuição. A não observância desse critério pode levar a resultados errados e
imprevisíveis, especialmente quando são utilizados compiladores para
pequenos microcontroladores. Deve-se sempre lembrar, que erros de cálculo
em aplicações que usam microcontroladores para controlar máquinas, podem
prejudicar aos seres vivos.
Um exemplo é mostrado a seguir, considerando um compilador com variáveis
inteiras de 16 bits.
#include<dos.h>
void main(){
int num; /* inteiro de 16 bits */
int x = 64535;
num = x + 1000;
}
Código 9-3
No exemplo, o resultado final para num é –1, enquanto, que o valor esperado
seria 65535. Neste código, ainda que não sendo aparente, existe um erro
grave que não é percebido pelo compilador nem o linker, já que é um erro
lógico de programação. O erro é a atribuição do valor 64535 para a variável
x, que é do tipo signed int, sendo que esse valor está fora da faixa de valores
positivos para o tipo, e será interpretado como –1001.
Avaliar o seguinte código.
#include<dos.h>
void main(){
unsigned int num;
unsigned int x = 6553;
num = x / 10 * 100 + 5;
}
Código 9-4
204
O valor esperado para num é 65535, mas o valor final destas linhas de código
para num é 65505. O fato é que a sequência em que serão executados os
operadores dentro de uma expressão, depende da implementação interna do
compilador que estiver sendo usado. Neste caso é executado o operador
divisão x/10 antes da multiplicação, perdendo um dígito (resultado igual a
655); multiplicado posteriormente por 100 (resultado igual a 65500) e
posterior adição com a constante 5 (resultado igual a 65505). A expressão
mais correta para a última linha de código seria: num = ( (x * 100) / 10 ) + 5;
ainda que dependendo do compilador deverão ser testados todos os valores
possíveis. O grande problema é quando as variáveis não são constantes, e sim
variáveis de um processo, como por exemplo, valores provenientes de um
sensor de temperatura, pressão ou vazão, e em ações que devem ser tomadas
de acordo com estas informações, tais como atuar em válvulas proporcionais,
ou na ativação de bombas pneumáticas ou hidráulicas. Desta forma, deve-se
ter cuidado com expressões como :
safe = temperature / 10 * pressure + flux;
if (safe >= 65535) { /* Condição de alarme – Situação de Perigo !*/
alarm(); /* Para valores iguais aos colocados no exemplo anterior o
valor de safe será menor que a necessária para o alarme.
O alarme não será ativado e seres vivos podem ser prejudicados */
}
Onde as variáveis temperature, pressure e flux provêm da leitura de
conversores analógico-digitais.
9.3.1. Conversão Temporária de Tipos
Quando a linguagem C avalia expressões onde há variáveis de tipos
diferentes, o compilador verificará se as conversões são possíveis. Se não
forem possíveis, será gerada uma mensagem de erro. Se as conversões forem
possíveis, estas serão feitas de acordo com as regras seguintes:
Todas as variáveis do tipo char e short int poderão eventualmente
ser convertidas para int. Todas as variáveis do tipo float poderão ser
convertidas para double.
Para pares de operandos de tipos diferentes: se um deles for long
double, o outro será convertido para long double; se um deles for
double o outro é convertido para double; se um é long o outro é
convertido para long; se um é unsigned o outro é convertido para
unsigned.
Em geral, sempre serão feitas as conversões para o tipo de maior precisão.
Deve ser evitado o uso de operadores aritméticos com tipos de variáveis
diferentes; em certos compiladores, os resultados poderão ser incorretos. Se
for extremamente necessário fazer operações com tipos diferentes, devem ser
usados os modeladores cast[68].
205
9.3.2. Expressões Abreviadas
A linguagem C permite abreviações para certas expressões, que podem ser
usadas para simplificar a representação ou para facilitar o entendimento de
um programa. Da mesma forma, que as abreviações de expressões agilizam a
leitura de um programa por um programador experiente, estas podem
dificultar o entendimento global do algoritmo implementado. Na Tabela 9-6
são mostrados alguns exemplos de abreviações permitidas pela linguagem C.
Expressão
Original
Expressão
Equivalente
x=x+k; x+=k;
x=x-k; x-=k;
x=x*k; x*=k;
x=x/k; x/=k;
x=x>>k; x>>=k;
x=x<<k; x<<=k;
x=x&k; x&=k;
Tabela 9-6 -Abreviação de Expressões
9.3.3. Encadeamento de Expressões: o Operador “,”
O operador ‘,’ determina uma lista de expressões que devem ser executadas
de forma sequencial. O valor retornado por uma expressão com o operador ‘,’
é sempre dado pela expressão mais à direita. No exemplo abaixo:
x=(y=2,y+3);
o valor 2 vai ser atribuído a y, se somará 3 a y e o retorno (5) será atribuído à
variável x . Pode-se encadear quantos operadores , forem necessários.
Não é recomendado o uso deste operador em instruções complexas
abreviadas, já que dificultam o entendimento do código, como no exemplo
anterior. Ficaria mais claro escrever:
y = 2;
y = y + 3;
x = y;
9.3.4. Precedências de Operadores
A precedência indica quais os operadores possuem maior prioridade numa
expressão complexa. A Tabela 9-7 mostra a relação de precedência dos
operadores da linguagem C.
Maior
precedência () [] ->
! ~ ++ -- . -(unário) (cast) *(unário) &(unário) sizeof
* / %
+ -
<< >>
<<= >>=
206
== !=
&
^
|
&&
||
?
= += -= *= /=
Menor
precedência ,
Tabela 9-7 - Precedência de Operadores
Caso não se conheça a tabela de precedências, para evitar cálculos incorretos,
separe a expressão complexa em blocos separados entre parênteses (maior
prioridade), de forma a ter certeza de como será efetuada a avaliação da
expressão, e de tornar o programa mais legível.
9.3.5. Expressões Primárias em C
Os operandos em expressões são chamados de "expressões primárias".
Identificadores em Expressões Primárias
Os identificadores podem ser do tipo inteiro, float, enum, struct, union, array,
ponteiro ou função. Um identificador é uma expressão primária colocada
para designar um objeto[69] ou uma função[70].
O valor do ponteiro representado pelo identificador do array não é uma
variável, de forma que um identificador de array não pode constituir um
operando à esquerda (left-hand operator) parauma operação de atribuição, e
desta forma não será um l-value válido.
int vetor[5];
vetor = 5; /* errado */
Um identificador declarado como função representa um ponteiro cujo valor é
o endereço da função. O ponteiro endereça uma função que retorna um valor
de um determinado tipo. Desta forma, os identificadores de funções também
não podem ser l-values em operações de atribuição.
int nFuncSoma(int x, int y); /* declaração da função nFuncSoma*/
main(){
nFuncSoma = 5; /* errado */
}
Código 9-5
Constantes em Expressões Primárias
Um operando constante possui o valor e o tipo do valor constante que este
representa. Uma constante caractere é do tipo int. Uma constante inteira
pode ter o tipo int, long, unsigned int ou unsigned long, dependendo do
tamanho do inteiro e a forma em que o valor é especificado.
207
String Literais em Expressões Primárias
Uma string literal é um caractere simples, caractere longo, ou sequência de
caracteres adjacentes encerrados entre aspas duplas. Uma vez que as strings
literais não são variáveis, nenhum dos seus elementos podem ser operandos l-
values em operações de atribuição. O tipo de uma string literal é um array de
caracteres. Os arrays nas expressões são convertidos em ponteiros.
Expressões entre Parênteses
Qualquer operando pode ser encerrado entre parênteses sem mudar o tipo ou
valor da expressão encerrada. Por exemplo, na expressão
( 10 + 5 ) / 5
os parênteses entre 10 + 5 permitem que o valor de 10 + 5 seja avaliado antes,
e cujo resultado é um operando de lado esquerdo (left-hand) para o operador
da divisão (/). O resultado de (10 + 5) / 5 é 3. Sem os parênteses 10 + 5 / 5
resultará em 11.
Embora, os parênteses afetem a forma em que os operandos são agrupados
nas expressões, eles não podem garantir uma ordem particular de avaliação
em todos os casos. Por exemplo, nem os parênteses nem o agrupamento de
esquerda para direita da seguinte expressão pode garantir que valor terá a
variável i em cada uma das subexpressões:
( i++ +1 ) * ( 2 + i )
Os compiladores poderão avaliar os dois lados da multiplicação em qualquer
ordem. Se o valor inicial de i é zero, a expressão poderá ser avaliada como
qualquer destas duas formas, por exemplo:
( 0 + 1 + 1 ) * ( 2 + 1 )
( 0 + 1 + 1 ) * ( 2 + 0 )
9.3.6. Expressões L-Value e R-Value
As expressões que referenciam locais da memória são chamadas de
expressões l-value (left-value). Um l-value representa uma região de
armazenamento, implicando que este poderá aparecer no lado esquerdo do
operador de atribuição (=). Os l-values são frequentemente identificadores.
As expressões que se referem a lugares de armazenamento modificáveis são
chamados l-values modificáveis. Estes não podem ser do tipo array, tipos
incompletos, nem tipos declarados com o atributo const. Para que as
estruturas e unions possam ser modificáveis, estas não podem ter nenhum dos
seus membros com o atributo const. O nome do identificador denota o lugar
de armazenamento, enquanto, que o valor da variável é o valor armazenado
naquela posição.
Um identificador é um valor l-value modificável. O mesmo identifica uma
posição de memória e o seu tipo, no caso, se for aritmético, union ou
ponteiro. Por exemplo, se ptr é um ponteiro para uma região de
208
armazenamento, então *ptr é um l-value modificável que designa a região de
armazenamento para o qual ptr aponta.
Qualquer uma das seguintes expressões em C pode ser uma expressão l-value:
Um identificador de tipo inteiro, float, ponteiro, estrutura ou union
Uma expressão entre colchetes ([ ]) que não é avaliada para um
array
Uma expressão de seleção de membro de estrutura ou union (-> ou
.)
Uma expressão de indireta unária (*) que não se refira a um array
Uma expressão l-value entre parênteses
Um objeto const (l-value não modificável)
O termo r-value é às vezes utilizado para descrever o valor de uma expressão
e para distingui-la de um l-value. Todos os l-values são r-values, mas o
recíproco nem sempre é verdadeiro.
9.3.7. Expressões Constantes em C
Uma expressão constante é avaliada em tempo de compilação, não no tempo
de execução, e pode ser utilizada em qualquer lugar em que uma constante
deva ser usada. As expressões constantes deverão resultar em valores que
estão na faixa de valores representáveis para um tipo determinado. Os
operandos de uma expressão constante podem ser constantes inteiras,
constantes caractere, constantes de ponto flutuante, casts, expressões sizeof e
outras expressões constantes.
Uma constante inteira deve ser usada para especificar o tamanho do campo de
bits de um membro de uma estrutura, o valor de uma constante de
enumeração, o tamanho de um array, ou o valor de uma constante case.
As expressões constantes usadas nas diretivas de pré-processador estão
sujeitas a restrições adicionais. Consequentemente, são conhecidas como
expressões constantes restringidas. Uma expressão constante restringida não
pode ter expressões sizeof, constantes de enumeração, cast de qualquer tipo,
nem constantes de ponto flutuante. Porém, elas podem conter expressões
constantes especiais predefinidas (identificadores).
9.3.8. Avaliação de Expressões em C
As expressões que envolvem atribuição, incremento unário, decremento
unário ou chamadas de função podem ter consequências incidentais das suas
avaliações (efeitos colaterais). Quando um ponto de sequência é encontrado,
tudo o que o precede, incluindo qualquer efeito colateral, se assegura que
tenha sido avaliado antes que a avaliação continue para o próximo ponto de
sequência.
209
Os efeitos colaterais são mudanças ocasionadas pela avaliação de uma
expressão. Esses efeitos acontecem quando o valor de uma variável é
mudado pela avaliação da expressão. Todas as operações de atribuição
possuem efeitos colaterais. As chamadas de função também podem criar
estes efeitos se mudam o valor de um item visível externamente, pela
designação direta ou indireta através de ponteiros.
Efeitos Colaterais
A ordem de avaliação das expressões é definida pela forma específica da
implementação do programador, exceto quando a linguagem garante uma
ordem particular de avaliação. Por exemplo, alguns efeitos colaterais
ocorrem na seguinte chamada de função:
add( i + 1, i = j + 2 );
myproc( getc(), getc() );
Os argumentos da chamada da função podem ser avaliados em qualquer
ordem. A expressão i + 1 pode ser avaliada antes de i = j + 2, ou vice-versa.
O resultado será diferente em cada caso. De modo análogo, na instrução
posterior, não é possível garantir quais caracteres serão passados para a
função myproc, se o primeiro ou o segundo[71].
Como os operadores unários de incremento e decremento envolvem
atribuições, tais operadores podem também causar efeitos colaterais como
mostra o seguinte exemplo:
x[i] = i++;
No exemplo, o valor de x que é modificado é imprevisível. O valor do
subscrito pode ser tanto o novo quanto o velho valor de i. O resultado pode
variar com compiladores diferentes ou diferentes níveis de otimização do
mesmo compilador.
Uma vez que a padronização da linguagem C não define a ordem de avaliação
dos efeitos colaterais, ambos os métodos de avaliação discutidos
anteriormente são corretos e ambos podem ser implementados. Para ter
certeza de que o código seja portável e claro, devem se evitar expressões que
dependam de uma ordem particular de avaliação que possa gerar efeitos
colaterais.9.3.9. Pontos de Sequência em C
Entre dois pontos de sequência consecutivos, o valor de um objeto pode ser
modificado somente por uma expressão. A linguagem C define os seguintes
pontos de sequência:
O operando do lado esquerdo de um operador AND lógico (&&). O
operando do lado esquerdo de um operador lógico AND é
completamente avaliado e todos os seus efeitos colaterais serão
completados antes de continuar. Se o operando do lado esquerdo foi
210
avaliado como falso (0), o outro operando não será avaliado.
O operando do lado esquerdo de um operador OR lógico (||). O
operando do lado esquerdo de um operador OR lógico é completamente
avaliado e seus efeitos colaterais completados antes de continuar. Caso
o operando do lado esquerdo seja avaliado como verdadeiro (não falso
ou diferente de zero), o outro operando não será avaliado.
O operando do lado esquerdo do operador (,). O operando do lado
esquerdo será completamente avaliado junto com seus efeitos colaterais
antes de continuar. Ambos operandos do operador vírgula serão
avaliados. Notar que o operador vírgula numa chamada de função, não
garante a ordem de avaliação.
Operador de chamada de função. Todos os argumentos de uma
função serão avaliados e todos os efeitos colaterais serão completados
antes da entrada na função. Nenhuma ordem de avaliação entre os
argumentos foi especificada.
Primeiro operando de um operador condicional. O primeiro
operando de um operador condicional será completamente avaliado e
seus efeitos colaterais completados antes de continuar.
O final de um expressão completa de inicialização (i.e., uma
expressão que não faz parte de outra expressão tal como a finalização de
uma inicialização em uma linha de declaração).
A expressão em uma linha de instrução. As expressões de instrução
consistem de uma expressão opcional seguida de um ponto e vírgula (;).
A expressão será avaliada para os seus efeitos colaterais e haverá um
ponto de sequência seguindo a avaliação.
A expressão de controle de um comando de seleção (if ou switch).
A expressão será completamente avaliada e todos os seus efeitos
colaterais completados antes que o código dependente desta expressão
seja executado.
A expressão de controle de um comando while ou do. A expressão
será completamente avaliada e todos os seus efeitos colaterais
completados antes que qualquer outra instrução na seguinte iteração seja
executada.
As três expressões de uma instrução for. As expressões serão
completamente avaliadas e todos os seus efeitos colaterais completados
antes que qualquer outra instrução na seguinte iteração seja executada.
A expressão num comando return. A expressão será completamente
avaliada e todos os seus efeitos colaterais completados antes que o
controle retorne á função requerente.
211
212
9.4. Modeladores (casts)
Um modelador é basicamente um conversor de tipo. Ele pode ser aplicado a
uma expressão ou a uma variável em particular. O cast força a expressão ao
tipo especificado, sem mudar o valor original das variáveis envolvidas. A
forma geral é:
(tipo) expressão
A expressão poderá também estar entre parênteses. Observar o exemplo
mostrado a seguir.
#include <stdio.h>
void main (void){
int num;
float f;
num=10;
f=(float)num/7;
printf ("%f",f);
}
Código 9-6
Se não fosse usado o cast no exemplo, o código gerado pelo compilador
executaria uma divisão inteira entre 10 e 7. O resultado seria 1 (um inteiro) e
este seria depois convertido para float para 1.0. Com o cast obtém-se o
resultado correto.
O tipo cast fornece um método de conversão explícita do tipo de um objeto
em uma situação específica.
A sintaxe pode se dar da seguinte forma:
( nome-do-tipo ) expressão-cast
Os compiladores tratam as expressões cast como um tipo (dado pelo nome do
tipo), depois de que a conversão tenha sido feita. Os casts podem ser usados
para converter objetos de um dado tipo escalar para qualquer outro tipo,
também escalar. Os casts são regidos pelas mesmas regras que determinam os
efeitos das conversões implícitas, discutidas anteriormente. Algumas
restrições adicionais nos casts podem resultar dos tamanhos atuais ou a
representação de certos tipos específicos.
9.4.1. Conversões Cast
As conversões tipo cast podem ser usadas para converter tipos de forma
explícita, com a seguinte sintaxe:
(nome-de-tipo) expressão-cast
O nome-do-tipo é o tipo e a expressão-cast é o valor a ser convertido para tal
tipo. Uma expressão com um tipo cast não é um l-value. A expressão-cast é
convertida como tem sido atribuída à variável do tipo nome-do-tipo. As
213
regras de conversão são as mesmas às das conversões implícitas. A tabela a
seguir mostra os tipos que podem ser modificados para qualquer outro [72].
Tipo Destino Fontes Potenciais
Tipos Inteiros Qualquer tipo inteiro, de ponto
flutuante, ou ponteiro para um objeto.
Ponto
Flutuante
Qualquer tipo aritmético.
Um ponteiro
para um
objeto, ou
ponteiro void
(void *)
Qualquer tipo inteiro, ponteiro void,
ponteiro para um objeto ou ponteiro
para função.
Ponteiro para
Função
Qualquer tipo inteiro, ponteiro para
um objeto ou ponteiro para função.
Estrutura,
Union ou
Array
Nenhum tipo
Tipo void Qualquer tipo
Tabela 9-8 - Conversões de tipos de dados
Qualquer identificador pode ser casteado para o tipo void. Assim, se o tipo
especificado na expressão não for do tipo void, então, o identificador a ser
casteado para este tipo não poderá ser uma expressão void. Qualquer
expressão pode ser casteada para void, mas uma expressão de tipo void não
pode ser casteada para qualquer outro tipo. Por exemplo, uma função com
tipo de retorno void, não pode ter seu retorno casteado para qualquer outro
tipo [73].
214
9.5. Operadores da Linguagem C
Existem três tipos de operadores. Uma expressão unária consiste de um
operador unário que atua sobre um operando, ou um comando sizeof seguido
de uma expressão. A expressão pode ser o nome de uma variável ou uma
expressão cast. Se a expressão é uma expressão cast, com fio visto na seção
9.4, deverá estar encerrada entre parênteses. Uma expressão binária consiste
de dois operandos unidos por um operador binário. Uma expressão ternária
consiste de três operandos ligados por um operador de expressão condicional.
A linguagem C implementa os seguintes operadores unários:
Símbolo Nome
– ~ ! Operadores de negação e complemento
* & Operadores indiretos e de endereçamento
sizeof Operador de tamanho
+ Operador plus unário
++ –– Operadores incrementador e decrementador
unários
Tabela 9-9 - Operadores unários
Os operadores binários associam-se da esquerda para a direita. A linguagem
C implementa os seguintes operadores binários:
Símbolo Nome
* / % Operadores de multiplicação,
divisão e resto da divisão
+ – Operadores de adição e subtração
<< >> Operadores de deslocamento
< > <= >= == != Operadores relacionais
& | ^ Operadores bit a bit
&& || Operadores lógicos
, Operadores de avaliação sequencial
Tabela 9-10 – Operadores binários
Os operadores de expressão condicionais têm precedência menor que as
expressões binárias e diferem em que são associados pela direita.
As expressões com operadores também incluem as expressões de atribuição,
que usam os operadores de atribuição unários ou binários. Os operadores de
atribuição unária são os operadores incremento (++) e o decremento (--); os
operadoresde atribuição são o operador simples de atribuição (=) e o
operador de atribuição composto. Cada operador de atribuição composto é
uma combinação de outro operador binário com o operador simples de
atribuição.
9.5.1. Precedência e Ordem de Avaliação
A precedência e associatividade dos operadores C afetam o agrupamento e a
215
avaliação dos operandos nas expressões. A precedência de um operador só é
significativa se outros operadores com precedência mais alta ou mais baixa
estão presentes. As expressões com maior ordem de precedência, serão
avaliadas primeiro. A precedência pode também ser entendida como uma
força de ligação entre operandos. Os operadores com precedência maior
podem ser entendidos como conectando os seus operandos mais fortemente.
A tabela a seguir resume a precedência e associação (a ordem na qual os
operandos são avaliados) dos operadores em C, listando-os na ordem de
precedência da mais alta, até a mais baixa. Quando muitos operadores são
colocados juntos, estes terão igual precedência e serão avaliados de acordo
com a sua associatividade. Os operadores da tabela serão descritos em
detalhes nas seções que seguem.
Precedência Símbolo[74] Tipo de
Operação
Associatividade
Maior
Precedência
[ ] ( ) . –> ++
postfixado e –
postfixado
Expressão Esquerda para
direita
++ prefixado
e -- prefixado
sizeof & * +
– ~ !
Unário Direita para
esquerda
Casts Unário Direita para
esquerda
* / % Multiplicação Esquerda para
direita
+ – Adição Esquerda para
direita
<< >> Deslocamento
de bits
Esquerda para
direita
< > <= >= Relacional Esquerda para
direita
== != Igualdade Esquerda para
direita
& AND entre
bits
Esquerda para
direita
^ XOR entre bits Esquerda para
direita
| OR entre bits Esquerda para
direita
&& AND lógico Esquerda para
direita
|| OR lógico Esquerda para
direita
? : Expressão
condicional
Direita para
esquerda
= *= /= %=
+= –= <<=
>>=
&= ^= |=
Atribuição
simples e
composta[75]
Direita para
esquerda
216
Menor
Precedência
, Avaliação
sequencial
Esquerda para
direita
Tabela 9-11 – Operadores por ordem de precedência
Somente os operadores de avaliação sequencial (,), AND lógico (&&), OR
lógico (||), de expressão condicional (? :) e o operador de chamada de função,
constituem pontos de sequência e garantem uma ordem particular de
avaliação dos seus operandos. O operador de chamada de função é o
conjunto de parênteses que seguem ao identificador da função. O operador de
avaliação sequencial (,) garante avaliar os seus operandos de esquerda para
direita[76].
Os operadores lógicos também garantem a avaliação dos seus operandos da
esquerda para direita. Porém, eles avaliam o menor número de operandos
necessários para determinar o resultado da expressão. Isso é chamado de
avaliação de “curto-circuito”. Dessa forma, alguns operandos de uma
expressão poderão não ser avaliados. Por exemplo, na expressão
x && y++
o segundo operando, y++, será avaliado somente se x for verdadeiro (não
zero). Assim, y não será incrementado caso x seja falso (0).
A Tabela 9-12 mostra como a maioria dos compiladores, automaticamente,
conecta algumas expressões simples:
Expressão Interpretação
automática
a & b || c (a & b) || c
a = b || c a = (b || c)
q && r || s-- (q && r) || s––
Tabela 9-12 - Exemplo de relacionamentos automáticos
Na primeira expressão, o operador AND entre bits (&) tem precedência maior
em comparação com o operador OR lógico (||), de forma que a & b forma o
primeiro operando da operação OR lógica.
Na segunda expressão, o operador lógico OR (||) tem precedência maior que o
operador simples de atribuição (=), de forma que b || c é agrupado como o
operando da direita na atribuição[77].
A terceira expressão mostra uma expressão corretamente formada que pode
produzir um resultado não esperado. O operador lógico AND (&&) tem
precedência sobre o operador OR lógico (||), de forma que N q && r seja
avaliada antes de s--. Porém, se q && r resulte num valor verdadeiro, s—
não será avaliada, e s não será decrementada. Se não houver o decremento,
poderá ocasionar um problema no programa, sendo que s—deverá aparecer
como o primeiro operando da expressão, ou s deverá ser decrementada em
uma operação separada.
217
A seguinte operação é incorreta e produzirá uma mensagem de diagnóstico
em tempo de compilação:
Expressão incorreta Agrupamento por default
P == 0 ? p += 1: p += 2 ( p == 0 ? p += 1 : p ) += 2
Nesta expressão, o operador de igualdade (==) tem a mais alta precedência,
de forma que p == 0 é agrupado como um operando. O operador de
expressão condicional (? :) possui a próxima mais alta precedência. O seu
primeiro operando é p == 0, e o seu segundo operando é p += 1. Porém, o
último operando do operador de expressão condicional é considerado sendo p
no lugar de p += 2, uma vez que a ocorrência de p conecta com força maior
ao operador de expressão condicional que ao operador de atribuição
composto.
Um erro de sintaxe deverá ocorrer porque += 2 não possui operando de lado
esquerdo. Para evitar este tipo de problemas e produzir um código mais claro
devem ser usados parênteses. Por exemplo:
( p == 0 ) ? ( p += 1 ) : ( p += 2 )
9.5.2. Conversões Aritméticas Usuais
A maioria dos operadores C efetua conversões de tipos, para deixar os
operandos de uma expressão num tipo comum ou para estender os valores
short para o tamanho do inteiro, usado pelo microprocessador em questão.
As conversões efetuadas pelos operadores dependem de cada operador e do
tipo de operando ou operandos. Porém, muitos operadores efetuam
conversões similares em operandos do tipo inteiro ou ponto flutuante. As
conversões de um valor de operando para um tipo compatível, não ocasionam
mudanças no seu valor original.
As conversões aritméticas mais comuns são resumidas a seguir. Estes passos
são aplicados frequentemente pelos operadores binários que esperam tipos
aritméticos e somente se os dois operandos não são do mesmo tipo. O
propósito é deixar os valores num tipo comum que também é do mesmo tipo
que o resultado. Para determinar que conversões serão efetuadas, os
compiladores aplicam, em geral, o seguinte algoritmo para as operações
binárias em expressões. Os passos a seguir não têm ordem de precedência.
Se um dos operandos é do tipo long double, o outro será convertido
para long double.
Se a condição acima não é verdadeira e um dos operandos for do
tipo double, o outro operando será convertido no tipo double.
Se as condições acima não correspondem, e um dos operandos for
do tipo float, o outro operando será convertido ao tipo float.
Se as três condições acima não correspondem (nenhum dos
operandos são do tipo ponto flutuante) então serão executadas as
218
conversões inteiras nos operandos como segue:
Se um dos operandos for do tipo unsigned long, o segundo
operando será convertido para este tipo.
Se a condição acima não for verdadeira, e um dos operandos
for do tipo long e o outro do tipo unsigned int, ambos operandos
serão convertidos para o tipo unsigned long.
Se as duas condições acima não forem verdadeiras, e um dos
operandos for do tipo long, o outro será convertido no tipo long.
Se nenhuma das três condições anterior for verdadeira, e um
dos operandos for do tipo unsigned int, o outro operando será
convertidono mesmo tipo.
Se nenhuma das condições acima for verdadeira, ambos
operandos serão convertidos ao tipo int.
O código que segue mostra as regras de conversão:
float fVal;
double dVal;
int iVal;
unsigned long ulVal;
dVal = iVal * ulVal; /* iVal convertida para unsigned long
* passo 4.
* Resultado da multiplicação convertido em double
*/
dVal = ulVal + fVal; /* ulVal convertida para float
* passo 3.
* Resultado da adição convertida em double
*/
9.5.3. Operadores Postfixados
Os operadores postfixados têm a mais alta precedência (maior força de
ligação) na avaliação de expressões.
Os operadores neste nível de precedência são os subscritos de array,
chamadas de função, membros de unions e estruturas, e os operadores de
incremento e decremento postfixado.
Arrays de uma Dimensão
Uma expressão postfixada, seguida por uma outra encerrada entre colchetes ([
]) é uma representação subscrita de um elemento de um objeto do tipo array.
A expressão subscrita representa o valor para o endereço que a expressão
representa, da forma:
expressão postfixada [ expressão ]
Usualmente, o valor representado pela expressão postfixada é um valor de
ponteiro, tal como um identificador de array, e a expressão é um valor inteiro.
Porém, tudo o que é requerido sintaticamente é que uma das expressões seja
do tipo ponteiro e a outra do tipo inteiro, independente da ordem. Assim, o
219
valor inteiro pode estar também como expressão postfixada e o valor do
ponteiro pode estar entre os colchetes na expressão (ou expressão subscrita).
Por exemplo, o seguinte código é válido:
int sum, *ptr, a[10];
void main() {
ptr = a;
a[4] = 9;
sum = 4[ptr]; /* mesmo que sum = ptr[4] ou sum = a[4] */
}
Código 9-7
O resultado na variável sum será o valor 9.
As expressões subscritas são geralmente usadas para referenciar os elementos
de um array, mas pode também ser aplicado o subscrito para qualquer
ponteiro. Qualquer que seja a ordem de colocação dos valores, a expressão
deve ser encerrada entre colchetes [78].
A expressão subscrita é avaliada pela adição do valor inteiro ao valor do
ponteiro, e então, é aplicado o operador indireto (*) ao resultado [79]. De
fato, para um array de uma dimensão, as seguintes quatro expressões são
equivalentes, assumindo que a é um ponteiro e b é um inteiro:
a[b]
*(a + b)
*(b + a)
b[a]
Ver o seguinte exemplo de código:
void main(void){
int a[10],b=3,res1,res2, res3,res4;
a[b] = 9;
res1 = a[b];
res2 = *(a + b);
res3 = *(b + a);
res4 = b[a];
}
Código 9-8
O resultado nas variáveis res1, res2, res3 e res4, é igual a 9.
De acordo com as regras de conversão para o operador de adição [80], o valor
inteiro é convertido para um offset de endereço, multiplicando-o pelo
comprimento (em bytes) do tipo de dado endereçado pelo ponteiro.
Por exemplo, suponha que o identificador line se refere a um array de valores
inteiros. O seguinte procedimento será usado para avaliar a expressão
subscrita line[i]:
O valor inteiro i é multiplicado pelo número de bytes definido como
sendo o tamanho de um item int. O valor convertido de i representa a
220
posição do inteiro referenciado por i.
O valor convertido é adicionado ao do ponteiro original (line), para
encontrar o endereço cujo offset é de i posições do tipo int a partir da
posição origem line
O operador indireto será então aplicado ao novo endereço. O
resultado é o valor do elemento do array na posição indicada.
A expressão subscrita line[0] representa o valor do primeiro elemento de line
já que o offset a partir do endereço representado por line é 0. Similarmente,
uma expressão tal como line[5] se refere ao elemento que possui um offset de
5 posições a partir de line, ou seja, o sexto elemento do array. Por exemplo,
se o sistema comporta valores int de 2 bytes, line[5] terá um offset de 5 x 2
bytes = 10 bytes a partir de line (ou line[0]).
Arrays Multidimensionais
A expressão subscrita pode também ter múltiplos subscritos, como segue:
expressão1 [expressão2] [expressão3]...
As expressões subscritas associam de esquerda para direita. A expressão
subscrita mais à esquerda, expressão1 [expressão2], é avaliada primeiro. O
endereço resultante da adição de expressão1 e expressão2 forma uma
expressão ponteiro; então expressão3 é somado à expressão ponteiro para
formar uma nova expressão ponteiro, e assim sucessivamente, até que a
última expressão subscrita tenha sido adicionada. Finalmente, é aplicado o
operador indireto (*), a menos que o valor final do ponteiro esteja
endereçando um tipo array (ver exemplos a seguir).
As expressões com múltiplos subscritos se referenciam a elementos de arrays
multidimensionais. Um array multidimensional é um array cujos elementos
são arrays. Por exemplo, o primeiro elemento de um array tridimensional é
um array de duas dimensões.
Exemplos
Nos seguintes exemplos, foi declarado um array chamado prop com três
elementos, cada um destes sendo um array 4 x 6 de valores int.
int prop[3][4][6];
int i, *ip, (*ipp)[6];
Uma referência a um elemento do array prop pode ser da seguinte forma:
i = prop[0][0][1];
O exemplo a seguir mostra como referenciar o segundo elemento int de
prop. Os arrays são armazenados por linha, de forma que o último subscrito
varia mais rapidamente, de modo que a expressão prop[0][0][2] se referencia
ao próximo (terceiro) elemento do array, e assim, sucessivamente.
i = prop[2][1][3];
A linha acima é uma referência um pouco mais complicada para um elemento
221
individual de prop. A expressão será avaliada como segue:
1. O primeiro subscrito 2, é multiplicado pelo tamanho do array 4x6 de
int e adicionado ao valor do ponteiro prop. O resultado aponta para
o terceiro array 4x6 de prop.
2. O segundo subscrito, 1, é multiplicado pelo tamanho do array de
ints de 6 elementos e então adicionado ao endereço representado por
prop[2].
3. Cada elemento do array de 6 elementos é um valor do tipo int, de
forma que o subscrito 3, é multiplicado pelo tamanho de um int
antes, e então adicionado a prop[2][1]. O ponteiro resultante
endereça o quarto elemento de um array de 6 elementos.
4. Finalmente, é aplicado o operador indireto ao valor do ponteiro
resultante. O resultado é o elemento inteiro armazenado naquele
endereço.
Os próximos dois exemplos mostram casos onde o operador indireto não é
aplicado.
ip = prop[2][1];
ipp = prop[2];
Na primeira instrução, a expressão prop[2][1] é uma referência válida para o
array tridimensional prop; ela se refere a um array de 6 elementos (declarados
no exemplo anterior). Uma vez que o valor do ponteiro endereça um array, o
operador indireto não será aplicado.
Similarmente, o resultado da expressão prop[2] na segunda instrução ipp =
prop[2] onde o resultado é um valor de ponteiro endereçando um array
bidimensional.
Chamadas de Função
Uma chamada de função é uma expressão que inclui o nome da função que
está sendo chamada ou o valor de um ponteiro para a função e,
opcionalmente, os argumentos que estão sendo passados para esta.
Uma expressão de chamada de função tem o valor e o tipo do valor de retorno
da mesma. Uma função não pode retornar um objeto do tipo array. Se o tipo
de retorno da função é void (i.e., a função foi declarada para não retornar
valores), a expressão da chamada de função também será do tipo void.
Membros de Estruturase Unions[81]
Uma expressão de seleção de membro refere-se aos membros de estruturas e
unions. Tal expressão possui o valor e o tipo do membro selecionado.
A sintaxe tem duas formas possíveis:
expressão-postfixada . identificador
expressão-postfixada –> identificador
Na primeira forma a expressão postfixada representa o valor de um tipo
222
estrutura ou union, e o identificador seleciona um determinado membro da
estrutura ou union em questão. O valor da operação é o do identificador que é
um l-value se a expressão postfixada é um l-value [82].
Na segunda forma, a expressão postfixada representa um ponteiro para uma
estrutura ou union, e o identificador seleciona o membro da mesma. O valor
é o do identificador e é um l-value. As duas formas de expressões de seleção
de membros possuem efeitos similares.
De fato, uma expressão envolvendo o operador de seleção de membro (->) é
uma versão simplificada da expressão que usa o operador ponto (.) se a
expressão anterior ao operador ponto consiste de um operador indireto (*)
aplicado ao valor de um ponteiro. Assim,
expressão –> identificador
é equivalente a
(*expressão) . identificador
quando a expressão é um valor ponteiro.
Exemplos
Os seguintes exemplos se referenciam a uma declaração de estrutura [83].
struct pair {
int a;
int b;
struct pair *sp;
} item, list[10];
Uma expressão de seleção de membro para a estrutura item pode aparecer
como:
item.sp = &item;
No exemplo acima, o endereço da estrutura item é atribuído ao membro sp da
mesma estrutura. Isto permite que item contenha um ponteiro para si mesma.
(item.sp)–>a = 24;
No exemplo, a expressão ponteiro item.sp é usada com o operador de seleção
de membro (->) para atribuir um valor ao membro a.
list[8].b = 12;
Esta instrução mostra como selecionar um membro de uma estrutura
individual de um array de estruturas.
Operadores de Incremento e Decremento Postfixados
Os operadores de incremento e decremento postfixado são tipos escalares que
implementam l-values modificáveis.
A sintaxe básica pode ser das seguintes formas:
Expressão-postfixada ++
Expressão-postfixada --
O resultado da operação de incremento ou decremento é o valor do operando.
223
Depois que o resultado é obtido, o valor do operando é incrementado (ou
decrementado). O código que segue mostra o operador de incremento
postfixado.
if( var++ > 0 )
*p++ = *q++;
No exemplo, a variável var é comparada com 0, depois é incrementada. Se
var for positiva antes de ser incrementada, a próxima instrução será
executada. Primeiro, o valor do objeto apontado por q é atribuído ao objeto
apontado por p. Logo, então q e p serão incrementados.
9.5.4. Operadores Unários em C
Os operadores unários aparecem antes que o operando e associam de direita a
esquerda. Os operadores unários são: & * + – ~ !.
As formas de utilização podem ser como segue:
++ expressão-unária
-- expressão-unária
operador-unário expressão-cast
sizeof expressão-unária
sizeof ( tipo )
Operadores e Incremento e Decremento Prefixados
Os operadores unários de incremento e decremento (++ e --) são chamados de
prefixados quando aparecem antes do operando. O incremento e decremento
postfixado tem uma precedência maior que o prefixado. O operando deve ser
do tipo inteiro, ponto flutuante ou ponteiro, e deve ser uma expressão l-value
modificável (uma expressão sem o atributo const). O resultado será um l-
value.
Quando o operador aparece antes do operando, o operando é incrementado ou
decrementado e seu novo valor é o resultado da expressão.
Um operando do tipo inteiro ou ponto flutuante é incrementado ou
decrementado pelo valor inteiro 1. O tipo do resultado é do mesmo tipo do
operando. Um operando do tipo ponteiro é incrementado ou decrementado
pelo tamanho do objeto que ele endereça [84]. Um ponteiro incrementado
aponta para o próximo objeto, e um ponteiro decrementado aponta para o
objeto anterior.
O exemplo a seguir ilustra o funcionamento do operador unário
decrementador prefixado:
if( line[--i] != '\n' )
return;
No exemplo, a variável i é decrementada antes de se utilizada como um
subscrito para line.
Operadores Indiretos e de Endereçamento
224
O operador indireto (*) acessa um valor de forma indireta através de um
ponteiro. O operando deve ser um valor do tipo ponteiro. O resultado da
operação é um valor endereçado pelo operando, isto é, o valor no endereço
para o qual o operando aponta. O tipo de resultado é o tipo que o operando
endereça.
Se o operando aponta para uma função, o resultado é um designador de
função. Aponta-se para uma posição de armazenamento, o resultado é um l-
value designando o local de armazenamento.
Se o valor do ponteiro for inválido, o resultado será indefinido. A lista que
segue inclui algumas das condições mais comuns que podem invalidar o valor
de um ponteiro.
O ponteiro é um ponteiro null.
O ponteiro especifica o endereço de um elemento local que não é
visível no momento da referência (fora do escopo).
O ponteiro especifica um endereço que está inapropriadamente
alinhado para o tipo de objeto apontado.
O ponteiro especifica um endereço que não é utilizado pelo
programa em execução.
O operador de endereço (&) fornece o endereço do operando. O operando de
um operador de endereço pode ser um designador de função ou um l-value,
que designa um objeto que não é pode ser um campo de bit nem um objeto
declarado com o especificador de classe de armazenamento register.
O resultado da operação de endereçamento é um ponteiro para o operando. O
tipo endereçado pelo ponteiro é do mesmo tipo do operando.
O operador de endereçamento pode ser somente aplicado à variáveis dos tipos
fundamentais, estruturas ou unions que foram declarados no nível de escopo
de arquivo, ou em referências subscritas de arrays. Nessas expressões, uma
expressão constante que não inclui o endereço do operador, pode ser
adicionada ou subtraída da expressão do endereço.
Os seguintes exemplos mostram estas declarações:
int *pa, x;
int a[20];
double d;
A seguinte instrução utiliza o operador de endereço:
pa = &a[5];
O operador de endereço (&) fornece o endereço do sexto elemento do array
a. O resultado é armazenado na variável ponteiro pa.
x = *pa;
O operador indireto (*) é utilizado no exemplo para acessar o valor inteiro
armazenado no endereço apontado por pa. O valor é, então, atribuído à
225
variável inteira x.
if( x == *&x )
printf( "True\n" );
Este exemplo imprime a palavra TRUE, demonstrando que o resultado da
aplicação do operador indireto no endereço de x é o mesmo que x.
int roundup( void ); /* Declaração de Função */
int (*proundup)(void) = roundup;
int (*pround)(void) = &roundup;
Depois que a função roundup é declarada, dois ponteiros para a função são
declarados e inicializados. O primeiro ponteiro proundup é inicializado
usando somente o nome da função, enquanto que o segundo, pround, usa o
operador de endereço para a inicialização. Ambas inicializações são
equivalentes.
Analise o seguinte código[85]:
int roundup( void ); /* Declaração de Função */
int (*proundup)(void) = roundup;
int (*pround)(void) = &roundup;
void main() {
(*proundup)(); /* mesmo efeito que roundup() */
(*pround)(); /* mesmo efeito que roundup() */
}
roundup(void){
int x = 2;
x++;
return x;
}
Código 9-9
Operadores Aritméticos Unários
Os operadores plus unário, negação aritmética (inversão de sinal),
complemento e negação lógica serão discutidos nas linhas que