Logo Passei Direto
Buscar
Material
páginas com resultados encontrados.
páginas com resultados encontrados.
left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

left-side-bubbles-backgroundright-side-bubbles-background

Experimente o Premium!star struck emoji

Acesse conteúdos dessa e de diversas outras disciplinas.

Libere conteúdos
sem pagar

Ajude estudantes e ganhe conteúdos liberados!

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

Mais conteúdos dessa disciplina