Você está na página 1de 205

Brian W.

Kernighan
Rob Pike

A Pr�tica da Programa��o

Consultores Editoriais
Fernando Barcellos Ximenes
KPMG Consulting

Roberto lerusalimschy
Professor Associado,
Departamento de Inform�tica da PUC/Rio

Tradu��o
K�tia Roque

Do original:
The Pract�ce oi Programming

Tradu��o autorizada do idioma ingl�s da edi��o publicada por Addison-Wesley


Copyright (c)1999 by Lucent Technologies (c) 2000, Editora Compus Ltda.

Capa
Claudia Lopes Mendes
Editora��o EJefr�n�ca Domus Design Gr�fico
Revis�o Gr�fica Deborah Rudiger Roberto Mauro Facce
Projeto Gr�fico
Editora Compus Ltda.
A Qualidade da Informa��o
Rua Sete de Setembro, 111-16� andar
20050-002 Rio de Janeiro RJ Brasil
Telefone: (021) 509-5340 FAX (021) 507-1991
e-mail: info@campus.com.br

ISBN 85-352-0546-2
(Edi��o original: ISBN 0-201-61586-X)

CIP-Brasil. Cataloga��o-na-fonte.
Sindicato Nacional dos Editores de Livros, RJ
K47p
Kernighan, Brian W.
A pr�tica da programa��o / Brian W. Kernighan ; tradu��o de K�tia A. Roque. - Rio
de Janeiro: Compus, 2000
Tradu��o de: The practice of programming
Ap�ndice
ISBN 85-352-0546-2
1. Programa��o (Computadores). 2. Linguagens de programa��o (Computadores). I.
Pike, Rob. II. T�tulo.
99-1796 CDD
005.1
CDU 004.42

Pref�cio

Voc� j�...
perdeu muito tempo codificando o algoritmo errado?
usou uma estrutura de dados complicada demais?
testou um programa mas n�o notou um problema �bvio?
perdeu o dia inteiro procurando um bug que deveria ter encontrado em cinco minutos?
precisou fazer um programa rodar tr�s vezes mais r�pido e usar menos mem�ria?
brigou para mover um programa de uma esta��o de trabalho para um PC ou vice-versa?
tentou fazer uma altera��o pequena no programa de outra pessoa?
reescreveu um programa porque n�o conseguiu entend�-lo?
Isso foi divertido?

Essas coisas acontecem todo o tempo para os programadores. Mas lidar com
esses problemas quase sempre � mais dif�cil do que deveria ser porque t�picos como
testar, depurar, portabilidade, desempenho, alternativas de projeto e estilo - a
pr�tica da programa��o - geralmente n�o s�o o foco da ci�ncia da computa��o ou dos
cursos de programa��o. A maioria dos programadores aprende esses t�picos
casualmente � medida que ganha experi�ncia, e alguns nunca os aprendem.
Em um mundo de interfaces enormes e complicadas, em que ferramentas,
linguagens e sistemas est�o sempre mudando, com a implac�vel press�o para se ter
mais de tudo, � poss�vel perder de vista os princ�pios b�sicos - simplicidade,
clareza, generalidade - que formam a base do bom software. � poss�vel esquecer o
valor de ferramentas e nota��es que mecanizam parte da cria��o de software,
colocando assim o computador em sua pr�pria programa��o.
Nossa abordagem neste livro se baseia nesses princ�pios b�sicos e
interrelacionados, os quais se aplicam a todos os n�veis da programa��o. Eles
incluem simplicidade, que mant�m o programa curto e gerenci�vel; clareza, que
garante que eles sejam f�ceis de entender, tanto para as pessoas quanto para as
m�quinas; generalidade, para que eles funcionem bem em uma ampla variedade de
situa��es e se adaptem bem �s situa��es novas que surgem; e automa��o, que permite
� m�quina fazer o trabalho por n�s, liberando-nos das tarefas mundanas. Olhando a
programa��o de computador em uma variedade de linguagens, desde os algoritmos e as
estruturas de dados at� o projeto, a depura��o, o teste e a melhoria do desempenho,
podemos ilustrar conceitos universais da engenharia que n�o dependem de linguagem,
sistema operacional ou paradigma de programa��o.

Este livro surgiu de muitos anos de experi�ncia escrevendo e atualizando


software, do ensino em cursos de programa��o e do trabalho com uma ampla variedade
de programadores. Queremos compartilhar li��es sobre quest�es pr�ticas, passar o
conhecimento adquirido com nossa experi�ncia e sugerir a programadores de todos os
n�veis maneiras de serem mais proficientes e produtivos.
Escrevemos para v�rios tipos de leitores. Se voc� foi aluno de um ou dois
cursos de programa��o e gostaria de ser um programador melhor, este livro vai
expandir alguns dos t�picos para os quais n�o houve tempo suficiente na escola. Se
voc� escreve programas em seu trabalho, mas para ajudar em outras atividades e n�o
com o objetivo �nico da programa��o, as informa��es o ajudar�o a programar mais
efetivamente. Se voc� � um programador profissional que n�o teve muita exposi��o a
esses t�picos na escola, ou gostaria de relembr�-los, ou se � um gerente de
software que deseja orientar sua equipe na dire��o certa, este material pode ser
valioso.
Esperamos que este livro o ajude a escrever programas melhores. O �nico pr�-
requisito � que voc� j� tenha programado, de prefer�ncia em C, C++ ou Java. � claro
que quanto mais experi�ncia tiver, mais f�cil ser�. Nada pode transform�-lo de
novato a especialista em 21 dias. Os programadores de Unix e Linux achar�o alguns
dos exemplos mais familiares do que aqueles que s� usaram os sistemas Windows e
Macintosh, mas os programadores de todos os ambientes descobrir�o coisas para
facilitar suas vidas.

A apresenta��o est� organizada em nove cap�tulos, cada um focalizando um


aspecto importante da pr�tica de programa��o.
O Cap�tulo l discute o estilo de programa��o. O bom estilo � t�o importante
para a boa programa��o que escolhemos abord�-lo em primeiro lugar. Os programas bem
escritos s�o melhores do que aqueles mal escritos - eles t�m menos erros e s�o mais
f�ceis de depurar e modificar - portanto, � importante pensar no estilo desde o
in�cio. Este cap�tulo tamb�m apresenta um tema importante para a boa programa��o, o
uso dos idiomas apropriados � linguagem usada.
Os algoritmos e as estruturas de dados, t�picos do Cap�tulo 2, s�o a parte
principal do curr�culo da ci�ncia da computa��o e parte importante dos cursos de
programa��o. Como a maioria dos leitores j� est� familiarizada com esse material,
nosso tratamento ser� uma breve revis�o de meia d�zia de algoritmos e estruturas de
dados que aparecem em quase todos os programas. Os algoritmos e as estruturas de
dados mais complexos geralmente evoluem dessa base; portanto, � preciso dominar os
fundamentos.
O Cap�tulo 3 descreve o projeto e a implementa��o de um programa pequeno que
ilustra as quest�es de algoritmo e estrutura de dados em uma situa��o real. O
programa � implementado em cinco idiomas. A compara��o das vers�es mostra como as
mesmas estruturas de dados s�o tratadas em cada uma, e como a express�o e o
desempenho variam atrav�s dos v�rios idiomas.
As interfaces entre usu�rios, programas e partes de programas s�o
fundamentais na programa��o, e grande parte do sucesso do software � determinada
pelo modo como as interfaces s�o criadas e implementadas. O Cap�tulo 4 mostra a
evolu��o de uma pequena biblioteca para analisar um formato de dados muito usado.
Embora o exemplo seja pequeno, ele ilustra muitas das quest�es do projeto de
interface: abstra��o, ocultamento de informa��es, gerenciamento de recursos e
tratamento de erros.
Por mais que tentemos escrever os programas corretamente, os bugs e,
portanto, a depura��o s�o inevit�veis. O Cap�tulo 5 traz estrat�gias e t�ticas para
a depura��o sistem�tica e efetiva. Entre os t�picos est�o as assinaturas de bugs
comuns e a import�ncia da "numerologia", em que os padr�es na sa�da da depura��o
quase sempre indicam onde est� um problema.
O teste � uma tentativa de desenvolver uma garantia razo�vel de que um
programa est� funcionando corretamente e de que ele permane�a correto durante sua
evolu��o. A �nfase do Cap�tulo 6 � o teste sistem�tico � m�o e � m�quina. Os testes
de condi��o limite verificam os pontos fracos em potencial. A mecaniza��o e os
"andaimes" de teste tornam f�cil testar de forma extensa com pouco esfor�o. Os
testes de carga fornecem um tipo diferente de teste daquele com o qual os usu�rios
est�o acostumados e investigam uma classe diferente de bugs.
Os computadores s�o t�o r�pidos e os compiladores, t�o bons que muitos
programas s�o suficientemente r�pidos j� no dia em que s�o escritos. Mas outros s�o
lentos demais, ou usam muita mem�ria, ou ambos. O Cap�tulo 7 aborda ordenadamente a
tarefa de fazer com que um programa use os recursos de maneira eficiente, para que
ele permane�a correto e s�lido quando se tornar mais eficiente.
O Cap�tulo 8 aborda a portabilidade. Os programas bem-sucedidos vivem o
suficiente para seu ambiente mudar, ou precisam ser movidos para sistemas, hardware
ou pa�ses novos. O objetivo da portabilidade � reduzir a manuten��o de um programa,
minimizando a quantidade necess�ria de altera��es para adapt�-lo a um novo
ambiente.
A computa��o � rica em linguagens, n�o apenas aquelas de prop�sitos gerais
que usamos para a maior parte da programa��o, como tamb�m muitas linguagens
especializadas que focalizam dom�nios estreitos. O Cap�tulo 9 apresenta v�rios
exemplos da import�ncia da nota��o na computa��o, e mostra como podemos us�-la para
simplificar os programas, orientar implementa��es e at� mesmo nos ajudar a escrever
programas que escrevem programas.
Para falar sobre programa��o precisamos mostrar muito c�digo. A maioria dos
exemplos foi escrita expressamente para o livro, embora alguns exemplos menores
tenham sido adaptados de outras fontes. Tentamos escrever nosso pr�prio c�digo
tamb�m, e o testamos em meia d�zia de sistemas diretamente do texto lido por
m�quina. Mais informa��es podem ser obtidas no site da Web do livro em:
http://tpop.awl.com

A maioria dos programas est� em C, com alguns exemplos em C++, Java e breves
incurs�es pelas linguagens de cria��o de scripts. Em seu n�vel mais inferior, C e
C++ s�o quase id�nticas e nossos programas em C tamb�m s�o programas C++ v�lidos.
C++ e Java s�o descendentes diretos da C, compartilhando mais do que a sintaxe e
muito de sua efici�ncia e express�o, adicionando sistemas de tipo e bibliotecas
mais ricos. Em nosso pr�prio trabalho usamos rotineiramente todas essas tr�s
linguagens, al�m de muitas outras. A op��o pela linguagem depende do problema: para
os sistemas operacionais � melhor uma linguagem eficiente e sem restri��es como C
ou C++, prot�tipos r�pidos s�o mais f�ceis em um int�rprete de comandos ou em uma
linguagem de cria��o de scripts como Awk ou Perl. Para as interfaces de usu�rio o
Visual Basic e Tcl/Tk s�o fortes concorrentes, juntamente com a Java.
H� uma importante quest�o pedag�gica na escolha da linguagem de nossos
exemplos. Assim como nenhuma linguagem soluciona todos os problemas igualmente bem,
nenhuma �nica linguagem � melhor para apresentar todos os t�picos. As linguagens de
n�vel mais alto exigem algumas decis�es de projeto. Se usarmos uma linguagem de
n�vel mais inferior precisamos considerar respostas alternativas para as perguntas.
Expondo mais detalhes, podemos falar melhor sobre elas. A experi�ncia mostra que,
mesmo quando usamos os recursos das linguagens de n�vel alto, � preciso saber como
elas se relacionam com as quest�es de n�vel inferior. Sem essa vis�o podemos ter
problemas de desempenho e comportamento misterioso. Assim sendo, quase sempre
usaremos C em nossos exemplos, muito embora na pr�tica possamos escolher algo
diferente.
Em sua maior parte, por�m, as li��es n�o dependem de nenhuma linguagem de
programa��o em particular. A op��o pela estrutura de dados � afetada pela linguagem
que temos � nossa disposi��o. Pode haver menos op��es em algumas linguagens,
enquanto outras podem suportar uma variedade de alternativas. Mas a maneira como
escolhemos a op��o ser� a mesma. Os detalhes de como testar e depurar s�o
diferentes nas v�rias linguagens, mas as estrat�gias e t�ticas s�o semelhantes em
todas. A maioria das t�cnicas para tornar um programa eficiente pode ser aplicada a
qualquer linguagem.
Independente da linguagem usada, sua tarefa como programador � fazer o
melhor poss�vel com as ferramentas dispon�veis. Um bom programador pode superar uma
linguagem ruim ou um sistema operacional desajeitado, mas nem mesmo um �timo
ambiente de programa��o poder� salvar um programador ruim. Esperamos que,
independente da sua experi�ncia e habilidade atuais, este livro o ajude a programar
melhor e gostar mais de programar.

Somos profundamente gratos a amigos e colegas que leram rascunhos do


manuscrito deste livro e fizeram muitos coment�rios �teis. Jon Bentley, Russ Cox,
John Lakos, John Linderman, Peter Memishian, lan Lance Taylor, Howard Trickey e
Chris Van Wyk leram o manuscrito, alguns deles mais de uma vez, com cuidado e
aten��o excepcionais. Agradecemos a Tom Cargill, Chris Cleeland, Steve Dewhurst,
Eric Grosse, Andrew Herron, Gerard Holzmann, Doug Mcllroy, Paul McNamee, Peter
Nelson, Dennis Ritchie, Rich Stevens, Tom Szymanski, Kentaro Toyama, John Wait,
Daniel C. Wang, Peter Weinberger, Margaret Wright e Cliff Young pelos coment�rios
valiosos sobre os rascunhos em seus diversos est�gios. Tamb�m apreciamos os bons
conselhos e sugest�es valiosas de Al Aho, Ken Arnold, Chuck Bigelow, Joshua Bloch,
Bill Coughran, Bob Flandrena, Ren�e French. Mark Kernighan, Andy Koenig, Sap�
Mullender, Evi Nemeth, Marty Rabinowitz, Mark V. Shaney, Bjarne Stroustrup, Ken
Thompson e Phil Wadler. Obrigado a todos.
Brian W. Kernighan/Rob Pike

Estilo
H� muito se diz que os melhores autores �s vezes ignoram as regras da ret�rica.
Quando eles fazem isso, por�m, o leitor geralmente encontra na senten�a algum
m�rito compensador, conseguido �s custas da viola��o. A menos que ele tenha certeza
de fazer isso bem, � melhor seguir as regras.
William Strunk e E. B. White, The Elements of Style

Este fragmento de c�digo vem de um programa grande que foi escrito h� muitos
anos:

if ( (country == SING) || (country == BRNI) ||


(country == POL) || (country == ITALY) )
{
/*
* Se o pa�s for Cingapura, Brunei ou Pol�nia
* ent�o a hora atual � a hora de resposta
* em vez da hora livre.
* Redefinir hora de resposta e definir dia da semana.
*/

Ele foi escrito, formatado e comentado com cuidado, e o programa de onde ele
vem funciona extremamente bem. Os programadores que criaram esse sistema t�m
orgulho, com toda raz�o, daquilo que constru�ram. Mas este trecho � intrigante para
o leitor eventual. Qual � o relacionamento que existe entre Cingapura, Brunei,
Pol�nia e It�lia? Por que a It�lia n�o � mencionada no coment�rio? Como coment�rio
e c�digo s�o diferentes, um deles deve estar errado. Talvez ambos estejam. O c�digo
� aquilo que � executado e testado; portanto, � mais prov�vel que ele esteja certo.
Talvez o coment�rio n�o tenha sido atualizado junto com o c�digo. O coment�rio n�o
diz o suficiente sobre o relacionamento entre os tr�s pa�ses mencionados. Se voc�
tivesse de atualizar este c�digo, precisaria saber mais.
As poucas linhas acima s�o t�picas da maioria do c�digo real: em sua grande
parte s�o bem feitos, mas existem algumas coisas que poderiam ser aperfei�oadas.
Este livro fala da pr�tica de programa��o - como escrever programas reais.
Nosso prop�sito � ajud�-lo a escrever software que funciona pelo menos t�o bem
quanto o programa do qual esse exemplo foi tirado, evitando pontos problem�ticos e
fracos. Falaremos sobre como escrever c�digo melhor desde o in�cio e melhor�-lo,
conforme ele evolui.
Vamos come�ar em um lugar pouco comum, discutindo o estilo de programa��o. O
prop�sito do estilo � facilitar a leitura do c�digo para voc� e para as outras
pessoas, e o bom estilo � crucial para a boa programa��o. Queremos falar primeiro
sobre isso, para que voc� fique atento quando ler o c�digo restante deste livro.
H� mais coisas na escrita de um programa do que sintaxe correta, consertar
bugs e fazer com que ele rode suficientemente r�pido. Os programas s�o lidos n�o
apenas pelos computadores, mas tamb�m pelos programadores. Um programa bem escrito
� mais f�cil de entender e modificar do que um programa mal escrito. A disciplina
de escrever bem leva a c�digo que muito provavelmente estar� correto. Felizmente,
essa n�o � uma disciplina dif�cil.
Os princ�pios do estilo de programa��o se baseiam no bom senso guiado pela
experi�ncia, n�o em regras e prescri��es arbitr�rias. O c�digo deve ser claro e
simples - l�gica direta, express�o natural, uso convencional da linguagem, nomes
significativos, formata��o simples, coment�rios �teis - e deve evitar tru-ques
espertos e constru��es incomuns. A consist�ncia � importante porque as outras
pessoas achar�o mais f�cil ler o seu c�digo, e voc� o delas, se todos voc�s
seguirem o mesmo estilo. Os detalhes podem ser impostos pelas conven��es locais,
requisitos do gerenciamento, ou por um programa, mas mesmo quando n�o s�o � melhor
obedecer a um conjunto de conven��es que s�o compartilhadas por todos. Seguimos o
estilo usado no livro The C Programming Language, com pequenos ajustes para C++ e
Java.
Vamos ilustrar as regras de estilo com pequenos exemplos de programa-��o
ruim e boa, uma vez que o contraste entre as duas maneiras de dizer a mesma coisa �
instrutivo. Esses exemplos n�o s�o artificiais. Os "ruins" s�o todos adaptados de
c�digo real, escrito por programadores comuns (eventualmente por n�s mesmos)
trabalhando sob as press�es comuns de excesso de trabalho e muito pouco tempo.
Alguns ser�o destilados por quest�es de brevidade, mas poder�o ser entendidos.
Depois, vamos reescrever os trechos ruins para mostrar como eles podem ser
melhorados. Como eles s�o c�digo real, por�m, podem apresentar v�rios problemas. A
abordagem de cada falha poderia nos levar muito al�m do t�pico; portanto, alguns
dos bons exemplos conter�o outras falhas n�o mencionadas.
Para distinguir os exemplos ruins dos bons em todo o livro, vamos colocar
pontos de interroga��o nas margens do c�digo question�vel, como neste trecho real:

? #define ONE l
? #define TEN 10
? #define TWENTY 20

Por que esses #defines s�o question�veis? Pense nas modifica��es que ser�o
necess�rias quando um array de TWENTY elementos precisar ficar maior. No m�nimo,
cada nome deve ser substitu�do por outro que indique o papel do valor espec�fico no
programa:

#define INPUT_MODE 1
#define INPUT_BUFSIZE 10
#define OUTPUT_BUFSIZE 20

1.1 Nomes

O que � um nome? Um nome de vari�vel ou fun��o rotula um objeto e veicula


informa��es sobre seu prop�sito. Um nome deve ser informativo, conciso, memoriz�vel
e, se poss�vel, pronunci�vel. Grande parte das informa��es vem do contexto e do
escopo. Quanto mais amplo for o escopo de uma vari�vel, mais informa��es devem ser
veiculadas pelo seu nome.

Use nomes descritivos para globais, nomes curtos para locais. As vari�veis globais,
por defini��o, podem aparecer em qualquer parte de um programa; portanto, elas
precisam de nomes suficientemente longos e descritivos para lembrar o leitor de
seus significados. Tamb�m � bom incluir um coment�rio breve na declara��o de cada
global:

int npending = 0; //comprimento atua! da fila de entrada

As fun��es, classes e estruturas globais tamb�m devem ter nomes descritivos


que sugerem seu papel em um programa.
Por outro lado, os nomes mais curtos s�o suficientes para as vari�veis
locais. Dentro de uma fun��o, n pode ser suficiente, npoints est� bom e
numberOfPoints � um exagero.
As vari�veis globais usadas de maneiras convencionais podem ter nomes
bastante curtos. O uso de i e j para �ndices de loop, p e q para ponteiros, s e t
para strings � t�o freq�ente que h� pouco lucro e, talvez, algum preju�zo nos nomes
mais longos. Compare

? for (theElementlndex = 0; theElementlndex < numberOfElements;


? theElementIndex++)
? elementArray[theElementlndex] = theElementlndex;

com

for (i = 0; i < nelems; i++)


elem[1] = i;

Os programadores s�o encorajados a usar nomes de vari�vel longos,


independente do contexto. Isso � um erro: a clareza quase sempre � atingida por
meio da brevidade.
H� muitas conven��es de nomea��o e costumes locais. Os mais comuns incluem o
uso de nomes que come�am ou terminam com p, tais como nodep para ponteiros; letras
iniciais mai�sculas para Globais; e todas mai�sculas para CONSTANTS. Algumas
empresas de programa��o usam regras mais abrangentes, tais como nota��o para
codificar tipo e informa��es de uso na vari�vel, tal como pch para um ponteiro para
um caractere, strTo e strFrom para strings que ser�o gravadas para e lidas de.
Quanto � grafia dos nomes em si, usar npending, numPending ou num_pending � uma
quest�o de gosto. As regras espec�ficas s�o bem menos importantes do que a
consist�ncia no uso de uma conven��o sensata.
As conven��es de nomea��o facilitam a compreens�o de seu pr�prio c�digo, bem como
do c�digo escrito por outras pessoas. Elas tamb�m facilitam a inven��o de nomes, �
medida que o c�digo � escrito. Quanto mais longo for o programa, mais importante �
a escolha de nomes bons, descritivos e sistem�ticos.
Os namespaces da C++ e os packages da Java fornecem maneiras de gerenciar o
escopo dos nomes e ajudar a manter os significados claros sem nomes longos e sem
gra�a.

Seja consistente. D� a coisas relacionadas nomes relacionados que mostram seu


relacionamento e destaquem sua diferen�a.
Al�m de serem muito longos, os nomes dos membros desta classe Java s�o muito
inconsistentes:

? class UserQueue {
? int noOfltemsInQ, frontOfTheQueue, queueCapacity;
? public int noOfUsersInQueue() {...}
? }

A palavra "queue" aparece como Q, Queue e queue. Mas como as queues s� podem
ser acessadas a partir de uma vari�vel do tipo UserQueue, os nomes dos membros n�o
precisam mencionar "queue", pois o contexto � suficiente e assim

? queue.queueCapacity

� redundante. Esta � uma vers�o melhor:

class UserQueue {
int nitems, front, capacity;
public int nusers() {...}
}
uma vez que leva a declara��es como esta:

queue.capacity++;
n = queue.nusers();

N�o houve perda de clareza. Ainda h� trabalho a ser feito neste exemplo,
"items" e "users" s�o a mesma coisa; portanto, apenas um termo deve ser usado para
um �nico conceito.

Use nomes ativos para as fun��es. Os nomes de fun��es devem se basear em verbos
ativos, que podem ser seguidos por substantivos:

now = date.getTime();
putchar('\n');

As fun��es que retornam um valor boolean (true ou false) devem ser nomeadas
para que o valor de retorno n�o gere ambiguidades. Assim sendo:

? if (checkoctal(c)) ...

n�o indica qual valor � true e qual � false, enquanto

if (isoctal(c)) ...

deixa claro que a fun��o retorna true, se o argumento � octal, e false, caso ele
n�o seja.

Seja preciso. Um nome n�o apenas rotula, ele veicula informa��es para o leitor. Um
nome enganoso pode resultar em bugs dif�ceis de encontrar.
Um de n�s escreveu e distribuiu durante anos uma macro chamada isoctal com
esta implementa��o incorreta:

? #define isoctal(c) ((c) >= '0' && (c) <= '8')

em vez da implementa��o adequada:

#define isoctal (c) ((c) >= '0' && (c) <= '7')

Neste caso, o nome veiculava a inten��o correta, mas a implementa��o estava errada.
� f�cil um nome bom disfar�ar uma implementa��o ruim.
Aqui temos um exemplo no qual o nome e o c�digo est�o em completa
contradi��o:

? public boolean inTable(Object obj){


? int j = this.getlndex(obj);
? return (j == nTable);
? }

A fun��o getlndex retorna um valor entre zero e nTable-1 se encontrar o


objeto, e retorna nTable caso n�o encontre o objeto. O valor boolean retornado por
inTable �, portanto, o oposto daquilo que o nome implica. Na �poca em que o c�digo
foi escrito, isso talvez n�o causasse problema, mas se o programa fosse modificado
mais tarde, quem sabe por um programador diferente, o nome certamente confundiria.

� Exerc�cio 1-1. Comente a escolha dos nomes e valores do seguinte c�digo:

? #define TRUE 0
? #define FALSE 1
?
? if ((ch = getchar()) == EOF)
? not_eof = FALSE;
?

� Exerc�cio 1-2. Melhore esta fun��o:

? int smaller(char *s, char *t) {


? if (strcmp(s, t) < 1)
? return 1;
? else
? return 0;
? }
?

� Exerc�cio 1-3. Leia este c�digo em voz alta:

? if ((falloc(SMRHSHSCRTCH, S_IFEXT|0644, MAXRODDHSH)) < 0)


?
?

1.2 Express�es e declara��es

Em analogia com a escolha dos nomes para ajudar a compreens�o do leitor,


escreva express�es e declara��es de maneira a tornar seus significados o mais
transparente poss�vel. Escreva o c�digo mais claro que realiza a tarefa. Use
espa�os nos operadores para sugerir agrupamento. Mais geralmente, formate para
ajudar a leitura. Isso � comum mas valioso, como manter uma mesa organizada onde
voc� encontra as coisas. Ao contr�rio de sua mesa, seus programas provavelmente
ser�o examinados por outras pessoas.

Recue para mostrar a estrutura. Um estilo consistente de recuos � a maneira que


consome menos energia para tornar evidente a estrutura de um programa. Este exemplo
est� mal formatado:

? for(n++;n<100;field[n++]='\0');
? *i = '\0'; return('\n');

A reformata��o o torna um pouco melhor:

? for (n++; n < 100; field[n++] = '\0')


? ;
? *i = '\0';
? return('\n');

Melhor ainda seria colocar a atribui��o no corpo e separar o incremento,


para que o loop assumisse uma forma mais convencional e fosse mais f�cil de
entender:

for (n++; n < 100; n++)


field[n] = '\0';
*i = '\0';
return '\n';

Use a forma natural nas express�es. Escreva express�es como voc� as falaria em voz
alta. As express�es condicionais que incluem nega��es s�o sempre dif�ceis de
entender:

? if (!(block_id < actblks) || !(block_id >= unblocks))


? ...
Cada teste � declarado negativamente, embora n�o haja necessidade de serem
declarados assim. Virando as rela��es ao contr�rio � poss�vel declarar os testes
positivamente:

if ((block_id >= actblks) || (block_id < unblocks))


...

Agora o c�digo � lido naturalmente.

Use par�nteses para resolver ambig�idades. Os par�nteses especificam agrupamentos e


podem ser usados para tornar clara a inten��o, mesmo quando n�o s�o necess�rios. Os
par�nteses internos do exemplo anterior n�o s�o necess�rios, nas n�o custa nada
adicion�-los. Os programadores experientes podem omiti-los, porque os operadores
relacionais (< <= == != >= >) t�m preced�ncia maior do que os operadores l�gicos
(&& e ||).
Ao misturar operadores n�o-relacionados, por�m, � bom coloc�-los entre
par�nteses. A linguagem C e suas amigas apresentam problemas perniciosos de
preced�ncia, e assim fica f�cil cometer um erro.
Como os operadores l�gicos se vinculam melhor do que a atribui��o, os
par�nteses s�o obrigat�rios na maioria das express�es que os combinam:

while ((c = getchar()) != EOF)


...

Os operadores bitwise, & e |, t�m preced�ncia mais baixa do que operadores


relacionais como ==, e assim, apesar de sua apar�ncia

? if (x&MASK == BITS)
? ...

na verdade significa

? if (x & (MASK==BITS))
? ...

que certamente n�o era a inten��o do programador. Como ele combina operadores
bitwise e relacionais, a express�o precisa de par�nteses:

if ((x&MASK) == BITS)
...

Mesmo quando os par�nteses n�o s�o necess�rios, eles podem ajudar se o


agrupamento for dif�cil de entender � primeira vista. Este c�digo n�o precisa de
par�nteses:

? leap_year = y % 4 == O && y % 100 != 0 || y % 400 == 0;

mas eles facilitam sua compreens�o:

leap_year = ((y%4 == 0) && (y%100 != 0)) || (y%400 == 0);

Tamb�m removemos alguns dos espa�os em branco: o agrupamento dos operandos de


operadores com preced�ncia mais alta ajuda o leitor a ver mais rapidamente a
estrutura.

Divida as express�es complexas. C, C++ e Java t�m sintaxe rica de express�o e


operadores. Assim, fica f�cil ceder � tenta��o de colocar tudo em uma mesma
constru��o. Uma express�o como a seguinte � compacta, mas coloca operadores demais
em uma �nica declara��o:

? *x += (*xp=(2*k < (n-m) ? c[k+l] : d[k--]));

Fica mais f�cil entender quando a dividimos em v�rias partes:

if (2*k < n-m)


*xp = c[k+l];
else
*xp = d[k--];
*x += *xp;

Seja claro. A infind�vel energia criativa dos programadores �s vezes � usada para
escrever o c�digo mais conciso poss�vel, ou para encontrar maneiras inteligentes de
atingir um resultado. �s vezes, por�m, essas habilidades s�o mal aplicadas, porque
o objetivo � escrever c�digo claro, n�o c�digo inteligente. O que faz este
complicado c�lculo?

? subkey = subkey " (bitoff - ((bitoff " 3) " 3));

A express�o mais interna muda bitoff tr�s bits � direita. O resultado � mudado
novamente para a esquerda, substituindo assim os tr�s bits mudados por zeros. Esse
resultado, por sua vez, � subtra�do do valor original, resultando nos tr�s bits
inferiores de bitoff. Esses tr�s bits s�o usados para mudar subkey � direita. Assim
sendo, a express�o original � equivalente a:

subkey = subkey " (bitoff & 0x7);

Leva um tempo para descobrir o que a primeira vers�o est� fazendo; a segunda � mais
curta e clara. Os programadores experientes a fazem mais curta ainda usando um
operador de atribui��o:

subkey "= bitoff & 0x7;

Algumas constru��es parecem convidar ao abuso. O operador ?: pode levar a


c�digo misterioso:

? child=(!LC&&!RC)?0:(!LC?RC:LC);

� quase imposs�vel descobrir o que ele faz seguindo todos os caminhos poss�veis da
express�o. Esta forma � mais longa, mas muito mais f�cil de seguir porque ela deixa
os caminhos expl�citos:

if (LC == O && RC == 0)
child = 0;
else if (LC == 0)
child = RC;
else
child = LC;

O operador ?: � bom para express�es curtas, onde ele pode substituir quatro linhas
de if-else por uma, como em:

max = (a > b) ? a : b;

ou quem sabe:

printf("The list has %d item%s\n", n, n==l ? "" : "s");


mas ele n�o � um substituto geral para as declara��es condicionais.
A clareza n�o � o mesmo que brevidade. Quase sempre o c�digo mais claro ser�
mais curto, como no exemplo de mudan�a de bit, mas ele tamb�m pode ser mais longo,
como na express�o condicional reformulada como uma if-else. O crit�rio adequado � a
facilidade de entendimento.

Cuidado com os efeitos colaterais. Operadores como ++ t�m efeitos cola-terais: al�m
de retornar um valor eles tamb�m modificam uma vari�vel b�sica. Os efeitos
colaterais podem ser extremamente convenientes, mas tamb�m podem causar problemas
porque as a��es de recupera��o do valor e atualiza��o da vari�vel podem n�o
acontecer ao mesmo tempo. Em C e C++, a ordem de execu��o dos efeitos colaterais �
indefinida; portanto, essa atribui��o m�ltipla deve produzir a resposta errada:

? str[i++] = str[i++] = ' ';

A inten��o � armazenar espa�os em branco nas duas pr�ximas posi��es de str.


Mas, dependendo de quando i � atualizado, uma posi��o de str deve ser pulada, e i
pode acabar aumentado somente por 1. Divida isso em duas declara��es:

str[i++] = ' ';


str[i++] = ' ';

Embora contenha apenas um incremento, esta atribui��o tamb�m pode ter


resultados variados:

? array[i++] = i;

Se i � inicialmente 3, o elemento array pode ser definido como 3 ou 4.


N�o s�o apenas os incrementos e decrementos que t�m efeitos colaterais. A
E/S � outra fonte de a��o nos bastidores. Este exemplo � uma tentativa de ler dois
n�meros relacionados de entrada padr�o:

? scanf("%d %d", &yr, &profit[yr]);

Ele est� dividido porque parte da express�o modifica yr e outra parte a usa. O
valor de profit[yr] nunca pode estar certo, a menos que o valor novo de yr seja
igual ao antigo. Voc� pode achar que a resposta depende da ordem na qual os
argumentos s�o avaliados, mas a verdadeira quest�o � que todos os argumentos de
scanf s�o avaliados antes de a rotina ser chamada, portanto &profit[yr] sempre ser�
avaliado usando o valor antigo de yr. Esse tipo de problema pode ocorrer em quase
todas as linguagens. A solu��o, como sempre, � dividir a express�o:

scanf("%d", &yr);
scanf("%d", &profit[yr]);

Cuidado com todas as express�es que t�m efeitos colaterais.

� Exerc�cio 1-4. Melhore cada um destes fragmentos:

? if ( !(c == 'y' || c == 'Y') )


? return;

? length = (length < BUFSIZE) ? length : BUFSIZE;

? flag = flag ? 0 : 1;

? quote = (*line == "") ? l : 0;

? if (val & 1)
? bit = 1;
? else
? bit = 0;
?

� Exerc�cio 1-5. O que h� de errado com este trecho?

? int read(int *ip) {


? scanf("%d", ip);
? return *ip;
? }
? ...
? insert(&graph[vert], read(&val), read(&ch));
?

� Exerc�cio 1-6. Relacione todas as sa�das diferentes que isto poderia produzir com
v�rios pedidos de avalia��o:

? n = 1;
? printf("%d %d\n", n++, n++);

Tente isso no m�ximo de compiladores que puder, para ver o que acontece na pr�tica.
?

1.3 Consist�ncia e idiomas

A consist�ncia leva a programas melhores. Se a formata��o varia de forma


imprevis�vel, ou um loop em um array sobe a montanha agora e desce depois, ou
se as strings s�o copiadas com strcpy aqui e com um loop for ali, as varia��es
tornam mais dif�cil ver o que realmente est� acontecendo. Mas se o mesmo c�lculo �
feito da mesma maneira sempre que aparece, toda varia��o sugere uma diferen�a
genu�na e digna de nota.

Use um estilo de recuo e chave consistente. O recuo mostra a estrutura, mas qual �
o melhor estilo de recuo? A chave de abertura deve ficar na mesma linha de if ou na
pr�xima? Os programadores sempre discutiram o layout dos programas, mas o estilo
espec�fico � bem menos importante do que a aplica��o consistente. Escolha um
estilo, de prefer�ncia o seu, use-o de forma consistente e n�o perca tempo
discutindo.
Voc� deve incluir as chaves quando elas n�o s�o necess�rias? Assim como os
par�nteses, as chaves podem resolver a ambig�idade e, eventualmente, tornar o
c�digo mais claro. Por quest�es de consist�ncia, muitos programadores experientes
sempre colocam o loop ou os corpos if entre chaves. Mas se o corpo � uma declara��o
simples elas n�o s�o necess�rias e, assim, nossa tend�ncia � omiti-las. Se voc�
tamb�m preferir deix�-las de fora, n�o as solte quando elas forem necess�rias para
resolver a ambig�idade do "dangling else" exemplificada neste trecho:

? if (month == FEB) {
? if (year%4 == 0)
? if (day > 29)
? legal = FALSE;
? else
? if (day > 28)
? legal = FALSE;
? }

O recuo � enganoso, uma vez que else �, na verdade, anexado � linha

? if (day > 29)


e o c�digo est� errado. Quando um if segue outro imediatamente, use sempre chaves:

? if (month == FEB) {
? if (year%4 == 0) {
? if (day > 29)
? legal = FALSE;
? } else {
? if (day > 28)
? legal = FALSE;
? }
? }

As ferramentas de edi��o orientadas para a sintaxe tornam menos poss�vel esse tipo
de erro.
Mesmo com o bug consertado, por�m, o c�digo � dif�cil de acompanhar. O
c�lculo � mais f�cil de entender se usarmos uma vari�vel para conter o n�mero de
dias de fevereiro:

? if (month == FEB) {
? int nday;
?
? nday = 28;
? if (year%4 == 0)
? nday = 29;
? if (day > nday)
? legal = FALSE;
? }

O c�digo ainda est� errado - 2000 � ano bissexto, enquanto 1900 e 2100 n�o s�o -
mas essa estrutura � muito mais f�cil de adaptar para torn�-lo absolutamente
correto.
Por falar nisso, se voc� trabalhar em um programa que n�o escreveu preserve
o estilo que encontrou. Quando fizer uma altera��o, n�o use seu pr�prio estilo
mesmo que prefira assim. A consist�ncia do programa � mais importante do que a sua,
porque ela torna a vida mais f�cil para aqueles que vierem depois.

Use idiomas para ter consist�ncia. Assim como os idiomas naturais, as linguagens de
programa��o t�m idiomas, maneiras convencionais pelas quais os programadores
experientes escrevem c�digo comum. Uma parte central do aprendizado de qualquer
linguagem � o desenvolvimento da familiaridade com os seus idiomas.
Um dos idiomas mais comuns � a forma de um loop. Veja o c�digo C, C++ ou
Java para passar pelos elementos n de um array, por exemplo, para inicializ�-los.
Algu�m poderia escrever o loop desta forma:

? i = 0;
? while (i <= n-1)
? array[i++] = 1.0;

ou quem sabe assim:

? for (i = 0; i < n; )
? array[i++] = 1.0;

ou mesmo:

? for (i = n; --i >= 0; )


? array[i] = 1.0;
Todos eles est�o corretos, mas a forma idiom�tica � esta:

for (i = 0; i < n; i++)


array[i] = 1.0;

Essa n�o � uma op��o arbitr�ria. Ela visita cada membro de um array de n elementos
indexado de 0 at� n-1. Ela coloca todo o controle do loop no pr�prio for, executa
na ordem crescente e usa o pr�prio operador idiom�tico ++ para atualizar a vari�vel
de loop. Ela deixa a vari�vel de �ndice com um valor conheci-do al�m do �ltimo
elemento do array. Os nativos a reconhecem sem estudo e a escrevem corretamente sem
pensar um momento.
Em C++ ou Java, uma variante comum inclui a declara��o da vari�vel de loop:

for (int i = 0; i < n; i++) array[i] = 1.0;

Este � o loop padr�o para caminhar por uma lista em C:

for (p = list; p != NULL; p = p->next)

Novamente, todo o controle do loop est� no for. Para um loop infinito, preferimos:

for (;;)
...

mas,

while (1)
...

tamb�m � conhecido. N�o use nada al�m dessas duas formas.

O recuo tamb�m deve ser idiom�tico. Esse layout vertical incomum denigre a
facilidade de leitura. Ele se parece com tr�s declara��es, n�o com um loop:

? for (
? ap = arr;
? ap < arr + 128;
? *ap++ = 0
? )
? {
? ;
? )

Um loop padr�o � muito mais f�cil de ler:

for (ap = arr; ap < arr+128; ap++)


*ap = 0;

Os layouts esparramados tamb�m for�am o c�digo a ficar em v�rias telas ou p�ginas


e, assim, denigre a facilidade de leitura.
Outro idioma comum � aninhar uma atribui��o dentro de uma condi��o loop,
como em:

while ((c = getchar()) != EOF) putchar(c);

A declara��o do-while � usada com muito menos freq��ncia do que for e while, porque
ela � sempre executada pelo menos uma vez, testando na parte inferior do loop em
vez da parte superior. Em muitos casos, aquele comportamento � um bug que est�
esperando para atacar, como nesta nova vers�o do loop getchar:
? do {
? c = getchar();
? putchar(c);
? } while (c != EOF);

Ele escreve um caractere de sa�da esp�rio porque o teste ocorre ap�s a chamada de
putchar. O loop do-while � o loop certo apenas quando o corpo do loop sempre deve
ser executado pelo menos uma vez; veremos outros exemplos mais tarde.
Uma vantagem do uso consistente dos idiomas � que ele chama a aten��o para
loops n�o-padr�o, um sinal freq�ente de problemas:

? int i, *iArray, nmemb;


?
? iArray = malloc(nmemb * sizeof(int));
? for (i = 0; i <= nmemb; i++)
? iArray[i] = i;

O espa�o � alocado para nmemb itens, de iArray[0] at� iArray[nmemb-1], mas como o
teste do loop � <=, o loop vai at� o final do array e sobrep�e tudo que estiver
armazenado a seguir na mem�ria. Infelizmente, erros como este n�o s�o detectados
antes que o dano tenha sido causado.
C e C++ tamb�m t�m idiomas para alocar espa�o para strings e depois
manipul�-lo, e o c�digo que n�o o usa quase sempre abriga um bug:

? char *p, buf[256];


?
? gets(buf);
? p = malloc(strlen(buf));
? strcpy(p, buf);

Nunca se deve usar gets, uma vez que n�o h� como limitar a quantidade de
entrada que ele ler�. Isso leva a problemas de seguran�a, aos quais retornaremos no
Cap�tulo 6, onde mostraremos que fgets � sempre uma op��o melhor. Mas tamb�m existe
outro problema: strlen n�o conta o '\0' que termina uma string, enquanto strcpy o
copia. Dessa forma, n�o � alocado espa�o sufi-ciente, e strcpy grava depois do
espa�o alocado. O idioma �:

p = malloc(strlen(buf)+l);
strcpy(p, buf);

ou

p = new char[strlen(buf)+l];
strcpy(p, buf);

em C++. Se voc� n�o vir o +1, tome cuidado.


A Java n�o sofre desse problema espec�fico, uma vez que as strings n�o s�o
representadas como arrays terminados por null. Os array subscritos tamb�m s�o
verificados; portanto, n�o � poss�vel o acesso fora dos limites de um array na
Java.
A maioria dos ambientes C e C-H- fornece uma fun��o de biblioteca, strdup,
que cria uma c�pia de uma string usando malloc e strcpy, tornando mais f�cil evitar
esse bug. Infelizmente, strdup n�o faz parte do padr�o ANSI C.
Por falar nisso, nem o c�digo original nem a vers�o corrigida verificam o
valor retornado por malloc. Omitimos esse aperfei�oamento para focalizar o ponto
principal, mas em um programa real o valor de retorno de malloc, realloc, strdup,
ou qualquer outra rotina de aloca��o sempre deve ser verifica-do.
Use else-ifs para decis�es de v�rias vias. As decis�es de v�rias vias s�o expressas
idiomaticamente como uma cadeia de if...else if...else, desta forma:

if (condition1)
statementl
else if (condition2)
statement2
else if (conditionn)
statementn
else
default-statement

As condi��es s�o lidas de cima para baixo. Na primeira condition que � satisfeita,
a statement seguinte � executada, e depois o restante da constru��o � saltado. A
parte statement pode ser uma �nica declara��o ou um grupo de declara��es entre
colchetes.
O �ltimo else trata da situa��o "padr�o", em que nenhuma das outras alterna-tivas
foi escolhida. Essa parte final do else pode ser omitida se n�o houver a��o para o
padr�o, embora deix�-lo com uma mensagem de erro seja algo que pode ajudar a pegar
as condi��es que "n�o podem acontecer".
Alinhe verticalmente todas as cl�usulas else em vez de alinhar cada else com
o if correspondente. O alinhamento vertical enfatiza que os testes s�o feitos na
seq��ncia e evita que eles saiam pelo lado direito da p�gina.
Uma seq��ncia de declara��es if aninhadas quase sempre � um aviso de c�digo
esquisito, se n�o for de erros:

? if (argc == 3)
? if ((fin = fopen(argv[l], "r")) != NULL)
? if ((fout = fopen(argv[2], "w")) != NULL) {
? while ((c = getc(fin)) != EOF)
? putc(c, fout);
? fclose(fin); fclose(fout);
? } else
? printf("Can't open output file %s\n", argv[2]);
? else
? printf("Can't open input file %s\n", argv[l]);
? else
? printf("Usage: cp inputfile outputfile\n");

A seq��ncia de ifs exige que criemos um registro mental dos testes que foram
feitos, para que no ponto apropriado n�s possamos tir�-los at� determinarmos a a��o
correspondente (se ainda conseguirmos nos lembrar dela). Como no m�ximo uma a��o
ser� executada, realmente queremos um else if. A altera��o da ordem na qual as
decis�es s�o tomadas leva a uma vers�o mais clara, na qual tamb�m corrigimos o
vazamento de recursos do original:

if (argc != 3)
printf ("Usage: cp inputfile outputfile\n");
else if ((fin = fopen(argv[l], "r")) == NULL)
printf ("Can't open input file %s\n", argv[l]);
else if ((fout = fopen(argv[2], "w")) == NULL) {
printf("Can't open output file %s\n", argv[2]);
fclose (fin);
}
else {
while ((c = getc(fin)) != EOF)
putc(c, fout);
fclose(fin);
fclose(fout);
}

Lemos os testes at� o primeiro que seja verdadeiro, executamos a a��o corres-
pondente e continuamos depois do �ltimo else. A regra � seguir o mais perto
poss�vel cada decis�o de sua a��o associada. Ou, em outras palavras, toda vez que
fizer um teste fa�a alguma coisa.
As tentativas de reutilizar c�digo quase sempre levam a programas
emaranhados:

? switch (c) {
? case '-': sign = -1;
? case '+': c = getchar();
? case '.': break;
? default: if (!isdigit(c))
? return 0;
? }

Este c�digo usa uma seq��ncia complicada de fall-throughs na declara��o switch para
evitar duplicar uma linha de c�digo. Ela tamb�m n�o � idiom�tica. Os cases quase
sempre terminam com um break, com as raras exce��es comentadas. Um layout e
estrutura mais tradicionais s�o mais f�ceis de ler, embora mais longos:

? switch (c) {
? case '-':
? sign = -1;
? /* fall through */
? case '+':
? c = getchar();
? break;
? case '.':
? break;
? default:
? if (!isdigit(c))
? return 0;
? break;
? }

O aumento no tamanho � mais do que compensado pelo aumento na clareza. Entretanto,


para tal estrutura incomum uma seq��ncia de declara��es else-if a deixa mais clara
ainda:

if (c == '-') {
sign = -1;
c = getchar();
} else if (c == '+') {
c = getchar();
} else if (c != '.' && !isdigit(c)) {
return 0;
}

As chaves dos blocos de uma linha destacam a estrutura paralela.


Um uso aceit�vel de um fall-through ocorre quando v�rios cases t�m c�digo
id�ntico. O layout convencional � assim:

case '0':
case 'l':
case '2':
...
break;
e nenhum coment�rio � requerido.

� Exerc�cio 1-7. Reescreva estes trechos em C/C++ com mais clareza:

? if (istty(stdin));
? else if (istty(stdout));
? else if (istty(stderr));
? else return(0);
? if (retval != SUCCESS)
? {
? return (retval);
? }
? /* Tudo correu bem! */
? return SUCCESS;

? for (k = 0; k++ < 5; x += dx)


? scanf("%lf", &dx);
?

� Exerc�cio 1-8. Identifique os erros deste fragmento em Java e conserte-o


reescrevendo com um loop idiom�tico:

? int count = 0;
? while (count < total) {
? count++;
? if (this.getName(count) == nametable.userName()) {
? return (true);
? )
? }
?

1.4 Macros de fun��o

H� uma tend�ncia entre os programadores mais experientes em C de escrever


macros em vez de fun��es para todos os c�lculos curtos que ser�o executados com
freq��ncia. Opera��es de E/S, tais como getchar, e testes de caracteres, tais como
isdigit, s�o exemplos sancionados oficialmente. O motivo � o desempenho: uma macro
evita a overhead de uma chamada de fun��o. Este argumento j� era fraco mesmo antes
da C ter sido definida pela primeira vez, um tempo de m�quinas lentas e chamadas de
fun��o caras. Hoje em dia isso � irrelevante. Com m�quinas e compiladores modernos,
as desvantagens das macros de fun��o superam seus benef�cios.

Evite as macros de fun��o. Em C++, as fun��es em linha tornam as macros de fun��o


desnecess�rias. Em Java n�o h� macros. Em C, elas causam mais proble-mas do que
solu��es.
Um dos problemas mais s�rios das macros de fun��o � que um par�metro que
aparece mais de uma vez na defini��o poderia ser avaliado mais de uma vez. Se o
argumento da chamada inclui uma express�o com efeitos colaterais, o resultado � um
bug sutil. Este c�digo tenta implementar um dos testes de caractere de <ctype.h>:

? #define isupper(c) ((c) >= 'A' && (c) <= 'Z')

Observe que o par�metro c ocorre duas vezes no corpo da macro. Se isupper � chamado
em um contexto como este,

? while (isupper(c = getchar()))


? ...
toda vez que um caractere de entrada for maior do que ou igual a A, ele ser�
descartado e outro caractere ser� lido para ser testado com rela��o a Z. O padr�o C
foi escrito cuidadosamente para permitir que isupper e fun��es an�logas se-jam
macros, mas apenas se elas garantirem a avalia��o do argumento apenas uma vez,
portanto essa implementa��o � dividida.
Sempre � melhor usar as fun��es ctype do que implement�-las voc� mesmo, e �
mais seguro n�o aninhar rotinas como getchar que t�m efeitos colaterais.
Reescrevendo o teste para usar duas express�es em vez de uma, voc� o torna mais
claro e tamb�m d� uma oportunidade de pegar o end-of-file explicitamente:

while ((c = getchar()) != EOF && isupper(c))


...

Eventualmente uma avalia��o m�ltipla causa um problema de desempenho em vez


de um erro direto. Veja este exemplo:

? #define ROUND_TO_INT(x) ((int)((x)+(((x)>0)?0.5:-0.5)))


? ...
? size = ROUND_TO_INT(sqrt(dx*dx + dy*dy));

Isso executar� duas vezes o c�lculo da raiz quadrada sempre que necess�rio. Mesmo
com argumentos simples, uma express�o complexa como o corpo de ROUND_TO_INT se
traduz em muitas instru��es, as quais devem ser abrigadas em uma �nica fun��o para
ser chamada quando necess�rio. A instancia��o de uma macro a cada ocorr�ncia torna
o programa compilado maior. (As fun��es em linha da C++ tamb�m t�m essa
desvantagem.)

Coloque o corpo e os argumentos da macro entre par�nteses. Se voc� insistir em usar


as macros de fun��o, tome cuidado. As macros funcionam por substitui��o textual: os
par�metros da defini��o s�o substitu�dos pelos argu-mentos da chamada e o resultado
substitui a chamada original, como texto. Essa � uma diferen�a importuna das
fun��es. A express�o

1 / square(x)

funciona bem se square � uma fun��o, mas se ela for uma macro como esta

? #define square(x) (x) * (x)

a express�o ser� expandida para a macro errada

? 1 / (x) * (x)

A macro deve ser reescrita como:

#define square(x) ((x) * (x))

Todos aqueles par�nteses s�o necess�rios. Mesmo com os par�nteses adequados, o


problema da avalia��o m�ltipla n�o se resolve. Se uma opera��o � cara ou
suficientemente comum para ser quebrada em linhas menores, use uma fun��o.
Em C++, as fun��es em linha evitam o problema sint�tico oferecendo as
vantagens de desempenho das macros. Elas s�o apropriadas para fun��es curtas que
definem ou recuperam um �nico valor.

� Exerc�cio 1-9. Identifique os problemas desta defini��o de macro:

? #define ISDIGIT(c) ((c >= '0') && (c <= '9')) ? 1 : 0


?
1.5 N�meros m�gicos

Os n�meros m�gicos s�o as constantes, os tamanhos de array, as posi��es de


caractere, fatores de convers�o e outros valores num�ricos literais que aparecem
nos programas.

D� nomes aos n�meros m�gicos. Como orienta��o, todo n�mero diferente de 0 ou 1 pode
ser m�gico e deve receber seu pr�prio nome. Um n�mero bruto no programa fonte n�o
indica sua import�ncia ou deriva��o, tornando o programa mais dif�cil de entender e
modificar. Este trecho de um programa para imprimir um histograma de freq��ncias de
letras em um terminal de cursor 24 por 80 � desnecessariamente opaco, por causa de
uma variedade de n�meros m�gicos:

? fac = lim / 20; /* definir fator de escala */


? if (fac < 1)
? fac = 1;
? /* gerar histograma */
? for (i = 0, col = 0; i < 27; i++, j++) {
? col += 3;
? k = 21 - (let[i] / fac);
? star = (let[i] == 0) ? ' ' : '*';
? for (j = k; j < 22; j++)
? draw(j, col, star);
? }
? draw(23, 2, ' '); /* eixo x de r�tulo */
? for (i = 'A'; i <= 'Z'; i++)
? printf("%c ", i);

O c�digo inclui, entre outros, os n�meros 20, 21, 22, 23 e 27. Eles est�o
claramente relacionados, n�o est�o? Na verdade, h� apenas tr�s n�meros cr�ticos
para esse programa: 24, o n�mero de linhas da tela, 80, o n�mero de colunas e 26, o
n�mero de letras do alfabeto. Mas nenhum deles aparece no c�digo, o que torna os
n�meros que aparecem mais m�gicos ainda.
Dando nomes aos n�meros principais do c�lculo, podemos tornar o c�digo mais
f�cil de acompanhar. Descobrimos, por exemplo, que o n�mero 3 vem de (80 - 1)/26, e
que let deve ter 26 entradas, n�o 27 (um erro off-by-one talvez causado pelas
coordenadas de tela indexadas por 1). Fazendo algumas outras simplifica��es temos
este resultado:

enum {
MINROW = 1, /* lado superior */
MINCOL = l, /* lado esquerdo */
MAXROW = 24, /* lado inferior (<=) */
MAXCOL = 80, /* lado direito (<=) */
LABELROW =1, /* posi��o dos r�tulos */
NLET = 26, /* tamanho do alfabeto */
HEIGHT = MAXROW - 4, /* altura das barras */
WIDTH = (MAXCOL-1)/NLET /* largura das barras */
};
...
fac = (lim + HEIGHT-1) / HEIGHT; /* definir fator de escala */
if (fac < 1)
fac = 1;
for (i = 0; i < NLET; i++) { /* gerar histograma */
if (let[i] == 0)
continue;
for (j = HEIGHT - let[i]/fac; j < HEIGHT; j++)
draw(j+1 + LABELROW, (i+1)*WIDTH, '*');
}
draw(MAXROW-1, MINCOL+1, ' '); /* eixo x do r�tulo */
for (i = 'A'; i <= 'Z'; i++)
printf("%c ", i);

Agora est� claro o que o loop principal faz. Ele � um loop idiom�tico de 0 para
NLET, indicando que o loop est� sobre os elementos dos dados. As chamadas de draw
s�o mais f�ceis de entender porque palavras como MAXROW e MINCOL nos lembram da
ordem dos argumentos. Mais importante do que isso � o fato de que n�o � poss�vel
adaptar o programa para outro tamanho de v�deo ou para dados diferentes. Os n�meros
s�o desmistificados, assim como o c�digo.

Defina os n�meros como constantes, n�o como macros. Os programadores em C usaram


#define tradicionalmente para gerenciar os valores de n�mero m�gico. O pr�-
processador C � uma ferramenta poderosa, por�m, e as macros s�o uma maneira
perigosa de programar porque elas mudam a estrutura l�xica do programa. Deixe que a
linguagem adequada fa�a o trabalho. Em C e C++, as constantes inteiras podem ser
definidas com uma declara��o enum, como vimos no exemplo anterior. As constantes de
qualquer tipo podem ser declaradas com const em C++.

const int MAXROW = 24, MAXCOL = 80;

ou final em Java:

static final int MAXROW = 24, MAXCOL = 80;

C tamb�m tem valores const; mas eles n�o podem ser usados como limites de array;
portanto, a declara��o enum continua sendo o m�todo escolhido em C.

Use constantes de caractere, n�o inteiros. As fun��es de <ctype.h> ou seu


equivalente devem ser usadas para testar as propriedades dos caracteres. Um teste
como este

? if (c >= 65 && c <= 90)


? ...

depende totalmente de uma determinada representa��o de caractere. � melhor usar

? if (c >= 'A' && c <= 'Z')


? ...

mas isso pode n�o ter o efeito desejado se as letras n�o forem cont�guas na
codifica��o do conjunto de caracteres, ou se o alfabeto incluir outras letras. �
melhor usar a biblioteca:

if (isupper(c))
...

em C ou C++, ou

if (Character.isUpperCase(c))
...

em Java.
Uma quest�o relacionada � que o n�mero 0 aparece quase sempre nos programas,
em muitos contextos. O compilador vai converter o n�mero para o tipo apropriado,
mas se o tipo for expl�cito fica mais f�cil para o leitor entender o papel de cada
0. Por exemplo, use (void*)0 ou NULL para representar um ponteiro zero em C, e '\0'
em vez de 0 para representar o byte null no final de uma string. Em outras
palavras, n�o escreva

? str = 0;
? name[i] = 0;
? x = 0;

mas sim:

str = NULL;
name[i] = '\0';
x = 0.0;

Preferimos usar constantes expl�citas diferentes, reservando O para um zero inteiro


literal, porque eles indicam o uso do valor e assim fornecem um pouco de
documenta��o. Em C++, por�m, 0 em vez de NULL � a nota��o aceita para um ponteiro
null. A Java soluciona o problema melhor, definindo a palavra-chave null para uma
refer�ncia de objeto que n�o se refere a nada.

Use a linguagem para calcular o tamanho de um objeto. N�o use um tamanho expl�cito
para nenhum datatype. Use sizeof(int) em vez de 2 ou 4, por exemplo. Por motivos
semelhantes, sizeof(array[0]) � melhor do que sizeof(int) porque � uma coisa a
menos a ser alterada se o tipo do array mudar.
Com o operador sizeof �s vezes n�o precisamos inventar nomes para os n�meros
que determinam os tamanhos de array. Por exemplo, se escrevermos

char buf[1024];
fgets(buf, sizeof(buf), stdin);

o tamanho do buffer continua sendo um n�mero m�gico, mas ele ocorre apenas uma vez
na declara��o. Talvez n�o valha a pena inventar um nome para o tamanho de um array
local, mas sem d�vida vale a pena escrever c�digo que n�o tem de ser alterado
quando o tamanho do tipo muda.
Os arrays da Java t�m um campo length que d� o n�mero de elementos:

char buf[] = new char[1024];


for (int i = 0; i < buf.length; i++)
...

N�o existe equivalente de length em C e C++, mas para um array (n�o um


ponteiro) cuja declara��o � vis�vel, esta macro calcula o n�mero de elementos do
array:

#define NELEMS(array) (sizeof(array) / sizeof(array[0]))

double dbuf[100];
for (i = 0; i < NELEMS(dbuf); i++)
...

O tamanho do array � definido em apenas um lugar. O restante do c�digo n�o


muda com o tamanho. N�o h� problema na avalia��o m�ltipla do argumento de macro
aqui, uma vez que n�o pode haver efeitos colaterais e, na verdade, o c�lculo �
feito � medida que o programa � compilado. Esse � um uso apropriado para uma macro
porque ele faz algo que uma fun��o n�o consegue fazer: calcula o tamanho de um
array a partir de sua declara��o.

� Exerc�cio 1-10. Como voc� reescreveria estas defini��es para minimizar os


poss�veis erros?

? #define FT2METER 0.3048


? #define METER2FT 3.28084
? #define MI2FT 5280.0
? #define MI2KM 1.609344
? #define SQMI2SQKM 2.589988
?

1.6 Coment�rios

Os coment�rios destinam-se a ajudar o leitor de um programa. Eles n�o ajudam


dizendo coisas que o c�digo j� diz claramente, ou contradizendo o c�digo, ou
distraindo o leitor com exibi��es tipogr�ficas elaboradas. Os melhores comen-t�rios
s�o aqueles que ajudam a entender um programa, indicando brevemente detalhes
importantes ou fornecendo uma vis�o em grande escala dos proce-dimentos.

N�o enfatize o �bvio. Os coment�rios n�o devem reportar informa��es evi-dentes,


tais como o fato de que i++ incrementou i. Aqui temos alguns de nossos coment�rios
in�teis preferidos:

? /*
? * default
? */
? default:
? break;

? /* retorna SUCCESS */
? return SUCCESS;

? zerocount++; /* Incrementa o contador de entrada zero */

? /* Inicializar "total" como "number_received" */


? node->total = node->number_received;

Todos esses coment�rios poderiam ser exclu�dos, eles s� atrapalham.


Os coment�rios devem acrescentar algo que n�o est� imediatamente evidente no
c�digo, ou para reunir em um s� lugar as informa��es que est�o espalhadas pela
fonte. Quando algo sutil est� acontecendo um coment�rio pode esclarecer, mas se as
a��es j� s�o �bvias, n�o h� porque declar�-las novamente com palavras:

? while ((c = getchar()) != EOF && isspace(c))


? ; /* saltar espa�o em branco */
? if (c == EOF) /* final do arquivo */
? type = endoffile;
? else if (c == '(') /* par�ntese esquerdo */
? type = leftparen;
? else if (c == ')') /* par�ntese direito */
? type = rightparen;
? else if (c == ';') /* ponto e v�rgula */
? type = semi c�lon;
? else if (is_op(c)) /* operador */
? type = operator;
? else if (isdigit(c)) /* n�mero */
? ...

Esses coment�rios tamb�m devem ser exclu�dos, uma vez que nomes foram bem
escolhidos e j� veiculam as informa��es.

Comente as fun��es e os dados globais. � claro que os coment�rios podem ser �teis.
Comentamos as fun��es, vari�veis globais, defini��es de constantes, campos de
estruturas e classes, e tudo aquilo onde um breve resumo pode auxiliar na
compreens�o.
As vari�veis globais t�m a tend�ncia de aparecer de forma intermitente em
todo um programa. Um coment�rio serve de lembrete a ser consultado sempre que
preciso. Aqui temos um exemplo de um programa do Cap�tulo 3 deste livro:

struct State { /* prefixo + lista de sufixo */


char *pref[NPREF]; /* palavras de prefixo */
Suffix *suf; /* lista de sufixos */
State *next; /* pr�ximo da hash table */
};

Um coment�rio que apresenta cada fun��o prepara a leitura do pr�prio c�digo.


Se o c�digo n�o for muito longo ou t�cnico demais, uma �nica linha ser� suficiente:

// random: retorna um inteiro na faixa [0..r-1].


int random(int r)
{
return (int)(Math.floor(Math.random()*r));
}

�s vezes o c�digo � verdadeiramente dif�cil, talvez devido a um algoritmo


complicado ou a estruturas de dados intrincadas. Nesse caso, um coment�rio que
indica uma fonte de compreens�o pode auxiliar o leitor. Tamb�m � bom sugerir por
que determinadas decis�es foram tomadas. Este coment�rio apresenta uma
implementa��o extremamente eficiente de uma discrete cosine transform (DCT) inversa
usada em um decodificador de imagem JPEG.

/*
* idct: Implementa��o de inteiro escalada de
* Discrete Cosine Transform bidimensional inversa 8x8,
* Algoritmo de Chen-Wang (IEEE ASSP-32, p�gs. 803-816, agosto de 1984)
*
* aritm�tica de inteiro de 32 bits (coeficientes de 8 bits)
* 11 multiplica, 29 soma por DCT
*
* Coeficientes estendidos para 12 bits para
* compatibilidade com IEEE 1180-1990
*/

static void idct(int b[8*8])


{
...
}

Esse coment�rio �til cita a refer�ncia, descreve brevemente os dados usados,


indica o desempenho do algoritmo e diz como e por que o algoritmo original foi
modificado.

N�o comente c�digo ruim, reescreva-o. Comente tudo que seja incomum ou
potencialmente confuso, mas quando o coment�rio ultrapassa o c�digo, provavelmente
este precisa de conserto. Este exemplo usa um coment�rio longo e confuso e uma
declara��o de impress�o de depura��o compilada condicionalmente para explicar uma
�nica declara��o:

? /* Se "result" for 0 uma coincid�ncia foi encontrada e, portanto,


? retorna true (n�o-zero).
? Caso contr�rio, "result" � n�o-zero e, portanto, retorna false
? (zero).
? */
?
? #ifdef DEBUG
? printf("*** isword returns !result = %d\n", !result);
? fflush(stdout);
? #endif
?
? return(! result);

As nega��es s�o dif�ceis de entender e devem ser evitadas. Parte do problema


est� no nome de vari�vel pouco informativo, result. Um nome mais descritivo,
matchfound, torna o coment�rio desnecess�rio e tamb�m limpa a de-clara��o de
impress�o.

#ifdef DEBUG
printf("*** isword returns matchfound = %d\n", matchfound);
fflush(stdout);
#endif

return matchfound;

N�o contradiga o c�digo. A maioria dos coment�rios concorda com o c�digo quando
eles s�o escritos. Mas, quando os bugs s�o solucionados e o programa evolui, os
coment�rios quase sempre ficam em sua forma original, resultando em desacordo com o
c�digo. Essa � a prov�vel explica��o para a inconsist�ncia do exemplo que abre este
cap�tulo.
Seja qual for a fonte de desacordo, um coment�rio que contradiz o c�digo �
confuso, e muita sess�o de depura��o foi realizada desnecessariamente porque um
coment�rio errado foi encarado como verdadeiro. Quando voc� alterar c�digo,
verifique se os coment�rios continuam precisos.
Os coment�rios n�o apenas devem concordar com o c�digo, como tamb�m devem
suport�-lo. O coment�rio deste exemplo est� correto - ele explica o prop�sito das
duas linhas seguintes - mas parece contradizer o c�digo. O coment�rio fala de
newline e o c�digo fala de blanks:

? time(&now);
? strcpy(date, ctime(&now));
? /* livrar-se do caractere newline final copiado de ctime */
? i = 0;
? while(date[i] >= ' ') i++;
? date[i] = 0;

Um aperfei�oamento seria reescrever o c�digo de forma mais idiom�tica:

? time(&now);
? strcpy(date, ctime(&now));
? /* livrar-se do caractere newline final copiado de ctime */
? for (i = 0; date[i] != '\n'; i++)
? ;
? date[i] = '\0';

Agora c�digo e coment�rio concordam entre si, mas ambos podem melhorar e ser mais
diretos. O problema � excluir a newline que ctime coloca no final da string que
retorna. O coment�rio deve dizer isso e o c�digo tamb�m:

time(&now);
strcpy(date, ctime(&now));
/* ctime(*) coloca newline no final da string; exclua-o */
date[strlen(date)-1] = '\0';
Esta �ltima express�o � o idioma de C para remover o �ltimo caractere de uma
string. O c�digo agora � curto, idiom�tico e claro, e o coment�rio o suporta
explicando por que ele precisa estar l�.

Esclare�a, n�o confunda. Os coment�rios devem ajudar os leitores nas partes


dif�ceis, n�o criar mais obst�culos. Este exemplo segue nossas orienta��es de
coment�rios de fun��o, e explica as propriedades pouco comuns. Por outro lado, a
fun��o � strcmp e as propriedades incomuns s�o perif�ricas da tarefa executada, que
� a implementa��o de uma interface padr�o e conhecida:

? int strcmp(char *sl, char *s2)


? /* a rotina de compara��o de string retorna -1 se s1 est� */
? /* acima de s2 em uma lista por ordem crescente, 0 se igual */
? /* l se s1 est� abaixo de s1 */
? {
? while(*sl==*s2) {
? if(*sl=='\0') return(O);
? sl++;
? s2++;
? }
? if(*sl>*s2) return(l);
? return(-l);
? }

Quando � preciso mais de algumas palavras para explicar o que est� acontecendo �
porque o c�digo deve ser reescrito. Aqui o c�digo talvez pudesse ser melhorado mas
o problema real � o coment�rio, que � quase t�o longo quanto a implementa��o, al�m
de ser confuso (para onde � "acima"?). Estamos enfatizando isso para dizer que essa
rotina � dif�cil de entender, mas como ela implementa uma fun��o padr�o, seu
coment�rio pode ajudar resumindo o comportamento e dizendo onde se origina a
defini��o. Isto � tudo o que � preciso:

/* strcmp: retorna < 0 se sl<s2, > 0 se sl>s2, 0 se igual */


/* ANSI C, se��o 4.11.4.2 */
int strcmp(const char *sl, const char *s2)
{
...
}

Os alunos aprendem que � importante comentar tudo. Os programadores devem


comentar todo o seu c�digo. Mas o prop�sito dos coment�rios pode se perder no
acompanhamento cego das regras. Os coment�rios servem para ajudar um leitor a
entender partes do programa que n�o podem ser facilmente entendidas a partir do
pr�prio c�digo. Tente ao m�ximo escrever c�digo que seja f�cil de entender. Quanto
melhor voc� fizer isso, menos coment�rios ser�o neces-s�rios. O bom c�digo precisa
de menos coment�rios do que o c�digo ruim.

� Exerc�cio 1-11. Comente estes coment�rios.

? void dict::insert(string& w)
? // retorna l se w est� no dicion�rio, caso contr�rio, retorna 0

? if (n > MAX || n % 2 > 0) // testa o n�mero par

? // Escreve uma mensagem


? // Soma um contador de linha para cada linha escrita
? ,.
? void write_message()
? {
? // incrementa o contador de linha
? line_number = line_number + 1;
? fprintf(fout, "%d %s\n%d %s\n%d %s\n",
? line_number, HEADER,
? line_number + l, BODY,
? line_number + 2, TRAILER);
? // incrementa o contador de linha
? line_number = line_number + 2;
? }
?

1.7 Por que se importar?

Neste cap�tulo falamos sobre as principais quest�es do estilo de


programa��o: nomes descritivos, clareza nas express�es, fluxo de controle dire-to,
facilidade de leitura do c�digo e coment�rios, e a import�ncia do uso consis-tente
de conven��es e idiomas para realizar tudo isso. � dif�cil dizer que essas s�o
coisas ruins.
Mas por que se preocupar com o estilo? Quem se importa com o jeito de um
programa se ele funciona bem? N�o � preciso muito tempo para deix�-lo bonito?
Afinal de contas, as regras n�o s�o arbitr�rias?
A resposta � que o c�digo bem escrito � mais f�cil de ler e entender. Ele
cont�m menos erros e provavelmente � menor do que o c�digo que foi jogado sem
cuidado e nunca foi aperfei�oado. Na correria de terminar os programas para atender
algum prazo � f�cil deixar de lado o estilo, para se preocupar com ele mais tarde.
Essa pode ser uma decis�o cara. Alguns dos exemplos deste cap�tulo mostram o que
pode dar errado, se n�o for dada aten��o suficiente para o bom estilo. O c�digo
desleixado � c�digo ruim - n�o apenas esquisito e dif�cil de ler, mas quase sempre
quebrado.
A principal observa��o � que o bom estilo deve ser uma quest�o de h�bito. Se
voc� pensar no estilo ao escrever o c�digo, e se voc� revisar e melhorar esse
c�digo, estar� desenvolvendo bons h�bitos. Depois que eles se tornarem autom�ticos,
seu subconsciente tomar� conta de muitos dos detalhes para voc�, e at� mesmo o
c�digo que voc� produzir sob press�o ser� melhor.

Leitura suplementar

Como dissemos no in�cio do cap�tulo, escrever c�digo bom tem muito em comum
com escrever em sua pr�pria l�ngua. O livro The Elements of Style, de Strunk e
White (Allyn & Bacon), ainda � o melhor livro sobre como escrever bem em ingl�s.
Este cap�tulo aproveita as abordagens dos livros The Elements of Programming
Style, de Brian Kernighan e P. J. Plauger (McGraw-Hill, 1978). O livro Writing
Solid Code, de Steve Maguire (Microsoft Press, 1993), � uma fonte excelente de
consulta sobre programa��o. H� tamb�m discuss�es �teis sobre estilo no livro Code
Complete, de Steve McConnell (Microsoft Press, 1993), e no livro Expert C
Programming: Deep C Secrets, de Peter van der Linden (Prentice Hall, 1994).

Algoritmos e
estruturas de dados

No final, apenas a familiaridade com as ferramentas e t�cnicas da �rea fornece


a solu��o certa para determinado problema, e apenas uma determinada
quantidade de experi�ncia fornece resultados profissionais consistentes.

Raymond Fielding, The Technique of Special Effects Cinematography


O estudo dos algoritmos e das estruturas de dados � uma das bases da ci�ncia
da computa��o, um campo rico de t�cnicas elegantes e an�lises matem�-ticas
sofisticadas. Ele � mais do que apenas divers�o e jogos para aqueles com inclina��o
te�rica: um bom algoritmo ou estrutura de dados possibilita a solu��o de um
problema em segundos, que de outra forma levaria anos.
Em �reas especializadas, tais como gr�ficos, an�lise, an�lise num�rica e
simula��o, a habilidade de solucionar problemas depende muito dos algoritmos e
estruturas de dados do estado da arte. Se voc� est� desenvolvendo programas em uma
�rea nova, deve descobrir o que j� � conhecido; caso contr�rio, vai perder seu
tempo fazendo trabalho ruim que outras pessoas podem j� ter feito bem.
Todo programa depende dos algoritmos e das estruturas de dados, mas poucos
programas dependem da inven��o de algoritmos e estruturas de dados novos. Mesmo
dentro de um programa complicado como um compilador ou browser da Web, a maioria
das estruturas de dados s�o arrays, listas, �rvores e tabelas hash. Quando um
programa precisa de algo mais elaborado, provavel-mente se basear� nessas
estruturas simples. Da mesma forma, para a maioria dos programadores, a tarefa �
saber quais algoritmos e estruturas de dados est�o dispon�veis e entender como
escolher entre as alternativas.
Em resumo, h� apenas meia d�zia de algoritmos b�sicos que aparecem em quase
todos os programas - primariamente de pesquisa e classifica��o - e mesmo esses
quase sempre est�o inclu�dos em bibliotecas. Da mesma forma, quase todas as
estruturas de dados se originam de algumas estruturas funda-mentais. Assim sendo, o
material abordado neste cap�tulo ser� conhecido de quase todos os programadores.
Escrevemos vers�es de trabalho para tornar a discuss�o concreta, e voc� pode criar
c�digo se for preciso, mas s� fa�a isso depois de investigar aquilo que a linguagem
de programa��o e suas bibliotecas oferecem.

2.1 Pesquisando

Nada � melhor do que um array para armazenar dados tabulares est�-ticos. A


inicializa��o no tempo de compila��o torna barata e f�cil a constru��o desses
arrays. (Em Java, a inicializa��o ocorre no tempo de execu��o, mas esse � um
detalhe de implementa��o sem import�ncia, a menos que os arrays sejam grandes.) Em
um programa para detectar palavras que s�o muito usadas na prosa ruim, podemos
escrever.

char *flab[] = {
"actually",
"just",
"quite",
"really",
NULL };

A rotina de pesquisa precisa saber quantos elementos h� no array. Uma


maneira de descobrir isso � passar o comprimento como um argumento; outra maneira
usada aqui � colocar um marcador NULL no final do array:

/* lookup: pesquisa sequencial de palavra no array */


int 1ookup(char *word, char *array[])
{
int i;
for (i = 0; array [i] != NULL; i++)
if (strcmp(word, array[i]) == 0)
return i;
return -1;
}
Em C e C++, um par�metro que � um array de strings pode ser declarado como
char *array[] ou char **array. Embora essas formas sejam equiva-lentes, a primeira
deixa mais claro o modo como o par�metro ser� usado.
Esse algoritmo de pesquisa se chama pesquisa seq�encial porque ele verifica
um elemento de cada vez para ver se � o elemento desejado. Quando a quantidade de
dados � pequena, a pesquisa seq�encial � suficientemente r�pida. H� rotinas de
biblioteca padr�o para realizar a pesquisa seq�encial em tipos de dados
espec�ficos. Por exemplo, fun��es como strchr e strstr pesquisam a primeira
inst�ncia de determinado caractere ou substring em uma string C ou C++, a classe
Java String tem um m�todo indexOf, e a gen�rica find da C++ se aplica � maioria dos
tipos de dados. Se tal fun��o existir para o tipo de dados que voc� tem, use-a.
A pesquisa seq�encial � f�cil, mas a quantidade de trabalho � diretamente
proporcional � quantidade de dados a ser pesquisada. O dobro do n�mero de ele-
mentos dobrar� o tempo da pesquisa se o item desejado n�o estiver presente. Esse �
um relacionamento linear - o tempo de execu��o � uma fun��o linear do tamanho dos
dados - portanto, esse m�todo tamb�m � conhecido como pesquisa linear.
Aqui temos um trecho de um array de tamanho mais realista, tirado de um
programa que analisa a HTML, a qual define nomes textuais para bem mais de uma
centena de caracteres individuais:

typedef struct Nameval Nameval;


struct Nameval {
char *name;
int value;
};

/* Caracteres HTML, por exemplo, AElig � ligadura de A e E. */


/* Valores de codifica��o Unicode/ISO10646. */

Nameval htmlchars[] = {
"AElig", 0x00C6,
"Aacute", 0x00C1,
"Acire", 0x00C2,
/* ... */
"zeta", 0x03B6,
};

Para um array maior como esse, � mais eficiente usar a pesquisa bin�ria. O
algoritmo da pesquisa bin�ria � uma vers�o ordenada da maneira como procuramos as
palavras em um dicion�rio. Verifique o elemento do meio. Se aquele valor for maior
do que o que estamos procurando, procure na primeira metade; caso contr�rio,
procure na segunda metade. Repita at� que o item desejado seja encontrado ou
determinado como n�o-presente.
Para a pesquisa bin�ria, a tabela deve estar classificada, como ela est�
aqui (isso � bom estilo e as pessoas tamb�m encontram as coisas mais r�pido nas
tabelas classificadas), e n�s precisamos saber o tamanho da tabela. A macro NELEMS
do Cap�tulo l pode ajudar:

printf("The HTML table h�s %d words\n", NELEMS(htmlchars));

Uma fun��o de pesquisa bin�ria para esta tabela seria assim:

/* lookup: pesquisa bin�ria por nome na tabela; retornar �ndice */


int lookup(char *name, Nameval tab[], int ntab)
{
int low, high, mid, cmp;

low = 0;
high = ntab - 1;
while (low <= high) {
mid = (low + high) / 2;
cmp = strcmp(name, tab[mid].name);
if (cmp < 0)
high = mid - 1;
else if (cmp > 0)
low = mid + 1;
else /* coincid�ncia encontrada */
return mid;
}
return -1; /* nenhuma coincid�ncia */
}

Juntando tudo isso, para pesquisar caracteres html escrevemos

half = lookup("fracl2", htmlchars, NELEMS(htmlchars));

para descobrir o �ndice de array do caractere 1/2.


A pesquisa bin�ria elimina metade dos dados em cada etapa. O n�mero de
etapas, portanto, � proporcional ao n�mero de vezes pelas quais podemos dividir n
por 2 at� ficarmos com um �nico elemento. Ignorando o arredondamento, isso � log2n.
Se tivermos 1.000 itens para pesquisar, a pesquisa linear leva at� 1.000 etapas,
enquanto a pesquisa bin�ria precisa de cerca de dez etapas. Se tivermos um milh�o
de itens, a linear precisa de um milh�o de etapas e a bin�ria precisa de 20. Quanto
mais itens, maior a vantagem da pesquisa bin�ria. Al�m de determinado tamanho de
entrada (o qual varia dependendo da implementa��o), a pesquisa bin�ria � mais
r�pida do que a pesquisa linear.

2.2 Classificando

A pesquisa bin�ria s� funciona quando os elementos est�o classificados. Se


voc� vai realizar pesquisas repetidas em algum conjunto de dados, � melhor
classificar uma vez e depois usar a pesquisa bin�ria. Se o conjunto de dados j� �
conhecido, ele pode ser classificado quando o programa for escrito e constru�do
usando a inicializa��o no tempo de compila��o. Caso contr�rio, ele deve ser
classificado quando o programa for executado.
Um dos melhores algoritmos de classifica��o � o quicksort, inventado em 1960
por C.A.R. Hoare. O quicksort � um bom exemplo de como evitar computa-��o extra.
Ele funciona por meio do particionamento de um array em elementos pequenos e
grandes:

escolha um elemento do array (o "pivot").


particione os outros elementos em dois grupos;
"os pequenos" que s�o menores do que o valor pivot, e
"os grandes" que s�o maiores do que ou iguais ao valor pivot.
classifique novamente cada grupo.

Quando esse processo termina o array est� na ordem. O quicksort � r�pido


porque depois que se sabe que um elemento � menor do que o valor pivot, n�o
precisamos compar�-lo com nenhum dos valores grandes. Da mesma forma, os valores
grandes n�o s�o comparados com os pequenos. Isso o torna muito mais r�pido do que
os m�todos de classifica��o simples, tais como a classifica��o por inser��o e a
classifica��o "bubble" que compara cada elemento diretamente com todos os outros.
O quicksort � pr�tico e eficiente. Ele j� foi muito estudado e existem
in�meras varia��es. A vers�o que apresentamos aqui � a implementa��o mais simples,
mas certamente n�o � a mais r�pida.
Esta fun��o quicksort classifica um array de inteiros:

/* quicksort: classificar v[0]..v[n-l] na ordem crescente */


void quicksort(int v[], int n)
{
int i, last;
if (n <= 1) /* nada a fazer */
return;
swap(v, 0, rand() % n); /* mover o elemento pivot para v[0] */ last = 0;
for (i = 1; i < n; i++) /* parti��o */
if (v[1] < v[0])
swap(v, ++last, i);
swap(v, 0, last); /* restaurar pivot */
quicksort(v, last); /* classificar cada parte */
quicksort(v+last+1, n-last-1); /* novamente */
}

A opera��o swap, que intercambia dois elementos, aparece tr�s vezes no


quicksort; portanto, � melhor torn�-la uma fun��o separada:

/* swap: intercambia v[i] e v[j] */


void swap(int v[], int i, int j)
{
int temp;

temp = v[i];
v[1] = v[j];
v [j] = temp;
}

O particionamento seleciona um elemento aleat�rio como o pivot, passa-o


temporariamente para a frente e depois troca os elementos restantes, trocando
aqueles que s�o menores do que o pivot ("os pequenos") para o in�cio (na
localiza��o last) e os grandes para o final (na localiza��o i). No in�cio do
proces-so, logo depois que o pivot foi passado para a frente, last = 0 e os
elementos i =1 at� n-1 n�o s�o examinados:

Na parte superior do loop for, os elementos de 1 at� last s�o estritamen-te


menores do que o pivot, os elementos last+1 at� i-1 s�o maiores do que ou iguais ao
pivot, e os elementos i at� n-1 ainda n�o foram examinados. At� v[i] >= v[0], o
algoritmo pode trocar v[i] por ele mesmo; isso faz perder algum tempo mas n�o �
preciso se preocupar.

Depois que todos os elementos foram particionados, o elemento 0 � troca-do


com o elemento last, para colocar o elemento pivot em sua posi��o final. Isso
mant�m a ordem final. Agora o array est� assim:

O mesmo processo se aplica aos subarrays esquerdo e direito; quando isso


termina, todo o array foi classificado.
Qual � a rapidez do quicksort? No melhor caso poss�vel:
� a primeira passagem particiona n elementos em dois grupos de cerca de n/2 cada
um.
� o segundo n�vel particiona dois grupos, cada um com cerca de n/2 elementos, em
quatro grupos cada um de cerca de n/4.
� o n�vel seguinte particiona quatro grupos de cerca de n/4 em oito grupos de cerca
de n/8.
� e assim por diante.
Isso continua at� mais ou menos log2n n�veis, de modo que a quantidade total
de trabalho no melhor caso � proporcional a n + 2xn/2 + 4xn/4 + 8x n/8 ...(log2n
termos), o que � nlog2n. Na m�dia, isso significa apenas um pouco mais de trabalho.
Geralmente se usam os logaritmos de base 2. Assim sendo, dizemos que o quicksort
leva um tempo proporcional a nlogn.
Essa implementa��o do quicksort � a mais clara para exposi��o, mas ela tem
um ponto fraco. Se cada op��o do pivot dividir os valores de elemento em dois
grupos quase iguais, nossa an�lise est� correta, mas se a divis�o for desigual com
muita freq��ncia, o tempo de execu��o pode crescer mais como n2. Nossa
implementa��o usa um elemento aleat�rio como o pivot para reduzir as chances de
dados de entrada incomuns causarem um n�mero grande de divis�es desiguais. Mas se
todos os valores de entrada forem iguais, nossa implementa��o divide apenas um
elemento de cada vez e, assim, � executada no tempo propor-cional a n2.
O comportamento de alguns algoritmos depende muito dos dados de entrada. As
entradas ruins ou infelizes podem fazer com que um algoritmo bem comportado tenha
execu��o extremamente lenta, ou use muita mem�ria. No caso do quicksort, embora uma
implementa��o simples como a nossa possa �s vezes ter execu��o lenta, as
implementa��es mais sofisticadas podem reduzir as chances de comportamento
patol�gico a quase zero.

2.3 Bibliotecas

As bibliotecas padr�o da C e C++incluem fun��es de classifica��o que devem


ser robustas contra entradas adversas, e tamb�m ajustadas para serem executadas o
mais r�pido poss�vel.
As rotinas de biblioteca s�o preparadas para classificar qualquer tipo de
dados, mas em contrapartida precisamos adaptar sua interface, o que pode ser um
pouco mais complicado do que aquilo que mostramos acima. Em C, a fun��o de
biblioteca se chama qsort, e precisamos fornecer uma fun��o de compara��o para ser
chamada por qsort sempre que ela precisar comparar dois valores. Como os valores
podem ser de qualquer tipo, a fun��o de compara��o recebe dois ponteiros void* para
os itens de dados a serem comparados. A fun��o disp�e os ponteiros no tipo
adequado, extrai os valores de dados, os compara e retorna o resultado (negativo,
zero ou positivo, dependendo do primeiro valor ser menor do que, igual a, ou maior
do que o segundo).
Aqui temos uma implementa��o de um caso comum que classifica um array de
strings. Definimos uma fun��o scmp para dispor os argumentos e chamar strcmp para
fazer a compara��o.

/* scmp: compara��o de string entre *pl e *p2 */


int scmp(const void *pl, const void *p2)
{
char *vl, *v2;

vi = *(char **) pi;


v2 = *(char **) p2;
return strcmp(vl, v2);
}

Poder�amos escrever isso como uma fun��o de uma linha, mas as vari�-veis
tempor�rias tornam o c�digo mais f�cil de ler.
N�o podemos usar strcmp diretamente como a fun��o de compara��o porque qsort
passa o endere�o de cada entrada do array, &str[i] (do tipo char**), e n�o str[i]
(do tipo char*), como mostra esta figura:

Para classificar os elementos str[0] at� str[N-1] de um array de strings,


qsort deve ser chamado com o array, seu comprimento, o tamanho dos itens que est�o
sendo classificados e a fun��o de compara��o:

char *str[N];

qsort(str, N, sizeof(str[0]), scmp);

Aqui temos uma fun��o icmp semelhante para comparar inteiros:

/* icmp: compara��o de inteiro entre *pl e *p2 */


int icmp(const void *pl, const void *p2)
{
int v1, v2;

v1 = *(int *)p1;
v2 = *(int *)p2;
if (v1 < v2)
return -1;
else if (v1 == v2)
return 0;
else
return 1;
}

Poder�amos escrever

? return vl-v2;

mas se v2 � grande e positivo e v1 � grande e negativo, ou vice-versa, o overflow


resultante produziria uma resposta incorreta. A compara��o direta � longa, por�m
mais segura.
A chamada de qsort exige o array, seu comprimento, o tamanho dos itens que
est�o sendo classificados e a fun��o de compara��o:

int arr[N];

qsort(arr, N, sizeof(arr[0]), icmp);

A ANSI C tamb�m define uma rotina de pesquisa bin�ria, bsearch. Assim como
qsrot, bsearch exige um ponteiro para uma fun��o de compara��o (quase sempre o
mesmo usado para qsort); ela retorna um ponteiro para o elemento coincidente ou
NULL se ele n�o for encontrado. Aqui temos nossa rotina lookup em HTML, reescrita
para usar bsearch:

/* lookup: usar bsearch para localizar name em uma tabela,


retornar indice */
int lookup(char *name, Nameval tab[], int ntab)
{
Nameval key, *np;

key.name = name;
key.value = 0; /*n�o usado; qualquer coisa serve */
np = (Nameval *)bsearch(&key, tab, ntab,
sizeof(tab[0]), nvcmp);
if (np == NULL)
return -1;
else
return np-tab;
}
Assim como acontece com qsort, a rotina de compara��o recebe o ende-re�o dos
itens a serem comparados, de modo que a chave deve ter aquele tipo. Neste exemplo,
precisamos construir uma entrada Nameval falsa que ser� pas-sada para a rotina de
compara��o. A rotina de compara��o em si � uma fun��o nvcmp que compara dois itens
Nameval chamando strcmp em seus componentes de string, ignorando seus valores:

/* nvcmp: compara dois nomes Nameval */


int nvcmp(const void *va, const void *vb)
{
const Nameval *a, *b;

a = (Nameval *)va;
b = (Nameval *)vb;
return strcmp(a->name, b->name);
}

Isso � parecido com scmp, s� que as strings s�o armazenadas como membros de
uma estrutura.
Fornecer a chave significa que bsearch fornece menos poder do que qsort. Uma
boa rotina de classifica��o geral toma uma ou duas p�ginas de c�digo, enquanto a
pesquisa bin�ria n�o � muito mais longa do que o c�digo necess�rio para fazer a
interface com bsearch. No entanto, � bom usar bsearch em vez de escrever seu
pr�prio c�digo. Ao longo dos anos a pesquisa bin�ria provou ser surpreendentemente
dif�cil para os programadores.
A biblioteca C++padr�o tem um algoritmo gen�rico chamado sort que garante o
comportamento O(nlog n). O c�digo � mais f�cil porque n�o precisa de convers�es ou
tamanhos de elemento, al�m de n�o exigir uma fun��o expl�cita de compara��o para os
tipos que t�m uma rela��o de ordem.

int arr[N];

sort(arr, arr+N);

A biblioteca C ++ tamb�m tem rotinas gen�ricas de pesquisa bin�ria, com vanta-gens


notacionais similares.

� Exerc�cio 2-1. A forma mais natural de expressar quicksort � recursivamente.


Escreva-o iterativamente e compare as duas vers�es. (Hoare descreve como foi
dif�cil trabalhar com o quicksort de forma iterativa, e como ele se ajustou bem
quando foi feito recursivamente.) ?

2.4 Um quicksort Java

Em Java a situa��o � diferente. As primeiras vers�es n�o tinham fun��o


padr�o de classifica��o; portanto, precisamos escrever nossa pr�pria fun��o. As
vers�es mais recentes fornecem uma fun��o sort, a qual opera nas classes que
implementam a interface Comparable; portanto, agora podemos pedir que a biblioteca
classifique para n�s. Mas, como as t�cnicas s�o �teis em outras situa��es, nesta
se��o vamos ver os detalhes da implementa��o de quicksort em Java.
� f�cil adaptar um quicksort para cada tipo que desejamos classificar, mas �
mais instrutivo escrever uma classifica��o gen�rica que pode ser chamada por
qualquer tipo de objeto, mais no estilo da interface qsort.
Uma grande diferen�a da C ou C ++ � que na Java n�o � poss�vel passar uma
fun��o de compara��o para outra fun��o; n�o h� ponteiros de fun��o. Em vez disso,
criamos uma interface cujo �nico conte�do � uma fun��o que compara dois Objects.
Para cada tipo de dados a ser classificado, criamos uma classe com uma fun��o
membro que implementa a interface daquele tipo de dados. Passa-mos uma inst�ncia
daquela classe para a fun��o sort, a qual por sua vez usa a fun��o de compara��o
dentro da classe para comparar os elementos.
Come�amos definindo uma interface chamada Cmp que declara um �nico membro,
uma fun��o cmp que compara dois Objects:

interface Cmp {
int cmp(Object x, Object y);
}

Depois, podemos escrever fun��es de compara��o que implementam essa


interface. Por exemplo, esta classe define uma fun��o que compara Integers:

// Icmp: compara��o de inteiro


class Icmp implements Cmp {
public int cmp(Object o1, Object o2)
{
int i1 = ((Integer) o1).intValue();
int i2 = ((Integer) o2).intValue();
if (i1 < i 2)
return -1;
else if (i1 == i2)
return 0;
else
return 1;
}
}

e isso compara Strings:

// Scmp: compara��o de string


class Scmp implements Cmp {
public int cmp(Object o1, Object o2)
{
String s1 = (String)o1;
String s2 = (String)o2;
return sl.compareTo(s2);
}
}

Podemos classificar apenas os tipos derivados de Object com este mecanismo.


Essa classifica��o n�o pode ser aplicada aos tipos b�sicos, como int ou double. Por
esse motivo, classificamos Integers em vez de ints.
Com esses componentes agora podemos traduzir a fun��o quicksort da C para a
Java, e podemos fazer com que ela chame a fun��o de compara��o de um objeto Cmp
passado como um argumento. A altera��o mais significativa � o uso dos �ndices left
e right, uma vez que Java n�o tem ponteiros para os arrays.

// Quicksort.sort: quicksort v[left]..v[right]


static void sort(0bject[] v, int left, int right, Cmp cmp)
{
int i, last;

if (left >= right) // nada a fazer


return;
swap(v, left, rand(left,right)); // mover o elemento pivot last = left;
// para v [left]
for (i = left+1; i <= right; i++) // parti��o
if (cmp.cmp(v[i], v[left]) < 0)
swap(v, ++last, i);
swap(v, left, last); // restaurar elemento pivot
sort(v, left, last-1, cmp); // classificar cada parte sort(v, last+1, right,
cmp); // novamente
}

Quicksort.sort usa cmp para comparar um par de objetos, e chama swap antes de
intercambi�-los.

// Quicksort.swap: swap v[i] and v[j]


static void swap(0bject[] v, int i, int j)
{
Object temp;

temp = v[i];
v[i] = v[j];
v[j] = temp;
}

A gera��o aleat�ria de n�meros � feita por uma fun��o que produz um inteiro
aleat�rio na faixa left at� right, inclusive:

static Random rgen = new Random();

// Quicksort.rand: retornar inteiro aleat�rio em [left, right]


static int rand(int left, int right)
{
return left + Math.abs(rgen.nextInt())%(right-left+1);
}

Calculamos o valor absoluto, usando Math.abs, porque o gerador de n�meros


aleat�rios de Java retorna inteiros negativos bem como positivos.
As fun��es sort, swap e rand, e o objeto de gerador rgen s�o os mem-bros de
uma classe Quicksort.
Finalmente, para chamar Quicksort.sort para classificar um array String,
poder�amos dizer:

String[] sarr = new String [n];

// preencher n elementos de sarr...

Quicksort.sort(sarr, 0, sarr.length-1, new Scmp());

Isso chama sort com um objeto de compara��o de strings criado para a


ocasi�o.

� Exerc�cio 2-2. Nosso quicksort Java faz uma quantidade razo�vel de convers�o de
tipo � medida que os itens s�o colocados de seu tipo original (como Integer) em
Object, e de volta novamente. Experimente uma vers�o de Quicksort.sort que usa o
tipo espec�fico que est� sendo classificado para estimar a penalidade para o
desempenho imposta pelas convers�es de tipo. ?

2.5 O-Notation

Descrevemos a quantidade de trabalho a ser feito por determinado algoritmo


em termos de n, o n�mero de elementos da entrada. Os dados n�o classificados de
pesquisa podem levar um tempo proporcional a n; se usarmos a pesquisa bin�ria nos
dados classificados, o tempo ser� proporcional a logn. Os tempos de classifica��o
podem ser proporcionais a n2 ou nlog n.
Precisamos de um modo de tornar tais declara��es mais precisas e, ao mesmo
tempo, abstrair os detalhes como velocidade da CPU e qualidade do compilador (e do
programador). Queremos comparar os tempos de execu��o e os requisitos de espa�o dos
algoritmos independentemente da linguagem de programa��o, do compilador, da
arquitetura de m�quina, da velocidade do processador, da carga do sistema e de
outros fatores complicadores.
H� uma nota��o padr�o para essa id�ia chamada "O-notation". Seu par�metro
b�sico � n, o tamanho de uma inst�ncia do problema, e a complexi-dade ou tempo de
execu��o � expresso como uma fun��o de n. O "O" � a ordem, como em "A pesquisa
bin�ria � O(logn); ela toma a ordem das etapas de logn para pesquisar um array de n
itens". A nota��o O(f(n()) significa que, depois que n fica grande, o tempo de
execu��o � proporcional a no m�ximo f(n), por exemplo, O(n2) ou O(nlogn).
Estimativas assint�ticas como essa s�o valiosas para as an�lises te�ricas e muito
�teis para compara��es brutas de algoritmos, mas na pr�tica os detalhes podem fazer
diferen�a. Por exemplo, um algoritmo de overhead baixa O(n2) pode ser executado
mais r�pido do que um algoritmo de overhead alta O(nlogn) para valores pequenos de
n, mas inevitavelmente, se n ficar suficientemente grande, o algoritmo com o
comportamento funcional de crescimento lento ser� mais r�pido.
Tamb�m devemos fazer distin��o entre o comportamento de pior caso e o
comportamento esperado. � dif�cil definir "esperado", uma vez que ele depende de
suposi��es sobre os tipos de entradas que ser�o dados. Geralmente podemos precisar
o pior caso, embora isso possa ser enganoso. O pior tempo de execu��o do quicksort
� O(n2), mas o tempo esperado � O(nlogn). Selecionando o elemento pivot com cuidado
a cada vez, podemos reduzir a probabilidade do comportamento quadr�tico ou O(n2) a
essencialmente zero. Na pr�tica, um quicksort bem implementado geralmente �
executado no tempo O(nlogn).
Estes s�o os casos mais importantes:

Nota��o Nome Exemplo O(1) constante �ndice de array O(logn)logar�tmicopesquisa


bin�riaO(n)linearcompara��o de stringO(nlogn)nlognquicksortO(n2)quadr�ticom�todos
simples de classifica��oO(n3)c�bicomultiplica��o de
matrizO(2n)exponencialparticionamento de conjunto
O acesso de um item de um array � uma opera��o de tempo constante ou O(1).
Um algoritmo que elimina metade da entrada em cada est�gio, como na pesquisa
bin�ria, geralmente leva o tempo O(logn). A compara��o de duas strings de n
caracteres com strcmp � O(n). Os algoritmos exponenciais geral-mente s�o muito
caros, a menos que n seja muito pequeno, uma vez que a adi��o de um item ao
problema dobra o tempo de execu��o. Infelizmente h� muitos problemas, tais como o
famoso "Problema do Vendedor Viajante", para os quais s� s�o conhecidos os
algoritmos exponenciais. Quando esse � o caso, os algoritmos que encontram
aproxima��es para a melhor resposta quase sempre s�o substitu�dos.

� Exerc�cio 2-3. Quais s�o algumas das seq��ncias de entrada que podem fazer com
que uma implementa��o de quicksort exiba o comportamento de pior caso? Tente
encontrar alguma que fa�a sua vers�o de biblioteca ser executada lentamente.
Automatize o processo para poder especificar e executar um n�mero grande de
experi�ncias de forma f�cil. ?

� Exerc�cio 2-4. Projete e implemente um algoritmo que classifique um array de n


inteiros o mais lentamente poss�vel. Voc� tem de jogar limpo: o algoritmo deve
fazer progresso e eventualmente ser encerrado, e a implementa��o n�o deve enganar
com truques como loops que fazem perder tempo. Qual � a complexidade do seu
algoritmo como fun��o de n? ?

2.6 Aumentando os arrays

Os arrays usados nas �ltimas se��es eram est�ticos, com seu tamanho e
conte�do fixos no tempo de compila��o. Se as fracas tabelas de caracteres do Word
ou HTML tivessem de ser modificadas no tempo de execu��o, uma hash table seria a
estrutura de dados mais apropriada. O aumento de um array classificado inserindo n
elementos de uma s� vez � uma opera��o O(n2) que deve ser evitada quando n for
grande.
Entretanto, freq�entemente precisamos controlar um n�mero vari�vel mas
pequeno de coisas e os arrays ainda podem ser o m�todo preferido. Para minimizar o
custo da aloca��o, o array deve ser redimensionado em peda�os, e por quest�es de
limpeza ele deve ser reunido �s informa��es necess�rias para atualiz�-lo. Em C++ ou
Java, isso seria feito com as classes das bibliotecas padr�o. Em C, n�s podemos
conseguir um resultado semelhante a uma struct.
O c�digo abaixo define um array que pode ser aumentado de itens Nameval. Os
itens novos s�o adicionados no final do array, o qual � aumentado segundo a
necessidade para criar espa�o. Todo elemento pode ser acessado por meio de seu
subscrito em tempo constante. Isso � an�logo �s classes de vetores das bibliotecas
de Java e C++.

typedef struct Nameval Nameval;


struct Nameval {
char *name;
int value;
};

struct NVtab {
int nval; /* n�mero atual de valores */
int max; /* n�mero alocado de valores */
Nameval *nameval; /* array de pares nome-valor */
} nvtab;

enum { NVINIT = l, NVGROW = 2 };

/* addname: adiciona nome e valor novos a nvtab */


int addname(Nameval newname)
{
Nameval *nvp;
if (nvtab.nameval == NULL) { /* primeira vez */
nvtab.nameval =
(Nameval *) malloc(NVINIT * sizeof(Nameval));
if (nvtab.nameval == NULL)
return -1;
nvtab.max = NVINIT;
nvtab.nval = 0;
}
else if (nvtab.nval >= nvtab.max) { /* grow */
nvp = (Nameval *) realloc(nvtab.nameval,
(NVGROW*nvtab.max) * sizeof(Nameval));
if (nvp == NULL)
return -1;
nvtab.max *= NVGROW;
nvtab.nameval = nvp;
}
nvtab.nameval[nvtab.nval] = newname;
return nvtab.nval++;
}

A fun��o addname retorna o �ndice do item que acabou de ser adicionado, ou -1 se


tiver ocorrido algum erro.
A chamada de realloc aumenta o array para o tamanho novo, preser-vando os
elementos existentes, e retorna um ponteiro para ele ou NULL se n�o houver mem�ria
suficiente. Ao dobrarmos o tamanho de cada realloc, mante-mos o custo esperado de
copiar cada constante de elemento; se o array foi aumentado em um elemento em cada
chamada, o desempenho poderia ser O(n2). Como o endere�o do array pode mudar quando
ele for realocado, o restante do programa deve se referir aos elementos do array
por subscritos, n�o por ponteiros. Observe que o c�digo n�o diz:

? nvtab.nameval = (Nameval *) realloc(nvtab.nameval,


? (NVGROW*nvtab.max) * sizeof(Nameval));

Desta forma, se a realoca��o falhasse, o array original se perderia.


Come�amos com um valor inicial bastante pequeno (NVINIT = 1) para o tamanho
do array. Isso for�a o programa a aumentar seus arrays imediatamente, garantindo
assim que essa parte do programa seja exercida. O tamanho inicial pode aumentar
depois que o c�digo entrar em uso de produ��o, embora o custo de come�ar pequeno
seja desprez�vel.
O valor de retorno de realloc n�o precisa ser convertido para o seu tipo
final porque em C void* � promovido automaticamente. Mas a C++ n�o faz isso. Nela a
convers�o � requerida. Pode-se argumentar que � mais seguro fazer a convers�o (por
quest�es de limpeza, honestidade), ou n�o fazer a convers�o (ela pode ocultar os
erros verdadeiros). Preferimos fazer a convers�o porque isso torna o programa legal
tanto em C quanto em C++. O pre�o � menos verifica��o de erros para o compilador C,
mas isso � compensado pela verifica��o extra dispon�vel com o uso de dois
compiladores.
A exclus�o de um nome pode ser complicada, porque devemos decidir o que
fazer com a lacuna resultante no array. Se a ordem dos elementos n�o importar, �
mais f�cil trocar o �ltimo elemento e coloc�-lo na lacuna. Se a ordem precisa ser
preservada, devemos mover os elementos al�m da lacuna uma posi��o abaixo:

/* delname: remover o primeiro nameval coincidente de nvtab */


int delname(char *name)
{
int i;

for (i = 0; i < nvtab.nval; i++)


if (strcmp(nvtab.nameval[i].name, name) == 0) {
memmove(nvtab.nameval+i, nvtab.nameval+i+1,
(nvtab.nval-(i+l)) * sizeof(Nameval));
nvtab.nval--;
return 1;
}
return 0;
}

A chamada de memmove espreme o array movendo os elementos uma posi��o abaixo;


memmove � uma rotina padr�o de biblioteca para copiar blocos de mem�ria com tamanho
arbitr�rio.
O padr�o ANSI C define duas fun��es: memcpy, que � r�pida mas pode sobrepor
a mem�ria se a origem e o destino se sobrepuserem; e memmove, que pode ser mais
lenta mas sempre estar� correta. O programador n�o deve ter a responsabilidade de
escolher entre corre��o e velocidade; s� deve haver uma fun��o. Finja que h�, e use
sempre memmove.
Poder�amos substituir a chamada de memmove pelo seguinte loop:

int j;
for (j = i; j < nvtab.nval-1; j++)
nvtab.nameval[j] = nvtab.nameval[j+1];

Preferimos usar memmove porque ela evita o erro f�cil de copiar os elementos
na ordem errada. Se estiv�ssemos inserindo em vez de excluindo, o loop precisaria
contar abaixo, n�o acima, para evitar sobrepor os elementos. Chamando memmove n�s
n�o precisamos pensar nisso todas as vezes.
Uma alternativa para a movimenta��o dos elementos do array seria marcar os
elementos exclu�dos como n�o-usados. Depois, para adicionar um item novo, primeiro
pesquise um espa�o n�o-usado e aumente o vetor apenas se nenhum for encontrado.
Neste exemplo, um elemento pode ser marcado como n�o-usado definindo seu campo de
nome como NULL.
Os arrays s�o a forma mais simples de agrupar dados. N�o � por acaso que a
maioria das linguagens fornece arrays indexados eficientes e convenientes e
representa as strings como arrays de caracteres. Os arrays s�o f�ceis de usar,
fornecem acesso O(1) a qualquer item, funcionam bem com a pesquisa bin�ria e
quicksort, e t�m pouca overhead de espa�o. Para os conjuntos de dados de tamanho
fixo, que podem at� mesmo ser constru�dos no tempo de compila��o, ou para cole��es
pequenas garantidas de dados, os arrays s�o imbat�veis. Mas manter um conjunto de
valores que muda em um array pode ser caro; portanto, se o n�mero de elementos for
imprevis�vel e potencialmente grande, � melhor usar outra estrutura de dados.

� Exerc�cio 2-5. No c�digo acima, delname n�o chama realloc para retornar a mem�ria
liberada pela exclus�o. Isso vale a pena? Como voc� decide se deve fazer isso? ?

� Exerc�cio 2-6. Implemente as altera��es necess�rias em addname e delname para


excluir itens marcando os itens exclu�dos como n�o-usados. Como o restante do
programa � isolado dessa altera��o? ?

2.7 Listas

Depois dos arrays, as listas s�o a estrutura de dados mais comum nos
programas t�picos. Muitas linguagens t�m tipos de lista incorporados - algumas,
tais como a Lisp, se baseiam neles - mas em C devemos constru�-las n�s mesmos. Em
C++ e Java, as listas s�o implementadas por uma biblioteca, mas ainda temos de
saber como e quando us�-la. Nesta se��o vamos discutir as listas em C, mas as
li��es t�m aplica��o mais ampla.
Uma lista encadeada simples � um conjunto de itens, cada um com dados e um
ponteiro para o pr�ximo item. A cabe�a da lista � um ponteiro para o primeiro item
e o final da lista � marcado por um ponteiro null. A seguir temos a representa��o
de uma lista com quatro elementos:

H� v�rias diferen�as importantes entre os arrays e as listas. A primeira �


que o arrays t�m tamanho fixo mas uma lista tem sempre exatamente o tamanho que ela
precisa ter para conter seu conte�do, al�m de uma overhead de armazenamento por
item para conter os ponteiros. Segunda diferen�a, as listas podem ser reorganizadas
trocando alguns ponteiros, o que � mais barato do que a movimenta��o de bloco
necess�ria em um array. Finalmente, quando os itens s�o inseridos ou exclu�dos os
outros itens n�o s�o movidos; se armazenarmos os ponteiros para os elementos em
alguma outra estrutura de dados, eles n�o ser�o invalidados pela altera��es feitas
na lista.
Essas diferen�as sugerem que se o conjunto de itens muda, freq�entemente,
particularmente se o n�mero de itens � imprevis�vel, uma lista � o modo de
armazen�-los. Por compara��o, um array � melhor para os dados relativamente
est�ticos.
H� meia d�zia de opera��es fundamentais de lista: adicionar um item novo na
frente ou no final, encontrar um item espec�fico, adicionar um item novo antes ou
depois de um item especificado e talvez excluir um item. A simplicidade das listas
facilita a adi��o de outras opera��es apropriadas.
Em vez de definir um tipo List expl�cito, a maneira comum de usar as lista
em C � come�ar com um tipo para os elementos, tal como nosso Nameval HTML e
adicionar um ponteiro que se vincula ao elemento seguinte:

typedef struct Nameval Nameval;


struct Nameval {
char *name;
int value;
Nameval *next; /* na lista */
};

� dif�cil inicializar uma lista n�o-vazia no tempo de compila��o; portanto,


ao contr�rio dos arrays, as listas s�o constru�das dinamicamente. Primeiro, preci-
samos de uma maneira para construir um item. A abordagem mais direta � alocar um
que tenha uma fun��o adequada, o qual chamamos de newitem:

/* newitem: cria um item novo a partir do nome e valor */


Nameval *newitem(char *name, int value)
{
Nameval *newp;
newp = (Nameval *) emalloc(sizeof(Nameval));
newp->name = name;
newp->value = value;
newp->next = NULL;
return newp;
}

A rotina emalloc � aquela que vamos usar em todo o livro; ela chama malloc,
e se a aloca��o falhar, ela reporta o erro e sai do programa. Vamos mostrar o
c�digo no Cap�tulo 4. Por enquanto, basta encarar emalloc como um alocador de
mem�ria que nunca retorna falha.
A maneira mais simples e r�pida de montar uma lista � adicionar cada novo
elemento na frente:

/* addfront: adiciona newp na frente de listp */


Nameval *addfront(Nameval *listp, Nameval *newp)
{
newp->next = listp;
return newp;
}

Quando uma lista � modificada, ela pode adquirir um primeiro elemento diferente,
como faz quando addfront � chamada. As fun��es que atualizam uma lista retornam um
ponteiro para o primeiro elemento novo, o qual � armazenado na vari�vel que cont�m
a lista. A fun��o addfront e outras fun��es desse grupo retornam todas o ponteiro
para o primeiro elemento como seus valores de fun��o. Um uso t�pico �:

nvlist = addfront(nvlist, newitem("smiley", Ox263A));

Esse � um projeto que funciona mesmo quando a lista existente est� vazia
(null) e facilita a combina��o das fun��es nas express�es. Isso parece mais natural
do que a alternativa de passar um ponteiro para o ponteiro que cont�m a cabe�a da
lista.
A adi��o de um item ao final de uma lista � um procedimento O(n), uma vez
que devemos caminhar pela lista para encontrar o final:

/* addend: adiciona newp ao final de listp */


Nameval *addend(Nameval *listp, Nameval *newp)
{
Nameval *p;

if (listp == NULL)
return newp;
for (p = listp; p->next != NULL; p = p->next)
;
p->next = newp;
return listp;
}

Se quisermos tornar addend uma opera��o O(1), podemos manter um ponteiro separado
para o final da lista. A desvantagem dessa abordagem, al�m do trabalho de manter o
ponteiro final, � que uma lista n�o � mais representada por uma �nica vari�vel de
ponteiro. Vamos ficar com o estilo simples.
Para pesquisar um item com um nome espec�fico, siga os ponteiros next:

/* lookup: pesquisa sequencial de name em listp */


Nameval *lookup(Nameval *listp, char *name)
{
for ( ; listp != NULL; listp = listp->next)
if (strcmp(name, listp->name) == 0)
return listp;
return NULL; /* nenhuma coincid�ncia */
}

Isso leva o tempo O(n) e em geral n�o h� como melhorar essa restri��o. Mesmo quando
a lista est� classificada, precisamos caminhar pela lista para chegar a determinado
elemento. A pesquisa bin�ria n�o se aplica �s listas.
Para imprimir os elementos de uma lista, n�s podemos escrever uma fun��o
para caminhar pela lista e imprimir cada elemento; para calcular o comprimento de
uma lista, n�s podemos escrever uma fun��o para caminhar pela lista e incrementar
um contador, e assim por diante. Uma alternativa � escrever uma fun��o, apply, que
caminha em uma lista e chama outra fun��o para cada elemento da lista. Podemos
tornar apply mais flex�vel fornecendo um argumento a ser passado toda vez que ele
chama a fun��o. Assim sendo, apply tem tr�s argumentos: a lista, uma fun��o a ser
aplicada a cada elemento da lista e um argumento para aquela fun��o:

/* apply: executa fn para cada elemento de listp */


void apply(Nameval *listp,
void (*fn)(Nameval*, void*), void *arg)
{
for ( ; listp != NULL; listp = listp->next)
(*fn)(listp, arg); /* chama a fun��o */
}

O segundo argumento de apply � um ponteiro para uma fun��o que assume dois
largumentos e retorna void. A sintaxe estranha, por�m padr�o,

void (*fn)(Nameval*, void*)

declara fn como sendo um ponteiro para uma fun��o avaliada como void, ou seja, uma
vari�vel que cont�m o endere�o de uma fun��o que retorna void. A fun��o assume dois
argumentos, um Nameval*, que � o elemento da lista, e um void*, que � um ponteiro
gen�rico para um argumento da fun��o.
Para usar apply, por exemplo para imprimir os elementos de uma lista,
poder�amos escrever uma fun��o trivial cujo argumento � uma string de formato:

/* printnv: imprime o nome e valor usando o formato de arg */


void printnv(Nameval *p, void *arg)
{
char *fmt;

fmt = (char *) arg;


printf(fmt, p->name, p->value);
}
a qual chamamos assim:

apply(nvlist, printnv, "%s: %x\n");

Para contar os elementos, definimos uma fun��o cujo argumento � um ponteiro para um
inteiro a ser incrementado:

/* inccounter: incrementa o contador *arg */


void inccounter(Nameval *p, void *arg)
{
int *ip;

/* p n�o � usado */
ip = (int *) arg;
(*ip)++;
}

e a chamamos assim:

int n;

n = 0;
apply(nvlist, inccounter, &n);
printf("%d elements in nvlist\n", n);

Nem toda opera��o de lista � feita melhor desta maneira. Por exemplo, pan
destruir uma lista devemos ter mais cuidado:

/* freeall: libera todos os elementos de listp */


void freeall(Nameval *listp)
{
Nameval *next;

for ( ; listp != NULL; listp = next){


next = listp->next;
/* assume que o nome est� liberado em algum lugar */
free(listp);
}
}

A mem�ria n�o pode ser usada depois de ter sido liberada; portanto, devemos salvar
listp->next em uma vari�vel local, chamada next, antes de liberar o elemento
apontado por listp. Se o loop leu, como os outros,

? for ( ; listp != NULL; listp = listp->next)


? free(listp);

o valor de listp->next poderia ser sobreposto por free e o c�digo falharia.


Observe que freeall n�o libera listp->name. Ela assume que o campo name de
cada Nameval ser� liberado em outra parte, ou nunca foi alocado. Para garantir que
os itens sejam alocados e liberados de forma consistente � preciso que haja um
acordo entre newitem e freeall; h� uma troca entre garantir que a mem�ria �
liberada e ter certeza de que as coisas que n�o devem ser liberadas n�o o sejam. Os
bugs s�o freq�entes quando isso � feito da maneira errada. Em outras linguagens,
incluindo a Java, a coleta de lixo soluciona esse problema para voc�. Vamos voltar
ao assunto do gerenciamento de recursos no Cap�tulo 4.
Temos mais trabalho para excluir do que para adicionar um �nico elemento de
uma lista:
/* delitem: exclui primeiro "name" de listp */
Nameval *delitem(Nameval *listp, char *name)
{
Nameval *p, *prev;

prev = NULL;
for (p = listp; p != NULL; p = p->next) {
if (strcmp(name, p->name) == 0) {
if (prev == NULL)
listp = p->next;
else
prev->next = p->next;
free(p);
return listp;
}
prev = p;
}
eprintf("delitem: %s not in list", name);
return NULL; /* n�o � poss�vel chegar aqui */
}

Como em freeall, delitem n�o libera o campo name.


A fun��o eprintf exibe uma mensagem de erro e sai do programa, o qual na
melhor das hip�teses � desajeitado. Recuperar-se bem dos erros pode ser algo
dif�cil e exige uma discuss�o mais longa do que aquela que apresentamos no Cap�tulo
4, onde tamb�m vamos mostrar a implementa��o de eprintf.
Estas estruturas e opera��es b�sicas de list atendem a grande maioria dos
aplicativos que voc� pode escrever nos programas comuns. Mas h� muitas
alternativas. Algumas bibliotecas, incluindo a C++ Standard Template Library,
suportam as listas duplamente encadeadas, nas quais cada elemento tem dois
ponteiros, um para seu sucessor e outro para seu predecessor. As listas duplamente
encadeadas exigem mais overhead, mas encontrar o �ltimo elemento e excluir o
elemento atual s�o opera��es O(1). Algumas pessoas alocam os ponteiros de lista
separadamente dos dados que eles vinculam; estes s�o um pouco mais dif�ceis de
usar, mas permitem que os itens apare�am em mais de uma lista ao mesmo tempo.
Al�m de adequadas para as situa��es nas quais h� inser��es e exclus�es no
meio, as listas s�o boas para gerenciar dados n�o ordenados de tamanho flutuante,
particularmente quando o acesso tende a ser last-in-first-out (LIFO), como em uma
pilha. Elas usam a mem�ria de forma mais efetiva do que os arrays quando h� v�rias
pilhas que aumentam e encolhem independentemente. Elas tamb�m se comportam bem
quando as informa��es s�o ordenadas intrinsecamente como uma cadeia de tamanho a
priori desconhecido, tal como as palavras sucessivas de um documento. Se voc�
precisa combinar atualiza��es freq�entes com acesso aleat�rio, por�m, seria bom
usar uma estrutura de dados menos insistentemente linear, tal como uma �rvore ou
hash table.

� Exerc�cio 2-7. Implemente alguns dos outros operadores de lista: copy, merge,
split, insert antes ou depois de um item espec�fico. Como as duas opera��es de
inser��o diferem em suas dificuldades? Quanto das rotinas que escrevemos voc� pode
usar, e quanto voc� deve criar sozinho? ?

� Exerc�cio 2-8. Escreva vers�es recursivas e iterativas de reverse, as quais


revertem uma lista. N�o crie itens de lista, mas reutilize os existentes. ?

� Exerc�cio 2-9. Escreva um tipo List gen�rico para C. A maneira mais f�cil � fazer
com que cada item da lista contenha um void* que aponta para os dados. Fa�a o mesmo
para a C++, definindo um modelo, e para a Java, definindo uma classe que cont�m
listas do tipo Object. Quais s�o os pontos fortes e fracos das diversas linguagens
para essa tarefa? ?
� Exerc�cio 2-10. Crie e implemente um conjunto de testes para verificar se as
rotinas de list que voc� escreveu est�o corretas. O Cap�tulo 6 discute as
estrat�gias de testes. ?

2.8 �rvores

Uma �rvore � uma estrutura hier�rquica de dados que armazena um conjunto de


itens no qual cada item tem um valor, pode apontar para zero ou para outros mais e
� apontado exatamente um pelo outro. A raiz da �rvore � a �nica exce��o. Nenhum
item aponta para ela.
H� muitos tipos de �rvores que refletem estruturas complexas, tais como
�rvores de an�lise que capturam a sintaxe de uma senten�a ou um programa, ou
�rvores de fam�lia que descrevem os relacionamentos entre as pessoas. Vamos
ilustrar os princ�pios com as �rvores de pesquisa bin�ria, as quais t�m dois links
em cada n�. Elas s�o as mais f�ceis de implementar e demonstram as propriedades
essenciais das �rvores. Um n� de uma �rvore de pesquisa bin�ria tem um valor e dois
ponteiros, left e right, que apontam para seus filhos. Os ponteiros filhos podem
ser null se o n� tiver menos de dois filhos. Em uma �rvore de pesquisa bin�ria, os
valores dos n�s definem a �rvore: todos os filhos � esquerda de determinado n� t�m
valores mais baixos, e todos os filhos � direita t�m valores mais altos. Devido a
essa propriedade, podemos usar uma variante da pesquisa bin�ria para pesquisar um
valor espec�fico rapidamente na �rvore ou determinar que ele n�o est� presente. A
vers�o de �rvore de Nameval � direta:

typedef struct Nameval Nameval;


struct Nameval {
char *name;
int value;
Nameval *left; /* menor */
Nameval *right; /* maior */
};

Os coment�rios menor e maior se referem �s propriedades dos v�nculos: os


filhos da esquerda armazenam valores menores, os filhos da direita armaze-nam
valores maiores.
Como exemplo concreto, esta figura mostra um subconjunto de uma tabela de
nomes de caracteres armazenados como uma �rvore de pesquisa bin�ria de Namevals,
classificada pelos valores dos caracteres ASCII dos nomes:

Com ponteiros m�ltiplos para outros elementos de cada n� de uma �rvore,


muitas opera��es que levam o tempo O(n) nas listas ou nos arrays exigem apenas o
tempo O(logn) nas �rvores. Os ponteiros m�ltiplos de cada n� reduzem a complexidade
de tempo das opera��es, reduzindo o n�mero de n�s que deve ser visitado para
encontrar um item.
Uma �rvore de pesquisa bin�ria (a qual chamamos simplesmente de "�rvore"
nesta se��o) � constru�da descendo-se pela �rvore de forma recursiva, com
ramifica��es para a esquerda ou direita, conforme a necessidade, at� encontrarmos o
lugar certo para vincular o n� novo, o qual deve ser um objeto inicializado
adequadamente do tipo Nameval: um nome, um valor e dois pontei-ros null. O n� novo
� adicionado como uma folha, ou seja, ele ainda n�o tem filhos.

/* insert: insere newp em treep, retorna treep */


Nameval *insert(Nameval *treep, Nameval *newp)
{
int cmp;
if (treep == NULL)
return newp;
cmp = strcmp(newp->name, treep->name);
if (cmp == 0)
weprintf("insert: duplicate entry %s ignored",
newp->name);
else if (cmp < 0)
treep->left = insert(treep->left, newp);
else
treep->right = insert(treep->right, newp);
return treep;
}

Ainda n�o dissemos nada sobre as entradas em duplicata. Esta vers�o de insert
reclama contra as tentativas de inserir entradas em duplicata (cmp == 0) na �rvore.
A rotina de inser��o de list n�o reclama porque isso exigiria a pesquisa da lista,
criando a inser��o O(n) em vez de O(1). Com as �rvores, por�m, o teste �
essencialmente gr�tis e as propriedades da estrutura de dados n�o s�o defi-nidas
t�o claramente quanto a haver duplicatas. Nos outros aplicativos, no entanto, seria
necess�rio aceitar as duplicatas, ou seria razo�vel ignor�-las completamente.
A rotina weprintf � uma variante de eprintf; ela imprime uma mensa-gem de
erro, prefixada com a palavra warning, mas ao contr�rio de eprintf ela n�o encerra
o programa.
Uma �rvore equilibrada � aquela na qual cada caminho da raiz para uma folha
tem aproximadamente o mesmo comprimento. A vantagem de uma �rvore equilibrada � que
a pesquisa de um item nela � um processo O(logn), uma vez que, como na pesquisa
bin�ria, o n�mero de possibilidades � cortado pela metade a cada etapa.
Se os itens s�o inseridos em uma �rvore como eles chegam, a �rvore pode n�o
ficar equilibrada. Na verdade, ela pode ficar muito desequilibrada. Se os elementos
chegarem j� classificados, por exemplo, o c�digo sempre descer� abaixo uma
ramifica��o da �rvore, produzindo, na verdade, uma lista abaixo dos v�nculos right,
com todos os problemas de desempenho de uma lista. Se os elementos chegarem em
ordem aleat�ria, por�m, isso provavelmente n�o acontece e a �rvore ser� mais ou
menos equilibrada.
� complicado implementar �rvores com garantia de equil�brio. Esse � um dos
motivos para haver muitos tipos de �rvores. Para nossos prop�sitos, vamos deixar
essa quest�o de lado e assumir que os dados recebidos s�o suficien-temente
aleat�rios para manter a �rvore equilibrada.
O c�digo de lookup � semelhante ao de insert:

/* lookup: pesquisa name na �rvore treep */


Nameval *lookup(Nameval *treep, char *name)
{
int cmp;

if (treep == NULL)
return NULL;
cmp = strcmp(name, treep->name);
if (cmp == 0)
return treep;
else if (cmp < 0)
return lookup(treep->left, name);
else
return lookup(treep->right, name);
}

H� algumas coisas que devemos observar sobre lookup e insert. Primeiro elas
se parecem muito com o algoritmo de pesquisa bin�ria que vimos no in�cio do
cap�tulo. Isso n�o acontece por acaso, uma vez que elas compartilham de uma id�ia
com a pesquisa bin�ria: dividir e conquistar, a origem do desempenho de tempo
logar�tmico.
Em segundo lugar, essas rotinas s�o recursivas. Se elas s�o reescritas como
algoritmos iterativos, elas ser�o mais semelhantes ainda � pesquisa bin�ria. Na
verdade, a vers�o iterativa de lookup deve ser constru�da pela aplica��o de uma
transforma��o elegante � vers�o recursiva. A menos que tenhamos encontrado o item,
a �ltima a��o de lookup � retornar o resultado de uma chamada para a mesma, uma
situa��o chamada tail recursion. Isso pode ser convertido para itera��o aplicando-
se patch nos argumentos e reiniciando a rotina. O m�todo mais direto � usar uma
declara��o goto, mas um loop while � mais limpo:

/* nrlookup: procurar name n�o recursivamente na �rvore treep */


Nameval *nrlookup(Nameval *treep, char *name)
{
int cmp;

while (treep != NULL) {


cmp = strcmp(name, treep->name);
if (cmp == 0)
return treep;
else if (cmp < 0)
treep = treep->left;
else
treep = treep->right;
}
return NULL;
}

Depois que podemos caminhar pela �rvore, as outras opera��es comuns


acontecem naturalmente. Podemos usar algumas das t�cnicas de gerenciamento de
lista, tais como escrever uma travessia de �rvore geral que chama uma fun��o em
cada n�. Desta vez, por�m, h� uma op��o a ser feita: quando executamos a opera��o
nesse item e quando processamos o restante da �rvore? A resposta depende daquilo
que a �rvore est� representando. Se ela estiver armazenando os dados em ordem, tal
como uma �rvore de pesquisa bin�ria, visitamos a metade esquerda antes da direita.
Eventualmente a estrutura de �rvore reflete alguma ordem intr�nseca dos dados, tal
como em uma �rvore de fam�lia, e a ordem na qual visitamos as folhas vai depender
dos relacionamentos que a �rvore representa.
Uma travessia in-order executa a opera��o ap�s visitar a sub�rvore esquerda e antes
de visitar a sub�rvore direita:

/* applyinorder: aplica��o inorder de fn a treep */


void applyinorder(Nameval *treep,
void (*fn)(Nameval*, void*), void *arg)
{
if (treep == NULL)
return;
applyinorder(treep->left, fn, arg);
(*fn)(treep, arg);
applyinorder(treep->right, fn, arg);
}

Essa seq��ncia � usada quando os n�s devem ser processados na ordem


classificada, por exemplo, para imprimi-los todos na ordem, o que seria feito como:

applyinorder(treep, printnv, "%s: %x\n");

Ela tamb�m sugere uma maneira razo�vel de classificar: inserir os itens em


uma �rvore, alocar um array com o tamanho certo, depois usar a travessia inorder
para armazen�-los no array na seq��ncia.
Uma travessia post-order invoca a opera��o no n� atual ap�s visitar os
filhos:

/* applypostorder: aplica��o postorder de fn a treep */


void applypostorder(Nameval *treep,
void (*fn)(Nameval*, void*), void *arg)
{
if (treep == NULL)
return;
applypostorder(treep->left, fn, arg);
applypostorder(treep->right, fn, arg);
(*fn)(treep, arg);
}

A travessia post-order � usada quando a opera��o no n� depende das sub�rvores que


est�o abaixo dele. Os exemplos incluem o c�lculo da altura de uma �rvore (tome o
m�ximo da altura de cada uma das duas sub�rvores e some um), o layout de uma �rvore
em um pacote de desenho gr�fico (aloque espa�o na p�gina para cada sub�rvore e
combine-as para esse espa�o de n�) e a medi��o do armazenamento total.
Uma terceira op��o, pre-order, � raramente usada; portanto, vamos omiti-la.
Realisticamente, as �rvores de pesquisa bin�ria s�o pouco usadas, embora as
�rvores B, que t�m ramifica��o alta, sejam usadas para manter as informa��es no
armazenamento secund�rio. Na programa��o di�ria, um uso comum de uma �rvore �
representar a estrutura de uma declara��o ou express�o. Por exemplo, a declara��o

m�dio = (baixo + alto) / 2;

pode ser representada pela �rvore de an�lise mostrada na figura abaixo. Para
avaliar a �rvore, fa�a uma travessia post-order e execute a opera��o apropriada em
cada n�.

Vamos ver melhor as �rvores de an�lise no Cap�tulo 9.

� Exerc�cio 2-11. Compare o desempenho de lookup e nrlookup. Quanto custa a


recurs�o comparada � itera��o? ?

� Exerc�cio 2-12. Use a travessia in-order para criar uma rotina de classifica��o.
Qual � a sua complexidade de tempo? Sob quais condi��es ela se comportaria de forma
ruim? Como seu desempenho pode ser comparado � nossa quicksort e a uma vers�o de
biblioteca? ?

� Exerc�cio 2-13. Crie e implemente um conjunto de testes para verificar se as


rotinas de �rvores est�o corretas. ?

2.9 Tabelas hash

As tabelas hash s�o uma das grandes inven��es da ci�ncia dos computadores.
Elas combinam os arrays, as listas e um pouco de matem�tica para criar uma
estrutura eficiente para armazenar e recuperar os dados din�micos. A aplica��o
t�pica � uma tabela de s�mbolos, a qual associa algum valor (os dados) a cada
membro de um conjunto din�mico de strings (as chaves). Seu compilador preferido
deve usar uma hash table para gerenciar as informa��es sobre cada vari�vel do seu
programa. Seu browser da Web tamb�m pode usar uma hash table para controlar as
p�ginas usadas mais recentemente, e sua conex�o � Internet provavelmente usa uma
para fazer o cache dos nomes de dom�nio e endere�os de IP usados mais recentemente.
A id�ia � passar a chave por meio de uma fun��o hash para gerar um valor
hash que ser� distribu�do uniformemente por meio de um intervalo de inteiros de
tamanho modesto. O valor hash � usado para indexar uma tabela na qual as
informa��es s�o armazenadas. A Java fornece uma interface padr�o para as tabelas
hash. Em C e C++, o estilo comum � associar a cada valor hash (ou "bucket") uma
lista dos itens que compartilham aquele hash, como ilustra esta figura:

Na pr�tica, a fun��o hash � predefinida e um tamanho de array apropriado �


alocado, quase sempre no tempo de compila��o. Cada elemento do array � uma lista
que encadeia os itens que compartilham de um valor hash. Em outras palavras, uma
hash table com n itens � um array das listas cujo comprimento m�dio � n/(tamanho do
array). Recuperar um item � uma opera��o O(1), des-de que escolhamos uma boa fun��o
hash e as listas n�o fiquem muito grandes.
Como uma hash table � um array de listas, o tipo de elemento � o mesmo de
uma lista:

typedef struct Nameval Nameval;


struct Nameval {
char *name;
int value;
Nameval *next; /* em cadeia */
}; Nameval *symtab[NHASH]; /* uma tabela de s�mbolos */

As t�cnicas de lista que discutimos na Se��o 2.7 podem ser usadas para
atualizar as cadeias individuais de hash. Depois que voc� tem uma boa fun��o hash,
o resto � f�cil. Basta pegar o dep�sito de hash e caminhar pela lista procurando
uma coincid�ncia perfeita. Aqui temos o c�digo da rotina de lookup/insert de uma
hash table. Se o item for encontrado, ele � retornado. Se o item n�o for encontrado
e o flag create estiver definido, lookup adiciona o item � tabela. Novamente, isso
n�o cria uma c�pia do nome, assumindo que quem chamou fez uma c�pia segura.

/* lookup: encontra name em symtab, com create opcional */


Nameval* lookup(char *name, int create, int value)
{
int h;
Nameval *sym;

h = hash(name);
for (sym = symtab[h]; sym != NULL; sym = sym->next)
if (strcmp(name, sym->name) == 0)
return sym;
if (create) {
sym = (Nameval *) emalloc(sizeof(Nameval))i
sym->name = name; /* assumido como alocado em outra parte */
sym->value = value;
sym->next = symtab[h];
symtab[h] = sym;
}
return sym;
}

Essa combina��o de lookup e inser��o opcional � comum. Sem ela o esfor�o �


dobrado, � preciso escrever

if (lookup("name") == NULL)
additem(newitem("name", value));

e o hash � calculado duas vezes.


Que tamanho o array deve ter? A ideia geral � deix�-lo suficientemente
grande para que cada cadeia de hash tenha no m�ximo alguns elementos, de modo que
lookup ser� O(1). Por exemplo, um compilador pode ter um tamanho de array de alguns
milhares, uma vez que um arquivo de origem grande tem algumas milhares de linhas, e
n�s n�o esperamos mais do que um identificador novo por linha de c�digo.
Agora, devemos resolver o que a fun��o hash, hash, deve calcular. A fun��o
deve ser determinista e deve ser r�pida e distribuir os dados de maneira uniforme
em todo o array. Um dos algoritmos de hash mais comuns para as strings constr�i um
valor hash somando cada byte da string a um m�ltiplo do hash at� o momento. A
multiplica��o espalha bits do novo byte at� o valor at� o momento; no final do
loop, o resultado deve ser uma combina��o completa dos bytes de entrada.
Empiricamente, os valores 31 e 37 provaram ser boas op��es para o multiplicador de
uma fun��o hash para as strings ASCII.

enum { MULTIPLIER = 31 };

/* hash: calcula o valor hash da string */


unsigned int hash(char *str)
{
unsigned int h;
unsigned char *p;

h = 0;
for (p = (unsigned char *) str; *p != '\0'; p++)
h = MULTIPLIER * h + *p;
return h % NHASH;
}

O c�lculo usa caracteres unsigned porque se char � signed n�o � especificado


por C e C++, e n�s queremos que o valor hash permane�a positivo.
A fun��o hash retorna o m�dulo de resultado com o tamanho do array. Se a
fun��o hash distribui os valores-chave de maneira uniforme, o tamanho preciso do
array n�o importa. Entretanto, � dif�cil ter certeza sobre a confiabilidade de uma
fun��o hash, e mesmo a melhor fun��o pode ter problemas com alguns conjuntos de
entradas; portanto, � bom fazer o tamanho do array ser um n�mero primo para dar um
bit extra de seguran�a, garantindo que o tamanho do array, do multiplicador hash e
dos valores prov�veis para os dados n�o tenham fator comum.
As experi�ncias mostram que para uma ampla variedade de strings � dif�cil
construir uma fun��o hash melhor do que aquela acima, mas � f�cil criar uma que
seja pior. Uma das primeiras vers�es da Java tinha uma fun��o hash para as strings
que era mais eficiente se a string fosse longa. A fun��o hash economizou tempo
examinando apenas oito ou nove caracteres a intervalos regulares em todas as
strings mais longas do que 16 caracteres, partindo do in�cio. Infelizmente, embora
a fun��o hash fosse mais r�pida, ela tinha propriedades estat�sticas ruins que
cancelavam qualquer ganho de desempenho. Saltando partes da string, ela tendia a
perder a �nica parte importante. Os nomes de arquivo come�am com prefixos longos e
id�nticos - o nome do diret�rio - e podem diferir apenas nos �ltimos caracteres
(.java versus .class). Os URLs geralmente come�am com http://www. e terminam
com .html e, portanto, tendem a diferir apenas no meio. A fun��o hash quase sempre
examinava apenas a parte que n�o varia do nome, resultando em longas cadeias hash
que deixavam a pesquisa lenta. O problema foi resolvido com a substitui��o do hash
por um equivalente �quele que mostramos (com um multiplicador de 37), o qual
examina cada caractere da string.
Uma fun��o hash boa para um conjunto de entradas (digamos, nomes curtos de
vari�veis) pode ser ruim para outro (URLs), de modo que uma fun��o hash em
potencial deve ser testada em uma variedade de entradas t�picas. Esse hash � bom
para as strings curtas? Strings longas? Strings de comprimentos iguais com pequenas
varia��es?
As strings n�o s�o as �nicas coisas que podem ter hash. Podemos us�-lo nas
tr�s coordenadas de uma part�cula em uma simula��o da F�sica, reduzindo o
armazenamento para uma tabela linear (O(n�mero de part�culas)) em vez de um array
tridimensional (O(xsize x ysize x zsize)).
Um uso importante do hashing � o programa Supertrace, de Gerard Holzmann,
para analisar protocolos e sistemas simult�neos. O Supertrace analisa todas as
informa��es sobre cada estado poss�vel do sistema e faz o hash das informa��es para
gerar o endere�o de um �nico bit da mem�ria. Se esse bit estiver on, o estado j�
foi visto antes; caso contr�rio, ele n�o foi visto antes. O Supertrace usa uma hash
table com muitos megabytes de comprimento, mas armazena apenas um �nico bit em cada
bucket. N�o h� encadeamento; se dois estados colidem por causa do hash do mesmo
valor, o programa n�o vai notar. O Supertrace depende da probabilidade de colis�o
ser baixa (ela n�o precisa ser zero porque o Supertrace lida com probabilidades e
n�o � exato). Assim sendo, a fun��o hash � particularmente cuidadosa. Ela usa uma
verifica��o c�clica de redund�ncia, uma fun��o que produz uma combina��o completa
dos dados.
As tabelas hash s�o excelentes para as tabelas de s�mbolos, uma vez que
fornecem o acesso esperado O(1) a qualquer elemento. Elas t�m algumas limita��es.
Se a fun��o hash � ruim ou o tamanho da tabela � muito pequeno, as listas podem
ficar muito longas. Como as listas n�o s�o classificadas, isso leva ao
comportamento O(n). Os elementos n�o s�o diretamente acess�veis na ordem
classificada, mas � f�cil cont�-los, alocar um array, preench�-los com ponteiros
para os elementos e classific�-los. Mesmo assim, quando usadas da forma adequada,
as propriedades de lookup em tempo constante, inser��o e exclus�o de uma hash table
s�o imbat�veis com rela��o �s outras t�cnicas.

� Exerc�cio 2-14. Nossa fun��o hash � uma hash excelente de prop�sito geral para as
strings. No entanto, dados peculiares podem causar um comportamento ruim. Construa
um conjunto de dados que fa�a com que nossa fun��o hash tenha desempenho ruim. �
mais f�cil encontrar um conjunto ruim para valores diferentes de NHASH? ?

� Exerc�cio 2-15. Escreva uma fun��o para acessar os elementos sucessivos da hash
table na ordem n�o classificada. ?

� Exerc�cio 2-16. Altere o lookup para que se o comprimento m�dio da lista se


tornar maior do que x, o array seja aumentado automaticamente por um fator y e a
hash table seja reconstru�da. ?

� Exerc�cio 2-17. Crie uma fun��o hash para armazenar as coordenadas dos pontos em
2 dimens�es. Com que facilidade a sua fun��o se adapta �s altera��es no tipo de
coordenadas, por exemplo, de inteiro para ponto flutuante ou de coordenadas
cartesianas para polares, ou �s altera��es de 2 para dimens�es mais altas? ?

2.10 Resumo

H� v�rias etapas para selecionar um algoritmo. Primeiro, avalie os


algoritmos e estruturas de dados em potencial. Pense em quantos dados o programa
deve processar. Se o problema envolver quantidades modestas de dados, selecione
t�cnicas simples; se os dados podem ser aumentados, elimine os projetos que n�o
podem ser escalados para entradas grandes. Depois, use uma biblioteca ou recurso de
linguagem se puder. Se isso falhar, escreva ou pe�a emprestada uma implementa��o
curta, simples, f�cil de entender. Experimente essa implementa��o. Se as medi��es
provarem que ela � muito lenta, somente ent�o voc� deve atualizar para uma t�cnica
mais avan�ada.
Embora haja muitas estruturas de dados, algumas vitais para o bom desempenho
em circunst�ncias especiais, a maioria dos programas se baseia em grande parte nos
arrays, nas listas, �rvores e tabelas hash. Cada um deles suporta um conjunto de
opera��es primitivas, as quais em geral incluem: criar elemento novo, encontrar um
elemento, adicionar um elemento em algum lugar, excluir um elemento e aplicar
alguma opera��o a todos os elementos.
Cada opera��o tem um tempo de computa��o esperado que quase sempre determina
se esse tipo de dados (ou implementa��o) � adequado para determinada aplica��o. Os
arrays suportam o acesso de tempo constante a todo elemento, mas n�o s�o aumentados
ou encolhem facilmente. As listas se adaptam bem �s inser��es e exclus�es, mas
levam o tempo O(n) para acessar os elementos aleat�rios. As �rvores e tabelas hash
fornecem um bom equil�brio: acesso r�pido a itens espec�ficos, combinado �
facilidade de aumento, desde que alguns crit�rios de equil�brio sejam mantidos.
Existem outras estruturas de dados mais sofisticadas para problemas
especializados, mas esse conjunto b�sico � suficiente para construir a maior parte
do software.

Leitura suplementar

A fam�lia de livros Algorithms, de Bob Sedgewick (Addison-Wesley), � uma


refer�ncia excelente para encontrar tratamentos acess�veis para uma variedade de
algoritmos �teis. A terceira edi��o do Algorithms in C++ (1998) tem uma boa
discuss�o sobre as fun��es hash e os tamanhos de tabela. O livro The Art of
Computer Programming (Addison-Wesley), de Don Knuth, � a fonte definitiva para
an�lises rigorosas de muitos algoritmos; o Volume 3 (2a edi��o, 1998) aborda a
classifica��o e pesquisa.
O Supertrace � descrito em Design and Validation of Computer Protocols, de
Gerard Holzmann (Prentice Hall, 1991).
Jon Bentley e Doug McIlroy descrevem a cria��o de um quicksort r�pido e
robusto em "Engineering a sort function", Software - Practice and Experience, 23,
1.p�gs. 1.249-1.265, 1993.

Projeto e implementa��o

Mostre-me seus fluxogramas e esconda suas tabelas, e eu continuarei sendo enganado.


Mostre-me suas tabelas e eu n�o precisarei dos seus fluxogramas; eles ser�o �bvios.
Frederick P. Brooks, Jr., The Mythical Man Month

Como sugere a cita��o do livro cl�ssico de Brooks, o projeto das estruturas


de dados � a decis�o central na cria��o de um programa. Depois que se tem o layout
das estruturas de dados, os algoritmos tendem a se ajustar e o c�digo fica
comparativamente f�cil.
Este � um ponto de vista simplificado, mas ele n�o � enganoso. No cap�tulo
anterior, examinamos as estruturas de dados b�sicas que s�o os blocos fundamentais
da maioria dos programas. Neste cap�tulo vamos combinar tais estruturas ao
trabalharmos com o projeto e a implementa��o de um programa de tamanho moderado.
Um aspecto desse ponto de vista � que a op��o pela linguagem de programa��o
� relativamente sem import�ncia para o projeto geral. Vamos criar um programa
abstrato e depois vamos escrev�-lo em C, Java, C++, Awk e Perl. A compara��o entre
as implementa��es demonstra como as linguagens podem ajudar ou atrapalhar, e por
que elas n�o s�o importantes. O projeto de programa certamente pode ser colorido
por uma linguagem, mas geralmente ele n�o � dominado por ela.
O problema que escolhemos � incomum, mas na forma b�sica ele � t�pico de
muitos programas: alguns dados entram, alguns dados saem, e o processamento depende
de um pouco de engenhosidade.
Especificamente, vamos gerar texto aleat�rio em ingl�s, que � bem lido. Se
emitirmos letras ou palavras aleat�rias, o resultado n�o ter� sentido. Por exemplo,
um programa que seleciona letras aleatoriamente (e espa�os em branco para separar
as palavras) poderia produzir o seguinte:

xptmxgn xusaja afqnzgxl Ihidlwcd rjdjuvpydrlwnjy

o que n�o � muito convincente. Se pesarmos as letras pela sua frequ�ncia de


apari��o no texto em ingl�s, podemos obter o seguinte:

idtefoae tcs trder jcii ofdslnqetacp t ola

o que tamb�m n�o � muita coisa. As palavras escolhidas no dicion�rio aleatoriamente


n�o fazem muito sentido:

polydactyl equatorial splashily jowl verandah circumscribe

Para obter resultados melhores, precisamos de um modelo estat�stico com mais


estrutura, tal como a freq��ncia de apari��o de frases inteiras. Mas onde podemos
encontrar esses dados estat�sticos?
Poder�amos pegar um texto grande em ingl�s e estud�-lo em detalhes, mas h�
uma abordagem mais f�cil e divertida. A principal observa��o � que n�s podemos usar
um texto existente para construir um modelo estat�stico da l�ngua-gem usada naquele
texto, e a partir dele podemos gerar texto aleat�rio que tem dados estat�sticos
semelhantes aos do original.

3.1 O algoritmo de cadeia de Markov

Uma maneira elegante de fazer essa classifica��o de processamento � uma


t�cnica chamada algoritmo de cadeia de Markov. Se imaginarmos a entrada como uma
seq��ncia de frases sobrepostas, o algoritmo divide cada frase em duas partes, um
prefixo de v�rias palavras e uma �nica palavra de sufixo que vem depois do prefixo.
Um algoritmo de cadeia de Markov emite a sa�da de frases selecionando
aleatoriamente o sufixo que vem depois do prefixo, segundo os dados estat�sticos
(no nosso caso) do texto original. As frases de tr�s palavras funcionam bem - um
prefixo de duas palavras � usado para selecionar a palavra de sufixo:

definir w1 e w2 para as duas primeiras palavras do texto


imprimir w1 e w2
loop:
selecionar aleatoriamente w3, um dos sucessores do prefixo
w1 w2 no texto
imprimir w3
substituir w1 e w2 por w2 e w3
repetir o loop

Para ilustrar, suponhamos que vamos gerar texto aleat�rio baseado em algumas
senten�as parafraseadas da cita��o acima, usando prefixos de duas palavras:

Show your flowcharts and conceal your tables and I will be mystified. Show your
tables and your flowcharts will be obvious. (end)

Estes s�o alguns dos pares de palavras de entrada e as palavras seguintes:

Prefixo de entrada:Palavras seguintes do sufixo:Show your


flowcharts tablesyour flowcharts and willflowcharts and
concealflowcharts will beyour tables and andwill
bemystified. obvious.be mystified.Showbe obvious(end)
Um algoritmo de Markov para processar esse texto come�ar� imprimindo Show
your e depois escolher� aleatoriamente entre flowcharts ou tables. Se ele escolher
o primeiro, o prefixo atual se torna your flowcharts e a palavra seguinte ser� and
ou will. Se ele escolher tables, a pr�xima palavra ser� and. Isso continua at� que
sa�da suficiente tenha sido gerada ou at� que o marcador final seja encontrado como
um sufixo.
Nosso programa ler� uma parte do texto em ingl�s e usar� um algoritmo de
cadeia de Markov para gerar o texto novo, com base na freq��ncia de apari��o das
frases com um comprimento fixo. O n�mero de palavras do prefixo, que � dois em
nosso exemplo, � um par�metro. A tend�ncia, quando tornamos o prefixo mais curto, �
produzir prosa menos coerente. Quando o tornamos mais longo tendemos a reproduzir
de forma literal o texto de entrada. Para o texto em ingl�s, o uso de duas palavras
para selecionar uma terceira � uma boa op��o. Ela parece recriar o sabor da
entrada, acrescentando seu pr�prio toque exc�ntrico.
O que � uma palavra? A resposta �bvia � uma seq��ncia de caracteres
alfab�ticos, mas � desej�vel deixar a pontua��o nas palavras, de modo que "words" e
"words." sejam diferentes. Isso ajuda a melhorar a qualidade da prosa gerada
deixando a pontua��o e, portanto (indiretamente) a gram�tica, influencia-rem a
escolha das palavras, embora isso tamb�m permita que cita��es desequili-bradas e
par�nteses sejam introduzidos. Assim sendo, vamos definir uma "palavra" como algo
entre espa�os em branco, uma decis�o que n�o faz nenhuma restri��o quanto ao idioma
de entrada e deixa a pontua��o anexada �s palavras. Como a maioria das linguagens
de programa��o tem recursos para dividir o texto em palavras separadas por espa�o
em branco, isso tamb�m � f�cil de implementar.
Devido ao m�todo, todas as palavras, todas as frases de duas palavras, e
todas as frases de tr�s palavras da entrada devem ter aparecido na entrada, mas
deve haver muitas frases de quatro palavras e mais longas que s�o sintetizadas.
Aqui temos algumas senten�as produzidas pelo programa, e que vamos desen-volver
neste cap�tulo, quando ele recebe o texto do Cap�tulo VII do livro The Sun Also
Rises, de Ernest Hemingway:

As I started up the undershirt onto his chest black, and big stomach muscles
bulging under the light. "You see them?" Below the line where his ribs stopped were
two raised white welts. "See on the forehead."
"Oh, Brett, I love you." "Let's not talk. Talking's all bilge. I'm going away
tomorrow." "Tomorrow?" "Yes. Didn't I say so? I am." "Let's have a drink, then."

Tivemos sorte aqui porque a pontua��o saiu correta; isso n�o precisa acontecer.

3.2 Alternativas de estruturas de dados

Com quantas entradas pretendemos lidar? Qual ser� a velocidade de execu��o


do programa? Parece razo�vel pedirmos ao nosso programa para ler um livro inteiro,
e assim devemos estar preparados para tamanhos de entrada de n = 100.000 palavras
ou mais. A sa�da ter� centenas ou talvez milhares de palavras e o programa deve ser
executado em alguns segundos, em vez de minutos. Com 100.000 palavras de texto de
entrada, n � suficientemente grande e os algoritmos n�o podem ser muito simples se
quisermos que o programa seja r�pido.
O algoritmo de Markov deve ver toda a entrada antes de come�ar a gerar a
sa�da; portanto, ele deve armazenar toda a entrada de alguma forma. Uma
possibilidade seria ler toda a entrada e armazen�-la em uma string longa, mas n�s
queremos que a entrada seja dividida em palavras. Se a armazenarmos como um array
de ponteiros para as palavras, a gera��o da sa�da � simples: produzir cada palavra,
examinar o texto de entrada para ver quais poss�veis palavras de sufixo v�m depois
do prefixo que acabou de ser emitido, e depois selecionar uma aleatoriamente.
Entretanto, isso significa examinar todas as 100.000 palavras de entrada para cada
palavra que geramos; 1.000 palavras de sa�da significam milhares de milh�es de
compara��es de string, o que n�o seria muito r�pido.
Outra possibilidade seria armazenar somente as palavras de entrada
exclusiva, juntamente com uma lista do lugar no qual elas aparecem na entrada, para
que possamos localizar as palavras sucessoras mais rapidamente. Poder�amos usar uma
tabela hash como aquela do Cap�tulo 2, mas essa vers�o n�o aborda diretamente as
necessidades do algoritmo de Markov, o qual deve localizar rapidamente todos os
sufixos de determinado prefixo.
Precisamos de uma estrutura de dados que represente melhor um prefixo e seus
sufixos associados. O programa ter� duas passagens, uma passagem de entrada, que
constr�i a estrutura de dados que representa as frases, e uma passagem de sa�da,
que usa a estrutura de dados para gerar a sa�da aleat�ria. Em ambas as passagens,
precisamos procurar um prefixo (rapidamente): na passagem da entrada para atualizar
seus sufixos, e na passagem de sa�da para selecionar aleatoriamente os sufixos
poss�veis. Isso sugere uma tabela hash cujas chaves sejam prefixos e cujos valores
sejam os conjuntos de sufixo dos prefixos correspondentes.
Para fins descritivos, vamos assumir um prefixo de duas palavras, de modo
que cada palavra da sa�da se baseie no par de palavras que a precede. O n�mero de
palavras do prefixo n�o afeta o projeto e os programas devem lidar com qualquer
comprimento de prefixo, mas a sele��o de um n�mero torna a dis-cuss�o concreta.
Vamos chamar o prefixo e o conjunto de todos os seus poss�veis sufixos de estado,
que � a terminologia padr�o dos algoritmos de Markov.
Dado um prefixo, precisamos armazenar todos os sufixos que lhe seguem, para
podermos acess�-los mais tarde. Os sufixos n�o est�o ordenados e s�o acrescentados
um de cada vez. N�o queremos saber quantos sufixos existem; portanto, precisamos de
uma estrutura de dados que seja aumentada f�cil e eficientemente, tal como uma
lista ou um array din�mico. Quando estamos gerando sa�da, precisamos selecionar um
sufixo aleatoriamente no conjunto de sufixos associado ao prefixo em particular. Os
itens nunca s�o exclu�dos.
O que acontece quando uma frase aparece mais de uma vez? Por exemplo, 'pode
aparecer duas vezes' poderia aparecer duas vezes, mas 'pode aparecer uma vez'
poderia aparecer somente uma vez. Isso poderia ser representado pela coloca��o de '
duas vezes' duas vezes na lista de sufixos para 'pode aparecer', ou colocando-o em
uma vez, com o contador associado definido como 2. Tentamos fazer isso com e sem
contadores; sem � mais f�cil, uma vez que para adicionar um sufixo n�o � preciso
verificar se ele j� est� l�, e as experi�ncias mostraram que a diferen�a no tempo
de compila��o era desprez�vel.
Em resumo, cada estado compreende um prefixo e uma lista de sufixos. Essas
informa��es s�o armazenadas em uma tabela hash, tendo o prefixo como chave. Cada
prefixo � um conjunto de palavras com tamanho fixo. Se um sufixo ocorrer mais de
uma vez para determinado prefixo, cada ocorr�ncia ser� inclu�da separadamente na
lista.
A pr�xima decis�o � sobre como representar as pr�prias palavras. A ma-neira
f�cil � armazen�-las como strings individuais. Como a maioria do texto tem muitas
palavras que aparecem v�rias vezes, provavelmente economizar�amos armazenamento se
tiv�ssemos uma segunda tabela hash de palavras simples, para que o texto de cada
palavra fosse armazenado apenas uma vez. Isso tam-b�m agilizaria o hashing dos
prefixos, uma vez que poder�amos comparar ponteiros, em vez de caracteres
individuais: as strings exclusivas t�m endere�os exclusivos. Vamos deixar esse
projeto para o exerc�cio. Por enquanto, as strings ser�o armazenadas
individualmente.

3.3 Construindo a estrutura de dados em C

Vamos come�ar com uma implementa��o em C. A primeira etapa � definir algu-mas


constantes.

enum {
NPREF =2, /* n�mero de palavras do prefixo */
NHASH = 4093, /* tamanho do array da tabela hash de estado */
MAXGEN = 10000 /* m�ximo de palavras geradas */
};

Essa declara��o define o n�mero de palavras (NPREF) para o prefixo, o


tamanho do array da tabela hash (NHASH), e um limite m�ximo para o n�mero de
palavras a serem geradas (MAXGEN). Se NPREF � uma constante do tempo de compila��o,
em vez de uma vari�vel do tempo de execu��o, o gerenciamento do armazenamento �
mais simples. O tamanho do array � definido de forma relativamente grande porque
n�s esperamos dar ao programa documentos de entrada grandes, talvez at� um livro
inteiro. Escolhemos NHASH = 4093 , porque se a entrada tiver 10.000 prefixos
distintos (pares de palavras), a cadeia m�dia ser� muito curta, com dois ou tr�s
prefixos. Quanto maior o tamanho, mais curto � o tamanho esperado para as cadeias
e, portanto, mais r�pida � a pesquisa. Esse programa, na verdade, � um brinquedo e
o desempenho n�o � cr�tico. Mas se o array for pequeno demais, o programa n�o vai
lidar com nossa entrada esperada num tempo razo�vel. Por outro lado, se ele for
grande demais, talvez n�o se ajuste � mem�ria dispon�vel.
O prefixo pode ser armazenado como um array de palavras. Os elementos da
tabela hash ser�o representados como um tipo de dados State, associando a lista
Suffix ao prefixo:

typedef struct State State;


typedef struct Suffix Suffix;
struct State { /* prefixo+ lista de sufixos */
char *pref[NPREF]; /* palavras de prefixo */
Suffix *suf; /* lista de sufixos */
State *next; /* pr�ximo da tabela hash */
};
struct Suffix { /* lista de sufixos */
char *word; /* sufixo */
Suffix *next; /* pr�ximo da lista de sufixos */
};

State *statetab[NHASH]; /* tabela hash de estados */

Uma representa��o das estruturas de dados seria assim:

Precisamos de uma fun��o hash para os prefixos, os quais s�o arrays de


strings. � simples modificar a fun��o hash de string do Cap�tulo 2, para que ela
fa�a o loop pelas strings do array, fazendo assim o hash da concatena��o das
strings:

/* hash: calcula o valor hash do array de strings NPREF */


unsigned int hash(char *s[NPREF])
{
unsigned int h;
unsigned char *p;
int i;

h = 0;
for (i = 0; i < NPREF; i++)
for (p = (unsigned char *) s[i]; *p != '\0'; p++)
h = MULTIPLIER * h + *p;
return h % NHASH;
}

Uma modifica��o semelhante na rotina de lookup completa a implemen-ta��o da


tabela hash:

/* lookup: pesquisa o prefixo; cria se solicitado. */


/* retorna o ponteiro se ele estiver presente ou criado;
NULL caso contrario. */
/* a cria��o n�o strdup e as strings n�o devem mudar mais tarde. */
State* lookup(char *prefix[NPREF], int create)
{
int i, h;
State *sp;
h = hash(prefix);
for (sp = statetab[h]; sp != NULL; sp = sp->next) {
for (i = 0; i < NPREF; i++)
if (strcmp(prefix[i], sp->pref[i]) != 0)
break;
if (i == NPREF) /* encontrado */
return sp;
}
if (create) {
sp = (State *) emalloc(sizeof(State));
for (i = 0; i < NPREF; i++)
sp->pref[i] = prefix[ij;
sp->suf = NULL;
sp->next = statetab[h];
statetab[h] = sp;
}
return sp;
}

Observe que lookup n�o faz uma c�pia das strings recebidas quando ele cria
um estado novo. Ele apenas armazena ponteiros em sp->pref[]. Quem chamar lookup
deve garantir que os dados n�o ser�o sobrepostos mais tarde. Por exemplo, se as
strings est�o em um buffer de E/S, uma c�pia deve ser feita antes que lookup seja
chamada. Caso contr�rio, a entrada subsequente pode sobrepor os dados para os quais
a tabela hash aponta. As decis�es sobre quem tem a propriedade de um recurso
compartilhado em uma interface sempre surgem. Vamos explorar esse assunto com
detalhes no pr�ximo cap�tulo.
A seguir, precisamos construir a tabela hash � medida que o arquivo � lido:

/* build: l� entrada, constr�i tabela de prefixos */


void build(char *prefix[NPREF], FILE *f)
{
char buf[100], fmt[10];
/* cria uma string de formato; %s poderia exceder buf */ sprintf(fmt, "%%%ds",
sizeof(buf)-l);
while (fscanf(f, fmt, buf) != EOF)
add(prefix, estrdup(buf));
}

A chamada peculiar de sprintf contorna um problema irritante de fscanf, o


qual de outra maneira seria perfeito para a tarefa. Uma chamada de fscanf com o
formato %s ler� a pr�xima palavra delimitada por espa�o em branco do arquivo no
buffer, mas n�o h� limite de tamanho. Uma palavra longa poderia exceder o buffer de
entrada, causando danos. Se o buffer tem 100 bytes de comprimento (o que est� bem
acima daquilo que esperamos que apare�a no texto normal), n�s podemos usar o
formato %99s (deixando um byte para o terminal '\0'), que diz a fscanf para parar
depois de 99 bytes. Uma palavra longa ser� dividida em peda�os, o que � ruim mas
seguro. N�s declarar�amos

? enum { BUFSIZE = 100 };


? char fmt[] = "%99s"; /* BUFSIZE-1 */

mas isso exige duas constantes para uma decis�o arbitr�ria - o tamanho do buffer -
e introduz a necessidade de manter seus relacionamentos. O problema pode ser
solucionado de uma vez por todas com a cria��o din�mica da string de formato com
sprintf, de modo que essa � a abordagem que usaremos.
Os dois argumentos para build s�o o array prefix contendo as palavras NPREF
anteriores da entrada e um ponteiro FILE. Ele passa o prefix e uma c�pia da palavra
de entrada para add, a qual soma a nova entrada � tabela hash e avan�a o prefixo:
/* add: adiciona a palavra � lista de sufixos, atualiza prefixo */
void add(char *prefix[NPREF], char *suffix)
{
State *sp;

sp = lookup(prefix, 1); /* criar se n�o for encontrado */


addsuffix(sp, suffix); /*mover as palavras abaixo do prefixo */
memmove(prefix, prefix+1, (NPREF-l)*sizeof(prefix[0]));
prefix[NPREF-l] = suffix;
}

A chamada de memmove � o idioma para a exclus�o em um array. Ela desloca os


elementos 1 at� NPREF-1 do prefixo abaixo para as posi��es 0 at� NPREF-2, excluindo
a primeira palavra de prefixo e abrindo um espa�o para uma palavra nova no final.
A rotina addsuffix adiciona o novo sufixo:

/* addsuffix: adiciona a state. O sufixo n�o deve mudar mais tarde */


void addsuffix(State *sp, char *suffix)
{
Suffix *suf;

suf = (Suffix *) emalloc(sizeof(Suffix));


suf->word = suffix;
suf->next = sp->suf;
sp->suf = suf;
}

Dividimos a a��o de atualiza��o do estado em duas fun��es: add executa o


servi�o geral de adicionar um sufixo a um prefixo, enquanto addsuffix executa a
a��o espec�fica da implementa��o de adicionar uma palavra a uma lista de sufixos. A
rotina add � usada por build, mas addsuffix � usada internamente somente por add;
ela � um detalhe de implementa��o que pode mudar e � melhor que fique em uma fun��o
separada, embora ela seja chamada apenas em um lugar.

3.4 Gerando sa�da

Com a estrutura de dados constru�da, a pr�xima etapa � gerar a sa�da. A


ideia b�sica � a mesma: dado um prefixo, selecione um de seus sufixos
aleatoriamente, imprima-o, depois avance o prefixo. Este � o estado est�vel do
processamento. Ainda devemos descobrir como iniciar e parar o algoritmo. Iniciar �
f�cil se nos lembrarmos das palavras do primeiro prefixo e come�armos com elas.
Parar tamb�m � f�cil. Precisamos de uma palavra que marque o encerramento do
algoritmo. Depois de toda a entrada regular, podemos adicionar um terminal, uma
"palavra" que certamente n�o aparece em nenhuma entrada:

build(prefix, stdin);
add(prefix, NONWORD);

NONWORD deve ter algum valor que nunca ser� encontrado na entrada normal. Como as
palavras da entrada s�o delimitadas por espa�o em branco, uma "palavra" com espa�o
em branco vai servir, tal como um caractere de linha nova:

char NONWORD[] = "\n"; /* n�o pode aparecer como palavra real */

Outra preocupa��o: o que acontece se n�o houver entrada suficiente para


iniciar o algoritmo? H� duas abordagens para esse tipo de problema: sair
prematuramente se houver entrada insuficiente, ou providenciar que sempre haja
entrada suficiente e n�o ter de se preocupar em verificar. Neste programa, a �ltima
abordagem funciona bem.
Podemos inicializar a constru��o e gera��o com um prefixo pr�-fabricado, o
que garante que sempre haja entrada suficiente para o programa. Para ter uma pr�via
dos loops, inicialize o array de prefixo para que todas as palavras sejam NONWORD.
Isso tem o benef�cio de que a primeira palavra do arquivo de entrada ser� o
primeiro sufixo do prefixo falso; portanto, o loop de gera��o precisa imprimir
apenas os sufixos que ele produzir.
No caso de a sa�da ser extremamente longa, podemos encerrar o algoritmo
depois que um certo n�mero de palavras foi produzido, ou quando atingirmos NONWORD
como um sufixo, a op��o que vier primeiro.
O acr�scimo de algumas NONWORDS aos finais dos dados simplifica os loops do
processamento principal do programa de maneira significativa; esse � um exemplo da
t�cnica de adicionar valores de sentinela para marcar os limites.
Como regra, tente lidar com as irregularidades e exce��es e os casos
especiais nos dados. � mais dif�cil acertar o c�digo; portanto, o fluxo de controle
deve ser t�o simples e regular quanto poss�vel.
A fun��o generate usa o algoritmo do qual fizemos um rascunho originalmente.
Ela produz uma palavra por linha de sa�da, a qual pode ser agrupada em linhas mais
longas com um processador de texto. O Cap�tulo 9 mostra um formatador simples
chamado fmt que realiza essa tarefa.
Com o uso das strings NONWORD inicial e final, generate � iniciada e parada
adequadamente:

/* generate: produz sa�da, uma palavra por linha */


void generate(int nwords)
{
State *sp;
Suffix *suf;
char *prefix[NPREF], *w;
int i, nmatch;

for (i = 0; i < NPREF; i++) /* redefine prefixo inicial */


prefix[i] = NONWORD;

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


sp = lookup(prefix, 0);
nmatch = 0;
for (suf = sp->suf; suf != NULL; suf = suf->next)
if (rand() % ++nmatch == 0) /* prob = 1/nmatch */
w = suf->word;
if (strcmp(w, NONWORD) == 0)
break;
printf("%s\n", w);
memmove(prefix, prefix+1, (NPREF-l)*sizeof(prefix[0]));
prefix[NPREF-l] = w;
}
}

Observe o algoritmo para selecionar um item aleatoriamente quando n�o sabemos


quantos itens existem. A vari�vel nmatch conta o n�mero de coincid�ncias � medida
que a lista � examinada. A express�o

Rand() % ++nmatch == O

incrementa nmatch e depois � verdadeira com a probabilidade de 1/nmatch. Assim


sendo, o primeiro item coincidente � selecionado com a probabilidade 1, o segundo o
substitui com a probabilidade 1/2, o terceiro vai substituir o sobrevivente com a
probabilidade 1/3 e assim por diante. A qualquer momento, cada um dos k itens
coincidentes vistos at� agora � selecionado com a probabilidade l/k.
No in�cio, definimos prefix para o valor inicial, o qual garantidamente est�
instalado na tabela hash. Os primeiros valores de Suffix que encontramos ser�o as
primeiras palavras do documento, uma vez que eles s�o a continua��o exclusiva para
o prefixo inicial. Depois disso, sufixos aleat�rios ser�o escolhidos. O loop chama
lookup para encontrar a entrada da tabela hash para o prefix atual, e depois
seleciona um suffix aleat�rio, o imprime e avan�a o prefixo.
Se o sufixo que escolhemos for NONWORD, n�s terminamos, porque escolhemos o
estado que corresponde ao final da entrada. Se o sufixo for NONWORD, n�s o
imprimimos, depois soltamos a primeira palavra do sufixo com uma chamada para
memmove, promovemos o sufixo para ser a �ltima palavra do prefixo, e fazemos o
loop.
Agora podemos colocar tudo isso junto em uma rotina main que l� a entrada
padr�o e gera no m�ximo um n�mero especificado de palavras:

/* markov main: gera��o de texto aleat�rio de cadeia de markov */


int main(void)
{
int i, nwords = MAXGEN;
char *prefix[NPREF]; /* prefixo de entrada atual */

for (i = 0; i < NPREF; i++) /* definir prefixo inicial */


prefix [i] = NONWORD;
build(prefix, stdin);
add(prefix, NONWORD);
generate(nwords);
return 0;
}
}

Isso completa nossa implementa��o em C. No final do cap�tulo faremos uma


compara��o dos programas nas diferentes linguagens. As grandes vantagens da C s�o
que ela d� ao programador controle completo sobre a implementa��o e os programas
escritos em C tendem a ser mais r�pidos. O custo, por�m, � que o programador em C
deve trabalhar mais, alocando e retomando mem�ria, criando tabelas hash e listas
encadeadas, por exemplo. A C � uma faca de dois gumes, na qual � poss�vel criar um
programa elegante e eficiente ou uma grande bagun�a.

� Exerc�cio 3-1. O algoritmo para selecionar um item aleat�rio em uma lista de


comprimento desconhecido depende de um bom gerador de n�meros aleat�rios. Crie e
execute experi�ncias para determinar como o m�todo funciona na pr�tica. ?

� Exerc�cio 3-2. Se cada palavra da entrada for armazenada em uma segunda tabela
hash, o texto s� � armazenado uma vez, o que deve economizar espa�o. Me�a alguns
documentos para estimar a quantidade de espa�o. Essa organiza��o deve permitir que
comparemos os prefixos dos ponteiros em vez das strings das cadeias hash, o que
deve ser mais r�pido. Implemente essa vers�o e me�a a altera��o na velocidade e no
consumo de mem�ria. ?

� Exerc�cio 3-3. Remova as declara��es que colocam as sentinelas NONWORDs no in�cio


e no final dos dados e modifique generate para que ela inicie adequadamente sem
elas. Verifique se isso produz a sa�da correta para a entrada com 0, 1, 2, 3 e 4
palavras. Compare essa implementa��o com a vers�o que usa as sentinelas. ?

3.5 Java

Nossa segunda implementa��o do algoritmo de cadeia de Markov est� em Java.


As linguagens orientadas para o objeto, como a Java, incentivam a aten��o
particular �s interfaces entre os componentes do programa, os quais depois s�o
encapsulados como itens de dados independentes chamados objetos ou classes, com as
fun��es associadas chamadas m�todos.
Java tem uma biblioteca mais rica do que C, incluindo um conjunto de classes
de cont�iner para agrupar os objetos existentes de diversas maneiras. Um exemplo �
uma Vector que fornece um array que pode ser aumentado dinamicamente e armazenar
qualquer tipo Object. Outro exemplo � a classe Hashtable, com a qual � poss�vel
armazenar e recuperar valores de um tipo usando objetos de outro tipo, como chaves.
Em nosso aplicativo, as Vectors de strings s�o a op��o natural para conter
os prefixos e sufixos. Podemos usar uma Hashtable cujas chaves s�o vetores prefixo
e cujos valores s�o vetores sufixo. A terminologia desse tipo de constru��o � um
mapa de prefixos para sufixos. Em Java, n�o precisamos de uma State expl�cita
porque a Hashtable conecta (mapeia) explicitamente os prefixos para os sufixos.
Esse projeto � diferente da vers�o C, na qual instalamos as estruturas State que
cont�m as listas de prefixo e sufixo, e fizemos o hash do prefixo para recuperar a
State completa.
Uma Hashtable fornece um m�todo put para armazenar um par de valores-chave, e um
m�todo get para recuperar o valor de uma chave:

Hashtable h = new Hashtable();


h.put(key, value);
Sometype v = (Sometype)h.get(key);

Nossa implementa��o tem tr�s classes. A primeira classe, Prefix, cont�m as palavras
do prefixo:

class Prefix {
public Vector pref; // NPREF palavras adjacentes da entrada
...

A segunda classe, Chain, l� a entrada, constr�i a tabela hash e gera a sa�da. Aqui
est�o suas vari�veis de classe:

class Chain {
static final int NPREF = 2;
// tamanho do prefixo
static final String NONWORD = "\n";
// "palavra" que n�o pode aparecer
Hashtable statetab = new Hashtable();
// key = Prefixo, valor = Vetor sufixo
Prefix prefix = new Prefix(NPREF, NONWORD);
// prefixo inicial
Random rand = new Random();
...

A terceira classe � a interface public; ela cont�m main e instancia uma


Chain:

class Markov {
static final int MAXGEN = 10000; // m�ximo de palavras geradas public static void
main(String[] args) throws IOException {
Chain chain = new Chain();
int nwords = MAXGEN;

chain.build(System.in);
chain.generate(nwords);
}
}

Quando uma inst�ncia da classe Chain � criada, ela, por sua vez, cria uma
tabela hash e define o prefixo inicial de NPREF NONWORDs. A fun��o build usa a
fun��o de biblioteca StreamTokenizer para analisar a entrada em palavras separadas
por caracteres de espa�o em branco. As tr�s chamadas antes do loop definem o
tokenizer no estado adequado para nossa defini��o de "palavra".

// Chain build: constr�i a tabela State


// a partir da stream de entrada
void build(InputStream in) throws IOException
{
StreamTokenizer st = new StreamTokenizer(in);

st.resetSyntax(); //remove regras padr�o


st.wordChars(0, Character.MAX_VALUE); // ativa todas as chars st.whitespaceChars(0,
' '); // exceto os brancos
while (st.nextToken() != st.TT_EOF)
add(st.sval);
add(NONWORD);
}

A fun��o add recupera o vetor de sufixos do prefixo atual da tabela hash; se


n�o houver nenhum (o vetor � null), add cria um vetor novo e um prefixo novo que
s�o armazenados na tabela hash. Em ambos os casos, ela adiciona a palavra nova ao
vetor de sufixo e avan�a o prefixo soltando a primeira palavra e adicionando a
palavra nova no final.

// Chain add: adiciona a palavra � lista de sufixo,


// atualiza o prefixo
void add(String word)
{
Vector suf = (Vector) statetab.get(prefix);
if (suf == null) {
suf = new Vector();
statetab.put(new Prefix(prefix), suf);
}
suf.addElement(word);
prefix.pref.removeElementAt(0);
prefix.pref.addElement(word);
}

Observe que se suf � null, add instala um prefix novo na tabela hash, em vez de
instalar o prefix propriamente dito. Isso acontece porque a classe Hashtable
armazena os itens por refer�ncia, e se n�o fizermos uma c�pia, podemos sobrepor os
dados da tabela. Essa quest�o � igual �quela com a qual tivemos de lidar no
programa em C.
A fun��o de gera��o � semelhante �quela da vers�o em C, por�m ligeira-mente
mais compacta porque ela pode indexar um elemento de vetor aleat�rio diretamente,
em vez de fazer o looping atrav�s de uma lista.

// Chain generate: gera as palavras de sa�da


void generate(int nwords)
{
prefix = new Prefix(NPREF, NONWORD);
for (int i =0; i < nwords; i++) {
Vector s = (Vector) statetab.get(prefix);
int r = Math.abs(rand.nextInt()) % s.size();
String suf = (String) s.elementAt(r);
if (suf.equals(NONWORD))
break;
System.out.println(suf);
prefix.pref.removeElementAt(0);
prefix.pref.addElement(suf);
}
}

Os dois construtores de prefix criam novas inst�ncias para os dados


fornecidos. O primeiro copia um prefix existente, e o segundo cria um prefixo a
partir de n c�pias de uma string; n�s o usamos para fazer NPREF c�pias de NONWORD
na inicializa��o:

//Prefix constructor: duplica o prefixo existente


Prefix(Prefix p)
{
pref = (Vector) p.pref.clone();
}

// Construtor de prefixo: n c�pias de str


Prefix(int n, String str)
{
pref = new Vector();
for (int i = 0; i < n; i++)
pref.addElement(str);
}

Prefix tamb�m tem dois m�todos, hashCode e equals, os quais s�o chamados
implicitamente pela implementa��o de Hashtable para o �ndice e pesquisam a tabela.
A necessidade de ter uma classe expl�cita para esses dois m�todos para Hashtable �
que nos for�ou a fazer de prefix uma classe completa, em vez de apenas uma Vector
como o sufixo.
O m�todo hashCode constr�i um �nico valor hash, combinando o conjunto de
hashCodes dos elementos do vetor:

static final int MULTIPLIER = 31; // para hashCode()

// Prefix hashCode: gera a hash de todas as palavras de prefixo


public int hashCode()
{
int h = 0;

for (int i = 0; i < pref.size(); i++)


h = MULTIPLIER * h + pref.elementAt(i).hashCode();
return h;
}

e equals faz uma compara��o elementwise das palavras dos dois prefixos:

// Prefix equals: compara as palavras iguais de dois prefixos


public boolean equals(0bject o)
{
Prefix p = (Prefix)o;

for (int i = 0; i < pref.size(); i++)


if (!pref.elementAt(i).equals(p.pref.elementAt(i)))
return false;
return true;
}

O programa Java � significativamente menor do que o programa C e cuida de


mais detalhes; Vectors e Hashtable s�o os exemplos �bvios. Em geral, o
gerenciamento de armazenamento � f�cil pois os vetores podem ser aumentados
conforme a necessidade, e a coleta de lixo cuida de retomar a mem�ria que n�o �
mais referenciada. Mas para usar a classe Hashtable, ainda precisamos escrever as
fun��es hashCode e equals, de modo que Java n�o est� tomando conta de todos os
detalhes.
Comparando o modo como os programas em C e Java representam e operam na
mesma estrutura de dados b�sica, vemos que a vers�o em Java tem uma separa��o
melhor da funcionalidade. Por exemplo, seria f�cil alternar de Vectors para arrays.
Na vers�o em C, tudo sabe o que tudo o mais faz: a tabela hash opera nos arrays que
s�o mantidos em diversos lugares, lookup sabe o layout das estruturas State e
Suffix, e todos sabem o tamanho do array de prefixo.

% java Markov <jr_chemistry.txt | fmt


Wash the blackboard. Watch it dry. The water goes
into the air. When water goes into the air it
evaporates. Tie a damp cloth to one end of a solid or
liquid. Look around. What are the solid things?
Chemical changes take place when something burns. If
the burning material has liquids, they are stable and
the sponge rise. It looked like dough, but it is
burning. Break up the lump of sugar into small pieces
and put them together again in the bottom of a liquid.

� Exerc�cio 3-4. Examine a vers�o em Java de markov para usar um array no lugar de
uma Vector para o prefixo da classe State. ?

3.6 C++

Nossa terceira implementa��o � em C++. Como a C++ � quase um superconjunto


da C, ela pode ser usada como se fosse C, com algumas poucas altera��es de nota��o,
e nossa vers�o original em C de markov tamb�m � um programa legal em C++.
Entretanto, um uso mais apropriado para a C++ seria para definir classes para os
objetos do programa, mais ou menos como fizemos em Java. Isso nos permitiria
ocultar os detalhe da implementa��o. Resolvemos ir mais adiante usando a Standard
Template Library ou STL, uma vez que a STL tem mecanismos incorporados que far�o
grande parte daquilo que precisamos. O padr�o ISO para a C++ inclui a STL como
parte da defini��o da linguagem.
A STL fornece cont�ineres tais como vetores, listas e conjuntos, al�m de uma
fam�lia de algoritmos fundamentais de pesquisa, classifica��o, inser��o e exclus�o.
Usando os recursos de modelo da C++, todo algoritmo STL funciona em uma variedade
de cont�ineres, incluindo tanto os tipos definidos pelo usu�rio quanto os tipos
incorporados como os inteiros. Os cont�ineres s�o expressos como modelos da C++ que
s�o instanciados para os tipos espec�ficos de dados; por exemplo, h� um cont�iner
de vetor que pode ser usado para criar tipos parti-culares como vector<int> ou
vector<string>. Todas as opera��es de vector, incluindo os algoritmos padr�o para
classificar, podem ser usadas em tais tipos de dados.
Al�m de um cont�iner vector, que � semelhante ao vector da Java, a STL
fornece um cont�iner deque. Um deque (pronuncia-se "deck") � uma fila de dois lados
que coincide com aquilo que fazemos com os prefixos: ela cont�m NPREF elementos, e
nos permite escolher o primeiro elemento e adicionar um novo ao final, no tempo
O(1) para ambos. O STL deque � mais geral do que precisamos, uma vez que permite
empurrar e tirar de qualquer lado, mas as garantias de desempenho o tornam uma
op��o �bvia.
A STL tamb�m fornece um cont�iner map expl�cito, com base em �rvores
equilibradas, que armazenam os pares de valores-chave e fornecem a recupera��o em
O(logn) do valor associado a qualquer chave. Os mapas podem n�o ser t�o eficientes
quanto as tabelas hash O(1), mas � bom o fato de n�o precisar escrever nenhum
c�digo para us�-los. (Algumas bibliotecas C ++ n�o-padr�o incluem um cont�iner hash
ou hash_map cujo desempenho pode ser melhor.)
Tamb�m usamos as fun��es incorporadas de compara��o, as quais neste caso
far�o compara��es de string usando as strings individuais do prefixo.
Com esses componentes em m�os, o c�digo fica mais f�cil. Aqui temos as
declara��es:

typedef deque<string> Prefix;


map<Prefix, vector<string> > statetab; // prefixo -> sufixos

A STL fornece um modelo para os deques. A nota��o deque<string> a espe-cializa para


um deque cujos elementos s�o strings. Como esse tipo aparece v�rias vezes no
programa, n�s usamos um typedef para dar-lhe o nome Prefix. O tipo map que armazena
os prefixos e sufixos ocorre apenas uma vez, por�m, e n�s n�o lhe demos um nome
separado. A declara��o map declara uma vari�vel statetab que � um mapa dos prefixos
para os vetores de strings. Isso � mais conveniente do que em C ou Java, porque n�o
precisamos fornecer uma fun��o hash ou um m�todo equals.
A rotina principal inicializa o prefixo, l� a entrada (da entrada padr�o,
chamada cin na biblioteca iostream C++), adiciona um final e gera a sa�da,
exatamente como nas vers�es anteriores:

// markov main: gera��o de texto aleat�rio da corrente de markov


int main(void)
{
int nwords = MAXGEN;
Prefix prefix; // prefixo de entrada atual

for (int i = 0; i < NPREF; i++) // define o prefixo inicial


add(prefix, NONWORD);
build(prefix, cin);
add(prefix, NONWORD);
generate(nwords);
return 0;
}
}

A fun��o build usa a biblioteca iostream para ler a entrada uma palavra de
cada vez:

// build: l� as palavras de entrada, constr�i a tabela state


void build(Prefix& prefix, istream& in)
{
string buf;

while (in " buf)


add(prefix, buf);
}

A string buf � aumentada conforme a necessidade para lidar com palavras de


comprimento arbitr�rio.
A fun��o add mostra mais das vantagens do uso da STL:

// add: adiciona a palavra � lista de sufixos, atualiza o prefixo


void add(Prefix& prefix, const string& s)
{
if (prefix.size() == NPREF) {
statetab[prefix].push_back(s);
prefix.pop_front();
}
prefix.push_back(s);
}
Muita coisa est� acontecendo nessas declara��es aparentemente simples. O cont�iner
map sobrecarrega subscrevendo (o operador []) para que ela se comporte como uma
opera��o lookup. A express�o statetab[prefix] faz uma lookup em statetab com Prefix
como a chave e retorna uma refer�ncia para a entrada desejada; o vetor � criado se
ele ainda n�o existir. As fun��es membro push_back de vector e deque empurram uma
string nova para o final do vetor ou deque; pop_front tira o primeiro elemento do
deque.
A gera��o � semelhante �s vers�es anteriores:

// generate: produz sa�da, uma palavra por linha


void generate(int nwords)
{
Prefix prefix;
int i;

for (i = 0; i < NPREF; i++) // redefine o prefixo inicial


add(prefix, NONWORD);
for (i = 0; i < nwords; i++) {
vector<string>& suf = statetab[prefix];
const string& w = suf[rand() % suf.size()];
if (w == NONWORD)
break;
cout " w " "\n";
prefix.pop_front(); // avan�ar
prefix.push_back(w);
}
}

Em geral, essa vers�o parece particularmente clara e elegante - o c�digo �


compacto, a estrutura de dados � vis�vel e o algoritmo � completamente
transparente. Infelizmente, h� um pre�o a pagar: a execu��o dessa vers�o � muito
mais lenta do que a vers�o original em C, embora ela n�o seja a mais lenta. Vamos
voltar �s medi��es do desempenho em breve.

� Exerc�cio 3-5. A grande vantagem da STL � a facilidade com a qual � poss�vel


experimentar diferentes estruturas de dados. Modifique a vers�o em C++ de Markov
para usar as diversas estruturas para representar o prefixo, lista de sufixos e
tabela state. Como o desempenho muda para as diferentes estruturas? ?

� Exerc�cio 3-6. Escreva uma vers�o em C++ que use apenas as classes e o tipo de
dados string, mas nenhum outro recurso avan�ado de biblioteca. Compare-a no estilo
e velocidade com as vers�es da STL. ?

3.7 Awk e Perl

Para encerrar o exerc�cio, tamb�m vamos escrever o programa em duas


linguagens de cria��o de scripts conhecidas, a Awk e a Perl. Elas fornecem os
recursos necess�rios para esse aplicativo, os arrays associativos e o tratamento de
string.
Um array associativo � um pacote conveniente de uma tabela hash; ele se
parece com um array mas seus subscritos s�o strings arbitr�rias ou n�meros, ou
listas deles separadas por v�rgulas. Ele � uma forma de mapa de um tipo de dados
para outro. Em Awk, todos os arrays s�o associativos, os quais s�o chamados de
"hashes", um nome que sugere como eles s�o implementados.
As implementa��es de Awk e Perl s�o especializadas para os prefixos de
comprimento 2.

# markov.awk: algoritmo cadeia de markov para prefixos de 2 palavras


BEGIN { MAXGEN = 10000; NONWORD = "\n"; wl = w2 = NONWORD }
{ for (i = 1; i <= NF; i++) { # ler todas as palavras
statetab[w1,w2,++nsuffix[wl,w2]] = $i
w1 = w2
w2 = $i
}
}
END {
statetab[wl,w2,++nsuffix[wl,w2]] = NONWORD # add tail
w1 = w2 = NONWORD
for (i = 0; i < MAXGEN; i++) { # generar
r = int(rand()*nsuffix[w1,w2]) + 1 # nsuffix >= l
p = statetab[w1,w2,r]
if (p == NONWORD)
exit
print p
w1 = w2 # avan�ar cadeia
w2 = p
}
}

Awk � uma linguagem de a��o padr�o: a entrada � lida uma linha de cada vez,
cada linha � coincidida com os padr�es, e para cada coincid�ncia a a��o
correspondente � executada. H� dois padr�es especiais, BEGIN e END, que coincidem
antes da primeira linha de entrada e depois da �ltima.
Uma a��o � um bloco de declara��es entre colchetes. Na vers�o Awk de Markov,
o bloco BEGIN inicializa o prefixo e algumas outras vari�veis.
O pr�ximo bloco n�o tem padr�o; portanto, como padr�o ele � executado uma
vez para cada linha de entrada. Awk divide automaticamente cada linha de entrada em
campos (palavras delimitadas por espa�o em branco) chamados $1 at� $NF; a vari�vel
NF � o n�mero de campos. A declara��o

statetab[w1,w2,++nsuffix[w1,w2]] = $i

constr�i o mapa de prefixo para sufixos. O array nsuffix conta os sufixos e o


elemento nsuffix[w1,w2] conta o n�mero de sufixos associados �quele prefixo. Os
pr�prios sufixos s�o armazenados nos elementos de array statetab[w1,w2,1],
statetab[w1,w2,2] e assim por diante.
Quando o bloco END � executado, toda a entrada foi lida. Nesse ponto, para
cada prefixo h� um elemento de nsuffix contendo a contagem de sufixos, e h� aquela
mesma quantidade de elementos de statetab contendo os sufixos.
A vers�o Perl � semelhante, mas usa um array an�nimo, em vez de um terceiro
subscrito para controlar os sufixos. Ela tamb�m usa v�rias atribui��es para
atualizar o prefixo. A Perl usa caracteres especiais para indicar os tipos de
vari�veis: $ marca um escalar e @ marca um array indexado, enquanto que os
colchetes [] s�o usados para indexar os arrays, e as chaves {} para indexar os
hashes.

# markov.pl: algoritmo cadeia de markov para prefixos de 2 palavras

$MAXGEN = 10000;
$NONWORD = "\n";
$w1 = $w2 = $NONWORD; # estado inicial
while (<>) { # l� cada linha de entrada
foreach (split) {
push(@{$statetab{$w1}{$w2}}, $_);
($w1, $w2) = ($w2, $_); # atribui��o m�ltipla
}
}
push(@{$statetab{$w1}{$w2}}, $NONWORD); # adiciona tail
$w1 = $w2 = $NONWORD;
for ($i = 0; $i < $MAXGEN; $i++) {
$suf = $statetab{$w1}{$w2}; # refer�ncia de array
$r = int(rand @$suf); # @$suf � o n�mero de elementos
exit if (($t = $suf->[$r]) eq $NONWORD);
print "$t\n";
($w1, $w2) = ($w2, $t); # avan�ar cadeia
}

Como no programa anterior, o mapa � armazenado com a vari�vel statetab. O


cora��o do programa � a linha:

push(@{$statetab{$w1}($w2)}, $_);

que empurra um sufixo novo para o final do array (an�nimo) armazenado em


statetab{$w1}{$w2}. Na fase de gera��o, $statetab{$w1}{$w2} � uma refer�n-cia para
um array de sufixos, e $suf->[$r] indica o sufixo de n�mero r-th.

Os programas Perl e Awk s�o curtos comparados �s tr�s vers�es anteriores,


mas eles s�o mais dif�ceis de adaptar para lidar com os prefixos que n�o s�o
exatamente duas palavras. O centro da implementa��o da STL em C++ (as fun��es add e
generate) tem tamanho compar�vel e parece mais claro. No entanto, as linguagens de
cria��o de scripts quase sempre s�o uma boa op��o para a programa��o experimental,
para criar prot�tipos e at� mesmo para o uso de produ��o quando o tempo de execu��o
n�o � muito importante.

� Exerc�cio 3-7. Modifique as vers�es Awk e Perl para lidar com prefixos de
qualquer comprimento. Experimente determinar qual efeito essa altera��o tem sobre o
desempenho. ?

3.8 Desempenho

N�s temos v�rias implementa��es para comparar. Medimos o tempo dos programas
do Livro dos Salmos da B�blia Segundo S�o Jo�o, o qual tem 42.685 palavras (5.238
palavras distintas, 22.482 prefixos). Esse texto tem frases repeti-das suficientes
("Aben�oados sejam ...") para que uma lista de sufixos tenha mais de 400 elementos
e h� algumas centenas de cadeias com dezenas de sufixos, de modo que esse � um bom
conjunto de dados para teste.

Blessed is the man of the net. Turn thee unto me, and raise me up, that I
may tell all my fears. They looked unto him, he heard. My praise shall be blessed.
Wealth and riches shall be saved. Thou has dealt well with thy hid treasure: they
are cast into a standing water, the flint into a standing water, and dry ground
into watersprings.

Os tempos da tabela seguinte s�o o n�mero de segundos para gerar 10.000 palavras
de sa�da; uma m�quina � um MIPS R10000 de 250MHz, executando o Irix 6.4, e a outra
� um Pentium II de 400 MHz, com 128 megabytes de mem�ria, executando o Windows NT.
O tempo de execu��o � quase que totalmente determinado pelo tamanho da entrada.
Comparativamente, a gera��o � muito r�pida. A tabela tamb�m inclui o tamanho
aproximado do programa em linhas de c�digo-fonte.

250MHz
R10000400MHz
Pentium IILinhas de c�digo-fonteC0,36 seg0,30
seg150Java4,99,2105C++/STL/deque2,611,270C++/STL/list1,71,570Awk2,22,1 20 Perl1,8
1,0 18
As vers�es C e C+ + foram compiladas com compiladores otimizados, enquanto
que a Java executa apenas os compiladores just-in-time ativados. Os tempos do Irix
C e C+ + s�o os mais r�pidos obtidos de tr�s compiladores diferentes; resultados
semelhantes foram observados nas m�quinas Sun SPARC e DEC Alpha. A vers�o em C do
programa � a mais r�pida por um fator grande, a Perl vem em segundo lugar. Os
tempos da tabela s�o um instant�neo de nossa experi�ncia com determinado conjunto
de compiladores e bibliotecas, entretanto voc� pode ter resultados diferentes em
seu ambiente.
Algo est� claramente errado com a vers�o deque STL no Windows. As
experi�ncias mostraram que o deque que representa o prefixo contribui com a maior
parte do tempo de execu��o, embora ele nunca contenha mais do que dois elementos.
Dever�amos esperar que a estrutura de dados central, o map, dominasse. A
altern�ncia de um deque para uma lista (a qual � uma lista duplamente encadeada na
STL) melhora o tempo dramaticamente. Por outro lado, a mudan�a de um mapa para um
cont�iner hash (n�o-padr�o) n�o fez nenhuma diferen�a no Irix; os hashes n�o
estavam dispon�veis em nossa m�quina com o Windows. Essas mudan�as exigiram apenas
a substitui��o da palavra list pela palavra deque ou hash para o map em dois
lugares e na compila��o, e isso � um testemunho da solidez fundamental do projeto
STL. Conclu�mos que a STL, que � um componente novo da C++, ainda sofre com as
implementa��es imaturas. O desempenho � imprevis�vel entre as implementa��es da STL
e entre as estruturas de dados individuais. O mesmo vale para a Java, onde as
implementa��es tamb�m est�o mudando rapidamente.
Existem alguns desafios interessantes no teste de um programa destinado a
produzir sa�da aleat�ria volumosa. Como sabemos se ele vai funcionar? Corno saber
se ele funciona o tempo todo? O Cap�tulo 6, que discute testes, cont�m algumas
sugest�es e descreve como testamos os programas Markov.

3.9 Li��es

O programa de Markov tem uma hist�ria longa. A primeira vers�o foi escrita
por Don P. Mitchell, adaptada por Bruce Ellis, e aplicada �s atividades
desconstrutivistas humor�sticas em toda a d�cada de 1980. Ela ficou adormecida at�
que pensamos em us�-la em um curso universit�rio como ilustra��o para o projeto de
programas. Em vez de tirar a poeira do original, n�s o reescrevemos desde o in�cio
em C para refrescar nossas mem�rias sobre as diversas quest�es que surgem, e depois
o escrevemos novamente em v�rias outras linguagens, usando os idiomas exclusivos de
cada linguagem para expressar a mesma id�ia b�sica. Depois do curso, retrabalhamos
os programas muitas vezes para melhorar a clareza e a apresenta��o.
Durante todo aquele tempo, por�m, o projeto b�sico permaneceu o mesmo. A
vers�o mais antiga usava a mesma abordagem que apresentamos aqui, embora ela
empregasse uma segunda tabela hash para representar as palavras individuais. Se
tiv�ssemos de reescrev�-lo novamente, provavelmente n�o mudar�amos muita coisa. O
projeto de um programa est� enraizado no layout de seus dados. As estruturas de
dados n�o definem todos os detalhes, mas elas d�o forma � solu��o geral.
Algumas op��es de estrutura de dados fazem pouca diferen�a, tais como listas
versus arrays que podem ser aumentados. Algumas implementa��es generalizam melhor
do que outras - os c�digos Perl e Awk poderiam ser modificados facilmente para
prefixo de uma ou tr�s palavras, mas a parame-triza��o da op��o seria esquisita.
Como conv�m �s linguagens orientadas para o objeto, pequenas altera��es feitas nas
implementa��es em C++ e Java tornariam as estruturas de dados adequadas para
objetos que n�o fossem texto em ingl�s, por exemplo programas (onde o espa�o em
branco seria significativo), ou notas musicais, ou mesmo cliques do mouse e
sele��es de menu para gerar sequ�ncias de teste.
Obviamente enquanto as estruturas de dados s�o muito parecidas, h� uma ampla
varia��o na apar�ncia geral dos programas, no tamanho do c�digo-fonte e no
desempenho. Muito aproximadamente, as linguagens de n�vel mais alto resultam em
programas mais lentos do que as linguagens de n�vel inferior, embora n�o seja bom
fazer generaliza��es que n�o sejam quantitativas. Os blocos construtores grandes,
como a C++ STL ou os arrays associativos e o tratamento de string das linguagens de
cria��o de scripts podem levar a c�digo mais compacto e tempo de desenvolvimento
menor. Tudo isso tem um pre�o, embora a penalidade sobre o desempenho possa n�o
importar muito para os programas, como o Markov, que s�o executados apenas por
alguns segundos.
Menos claro, por�m, � o modo como avaliamos a perda de controle e vis�o
quando a pilha de c�digo fornecida pelo sistema fica t�o grande que n�o se sabe
mais o que est� acontecendo l� embaixo. Esse � o caso da vers�o STL: seu desempenho
� imprevis�vel e n�o h� uma maneira f�cil de abord�-lo. Uma implementa��o imatura
que usamos precisou ser reparada antes de executar nosso programa. Poucos de n�s
t�m os recursos ou a energia para detectar e consertar tais problemas.
Essa � uma preocupa��o sempre presente e cada vez maior no software: as
bibliotecas, interfaces e ferramentas se tornam mais complicadas e menos
compreens�veis e control�veis. Quando tudo funciona, os ambientes ricos de
programa��o podem ser muito produtivos, mas quando eles falham h� pouca coisa a
recorrer. Sem d�vida, podemos nem perceber que alguma coisa est� errada quando os
problemas envolvem o desempenho ou erros l�gicos sutis.
O projeto e a implementa��o deste programa ilustram v�rias li��es para os
programas maiores. Primeiro h� a import�ncia de selecionar algoritmos e estruturas
de dados simples, os mais simples que far�o o trabalho em tempo razo�vel para o
tamanho esperado do problema. Se outra pessoa j� os escreveu e os colocou em uma
biblioteca para voc�, isso � melhor ainda. Nossa implementa��o C++ se beneficiou
com isso.
Seguindo o conselho de Brook, achamos melhor come�ar pelo projeto detalhado
com as estruturas de dados, guiados pelo conhecimento daquilo para o qual os
algoritmos podem ser usados. Com as estruturas de dados definidas, o c�di�o fica
mais f�cil.
� dif�cil criar um programa completamente e depois constru�-lo. A constru-
��o de programas reais envolve a itera��o e experimenta��o. O ato de construir nos
for�a a esclarecer as decis�es que haviam sido desprezadas anteriormente. Esse
certamente foi o caso destes nossos programas, os quais passaram por muitas
mudan�as de detalhes. Sempre que poss�vel, comece com algo simples e evolua
seguindo sua experi�ncia. Se o nosso objetivo fosse apenas escrever uma vers�o
pessoal do algoritmo de cadeia de Markov s� para nos divertirmos, n�s o
escrever�amos em Awk ou Perl - embora n�o com tanto polimento quanto aqueles
mostrados aqui.
Entretanto, o c�digo de produ��o exige muito mais esfor�o do que os
prot�tipos. Se pensarmos nos programas apresentados aqui como c�digo de produ��o
(uma vez que eles foram polidos e testados exaustivamente), a qualidade da produ��o
exige uma ou duas ordens de magnitude a mais de esfor�o do que um programa
destinado ao uso pessoal.

� Exerc�cio 3-8. J� vimos vers�es do programa de Markov em uma ampla variedade de


linguagens, incluindo Scheme, Tcl, Prolog, Python, Generic Java, ML e Haskell. Cada
uma apresenta seus pr�prios desafios e suas vantagens. Implemente o programa em sua
linguagem preferida e compare seu aspecto e desempenho gerais. ?

Leitura suplementar

A Standard Template Library � descrita em uma variedade de livros, incluindo


Generic Programming and the STL, de Matthew Austern (Addison-Wesley, 1998). A
refer�ncia definitiva sobre a pr�pria C++ � The C++ Programming Language, de Bjarne
Stroustrup (3a edi��o, Addison-Wesley, 1997). Para Java, consultamos o livro The
Java Programming Language, 2"d Edition, de Ken Arnold e James Gosling (Addison-
Wesley, 1998). A melhor descri��o da Perl est� em Programming Perl, 2"d Edition, de
Larry Wall, Tom Christiansen e Randal Schwartz (O'Reilly, 1996).
A ideia dos padr�es de projeto � que haja apenas algumas poucas constru��es
de projeto distintas na maioria dos programas, assim como h� apenas algumas
estruturas de dados b�sicas. Isto � aproximadamente equivalente aos idiomas de
c�digo que discutimos no Cap�tulo 1. A refer�ncia padr�o � Design Patterns:
Elements of Reusable Object-Oriented Software, de Erich Gamma, Richard Helm, Ralph
Johnson e John Vlissides (Addison-Wesley, 1995).
As aventuras picarescas do programa markov, originalmente chamado shaney,
foram descritas na coluna "Computing Recreations" de junho de 1989, na Scientific
American. O artigo foi republicado em The Magic Machine, de A. K. Dewdney (W H.
Freeman, 1990).

Interfaces

Antes de construir uma parede eu perguntaria


O que vai ficar dentro ou fora da parede,
E a quem eu poderia ofender.
H� algo em mim que n�o gosta de muros,
e os quer abaixo.
Robert Frost, Mending Wall

A ess�ncia do projeto � o equil�brio entre objetivos e restri��es concor-


rentes. Embora haja muitas op��es a serem feitas quando se escreve um sistema
pequeno e restrito, as ramifica��es das op��es em particular permanecem dentro do
sistema e afetam apenas o programador individual. Mas quando o c�digo ser� usado
por outras pessoas, as decis�es assumem repercuss�es maiores.
Entre as quest�es a serem trabalhadas em um projeto est�o:
* Interfaces: quais s�o os servi�os e acesso fornecidos? A interface �, na verdade,
um contrato entre fornecedor e cliente. O desejo � fornecer servi�os que sejam
uniformes e convenientes, com funcionalidade suficiente para serem f�ceis de usar,
mas n�o muito a ponto de se tornarem dif�ceis de gerenciar.
* Ocultamento de informa��es: quais informa��es est�o vis�veis e quais s�o
privadas? Uma interface deve fornecer acesso direto aos componentes, ocultando ao
mesmo tempo detalhes da implementa��o para que eles possam ser alterados sem afetar
os usu�rios.
* Gerenciamento de recursos: quem � respons�vel por gerenciar a mem�ria e outros
recursos limitados? Aqui, os problemas principais s�o a aloca��o e libera��o do
armazenamento, e o gerenciamento das c�pias compartilhadas de informa��es.
* Tratamento de erros: quem detecta os erros, quem os reporta e como? Quando um
erro � detectado, qual tipo de recupera��o � tentado?
No Cap�tulo 2 vimos as partes individuais - as estruturas de dados - a partir
das quais um sistema � constru�do. No Cap�tulo 3, vimos como combinar essas partes
em um programa pequeno. O t�pico agora s�o as interfaces entre os componentes que
podem vir de fontes diferentes. Neste cap�tulo, ilustramos o projeto da interface,
construindo uma biblioteca e estruturas de dados para uma tarefa comum. Nesse meio
tempo, vamos apresentar alguns princ�pios do projeto. Geralmente h� um n�mero
enorme de decis�es a serem tomadas, mas a maioria � tomada quase que
inconscientemente. Sem esses princ�pios, o resultado quase sempre s�o interfaces
casuais que frustram e impedem os programadores todos os dias.

4.1 Valores separados por v�rgulas

Valores separados por v�rgulas, ou CSV � o termo para uma representa��o


natural e amplamente usada para os dados tabulares. Cada linha de uma tabela � uma
linha de texto; os campos de cada linha s�o separados por v�rgulas. A tabela no
final do cap�tulo anterior pode come�ar desta maneira no formato CSV:

,"ZSOMHz","400MHz","Lines of"
,"R10000","Pentium II","source code"
C,0.36 sec,0.30 sec,150
Java,4.9,9.2,105
Esse formato � lido e gravado por programas como as planilhas. N�o � acaso o
fato de que eles tamb�m aparecem nas p�ginas da Web de servi�os, tais como cota��es
de pre�os de a��es. Uma p�gina conhecida da Web com cota��es de a��es apresenta
esta exibi��o:

S�mboloUltimo Preg�oAltera��oVolumeLU2:19PM 86-1/4+4-1/16


4.94%5.804,800T2:19PM 60-11/16-1-3/16 -1.92%2.468,000MSFT2:24PM
106-9/16+ 1-3/8 +1.31%11.474,900Download Spreadsheet Format

A recupera��o de n�meros por meio da intera��o com um browser da Web �


efetiva, por�m demorada. � um aborrecimento invocar um browser, aguardar, observar
uma infinidade de an�ncios, digitar uma lista de a��es, esperar, esperar, esperar,
depois observar outra infinidade de an�ncios, tudo para ter alguns n�meros. Para
processar mais os n�meros � preciso mais intera��o ainda; selecionando o link
"Download Spreadsheet Format" voc� recupera um arquivo contendo grande parte das
mesmas informa��es das linhas de dados CSV como estes (editado para se ajustar):

"LU",86.25, "11/4/1998","2:19PM",+4.0625,
83.9375,86.875,83.625,5804800
"T" ,60.6875,"11/4/1998","2:19PM",-1.1875,
62.375,62.625,60.4375,2468000
"MSFT",106.5625,"11/4/1998","2:24PM",+1.375,
105.8125,107.3125,105.5625,11474900

Evidente pela sua aus�ncia nesse processo est� o princ�pio de deixar a


m�quina trabalhar. Os browsers permitem que o seu computador acesse os dados em um
servidor remoto, mas seria mais conveniente recuperar os dados sem a intera��o
for�ada. Abaixo de todos os bot�es que s�o pressionados h� um procedimento
puramente textual - o browser l� alguma HTML, voc� digita algum texto, o browser
envia isso para um servidor e l� alguma HTML de volta. Com as ferramentas e a
linguagem certas � f�cil recuperar as informa��es automati-camente. Aqui temos um
programa na linguagem Tcl para acessar o site na Web de cota��es de a��es e
recuperar os dados CSV no formato acima, precedido por algumas linhas de header:

# getquotes.tcl: pre�os das a��es da Lucent, AT&T, Microsoft

set so [socket quote.yahoo.com 80] ;# conectar ao servidor


set q "/d/quotes.csv?s=LU+T+MSFT&f=slldltlclohgv"

puts $so "GET $q HTTP/1.0\r\n\r\n" ;# enviar solicita��o


flush $so
puts [read $so] ;# ler e imprimir resposta

A seq��ncia cr�tica f =... que vem depois dos s�mbolos de verifica��o � uma
string n�o documentada de controle, como o primeiro argumento de printf, que
determina quais valores devem ser recuperados. Por experimenta��o, deter-minamos
que s identifica o s�mbolo da a��o, l1 o �ltimo pre�o, c1 a altera��o desde ontem e
assim por diante. Os detalhes n�o s�o importantes, pois est�o sujeitos a altera��es
de qualquer forma, mas sim a possibilidade da automa��o: recuperar as informa��es
desejadas e convert�-las para a forma que precisamos sem nenhuma interven��o
humana. N�s podemos deixar a m�quina fazer o trabalho.
Geralmente � preciso uma fra��o de um segundo para executar getquotes, bem
menos do que a intera��o com um browser. Depois que tivermos os dados, vamos
process�-los um pouco mais. Formatos de dados como CSV funcionam melhor se houver
bibliotecas convenientes para converter de e para o formato, talvez aliadas a algum
processamento auxiliar, tal como as convers�es num�ricas. Mas n�s n�o conhecemos
uma biblioteca p�blica para lidar com os CSV; portanto, vamos escrev�-la n�s
mesmos.
Nas pr�ximas se��es, vamos construir tr�s vers�es de uma biblioteca para ler
os dados CSV e convert�-los para uma representa��o interna. Nesse meio tempo, vamos
falar sobre quest�es que surgem quando se cria software que deve funcionar com
outro software. Por exemplo, n�o parece haver uma defini��o padr�o para o CSV;
portanto, a implementa��o n�o pode se basear em uma especifica��o precisa, uma
situa��o comum no projeto das interfaces.

4.2 Uma biblioteca prot�tipo

� pouco prov�vel que consigamos o projeto de uma biblioteca ou interface na


primeira tentativa. Como escreveu Fred Brooks: "planeje jogar uma fora; voc� vai
acabar jogando mesmo". Brooks estava escrevendo sobre sistemas grandes, mas a id�ia
� relevante para qualquer software. Isso n�o � comum at� voc� ter constru�do e
usado uma vers�o do programa, quando ent�o voc� j� entende as quest�es o suficiente
para projetar certo.
Dentro desse esp�rito, vamos abordar a constru��o de uma biblioteca para o
CSV, construindo uma para jogar fora, um prot�tipo. Nossa primeira vers�o vai
ignorar muitas das dificuldades de uma biblioteca feita completamente, mas ser�
suficientemente completa para ser �til e permitir que tenhamos uma certa
familiaridade com o problema.
Nosso ponto de partida � uma fun��o csvgetline que l� uma linha de dados CSV
de um arquivo para um buffer, a divide em campos de um array, remove as aspas e
retorna o n�mero de campos. Ao longo dos anos, escrevemos c�digo semelhante em
quase todas as linguagens que conhecemos; portanto, essa � uma tarefa conhecida.
Aqui temos uma vers�o de prot�tipo em C. N�s a marcamos como question�vel, porque
ela � apenas um prot�tipo:

? char buf[200]; /* buffer de linha de entrada */


? char *field[20]; /* campos */
?
? /* csvgetline: l� e analisa a linha, retorna a contagem de campo*/
? /* entrada de exemplo: "LU",86.25,"11/4/1998","2:19PM",+4.0625 */
? int csvgetline(FILE *fin)
? {
? int nfield; char *p, *q;
?
? if (fgets(buf, sizeof(buf), fin) == NULL)
? return -1;
? nfield = 0;
? for (q = buf; (p=strtok(q, ",\n\r"))!= NULL; q = NULL)
? field[nfield++] = unquote(p);
? return nfield;
? )

O coment�rio na parte superior da fun��o inclui um exemplo do formato de


entrada que o programa aceita; tais coment�rios s�o �teis para os programas que
analisam entrada confusa.
O formato CSV � complicado demais para ser analisado facilmente por scanf,
portanto usamos a fun��o da biblioteca padr�o strtok. Cada chamada de strtok(p,s)
retorna um ponteiro para o primeiro token dentro de p, consistindo nos caracteres
que n�o est�o em s; strtok encerra o token sobrepondo o caractere seguinte da
string original com um byte null. Na primeira chamada, o primeiro argumento de
strtok � a string a ser examinada; as chamadas subsequentes usam NULL para indicar
que o exame deve retomar de onde parou na chamada anterior. Essa � uma interface
fraca. Como strtok armazena uma vari�vel em um lugar secreto entre as chamadas,
apenas uma sequ�ncia de chamadas pode estar ativa de cada vez. As chamadas
entrela�adas n�o-relacionadas interferir�o umas nas outras.
Nossa fun��o unquote remove as aspas iniciais e finais que aparecem na
entrada de exemplo acima. Ela n�o lida com as cita��es aninhadas e, assim, embora
seja suficiente para um prot�tipo ela n�o � geral.

? /* unquote: remove as aspas iniciais e finais */


? char *unquote(char *p)
? {
? if (p[0] == '"') {
? if (p[strlen(p)-l] == '"')
? p[strlen(p)-l] = '\0';
? p++;
? }
? return p;
? }

Um programa simples de teste ajuda a verificar se csvgetline funciona:

? /* csvtest main: testa a fun��o csvgetline */


? int main(void)
? {
? int i, nf;
?
? while ((nf = csvgetline(stdin)) != -1)
? for (i = 0; i < nf; i++)
? printf("field[%d] = '%s'\n", i, field[i]);
? return 0;
? }

printf inclui os campos dentro de ap�strofos, os quais os demarcam e ajudam a


revelar os bugs que lidam incorretamente com o espa�o em branco.
Agora podemos executar isso na sa�da produzida por getquotes.tcl:

% getquotes.tcl | csvtest
...
field[0] = 'LU'
field[l] = '86.375'
field[2] = '11/5/1998'
field[3] = '1:01PM'
field[4] = '-0.125'
f1eld[5] = '86'
field[6] = '86.375'
field[7] = '85.0625'
field[8] = '2888600'
field[0] = 'T'
field[l] = '61.0625'
...

(Editamos as linhas do header HTTP.)

Agora temos um prot�tipo que parece funcionar nos dados da classifica��o


mostrada acima. Mas seria prudente experiment�-la tamb�m em alguma outra coisa,
particularmente se planejamos deixar que os outros a usem. Descobrimos outro site
da Web que descarrega cota��es de a��es e obtivemos um arquivo com informa��es
similares, mas em uma forma diferente: retornos de carro (\r) em vez de linhas
novas para separar os registros, e sem retorno de carro de encerramento no final do
arquivo. N�s o editamos e formatamos para que ele se ajustasse � p�gina:

"Ticker","Price","Change","Open","Prev Close","Day High",


"Day Low","52 Week High","52 Week Low","Dividend",
"Yield","Volume","Average Volume","P/E"
"LU",86.313,-0.188,86.000,86.500,86.438,85.063,108.50,
36.18,0.16,0.1,2946700,9675000,N/A
"T",61.125,0.938,60.375,60.188,61.125,60.000,68.50,
46.50,1.32,2.1,3061000,4777000,17.0
"MSFT",107.000,1.500,105.313,105.500,107.188,105.250,
119.62,59.00,N/A,N/A,7977300,16965000,51.O

Com essa entrada, nosso prot�tipo falhou de forma terr�vel.


Criamos o nosso prot�tipo ap�s examinar uma fonte de dados, e o testamos
originalmente apenas nos dados daquela mesma fonte. Assim sendo, n�o dever�amos nos
surpreender quando o primeiro encontro com uma fonte diferente revelasse falhas
s�rias. Linhas de entrada longas, muitos campos e separadores inesperados ou
faltando, tudo isso causa problemas. Esse fr�gil prot�tipo poderia servir para o
uso pessoal ou para demonstrar a possibilidade de uma abordagem, mas nada al�m
disso. Est� na hora de repensar o projeto antes de experimentar outra
implementa��o.
Tomamos um n�mero grande de decis�es no prot�tipo, tanto impl�citas quanto
expl�citas. Aqui temos algumas das op��es que fizemos, nem sempre da melhor maneira
para uma biblioteca de fins gerais. Cada uma levanta uma quest�o que precisa de
mais aten��o.
* O prot�tipo n�o lida com as linhas de entrada longas ou com muitos campos, Ele
pode dar respostas erradas ou entrar em pane porque nem mesmo verifica os
overflows, muito menos retorna valores sens�veis em caso de erros.
* A entrada � assumida como consistindo em linhas terminadas por caracteres
newline.
* Os campos s�o separados por v�rgulas e as aspas s�o removidas. N�o h� provis�o
para a incorpora��o de aspas ou v�rgulas.
* A linha de entrada n�o � preservada; ela � sobreposta no processo da cria��o dos
campos.
* Nenhum dado � salvo de uma linha de entrada para a pr�xima. Se algo precisa ser
lembrado, deve-se fazer uma c�pia.
* O acesso aos campos � feito por meio de uma vari�vel global, o array field, o
qual � compartilhado por csvgetline e pelas fun��es que o chamam. Na existe nenhum
controle do acesso ao conte�do dos campos ou ponteiros. Tamb�m n�o existe nenhuma
tentativa de evitar o acesso al�m do �ltimo campo.
* As vari�veis globais tornam o projeto inadequado para um ambiente multiencadeado
ou mesmo para duas sequ�ncias de chamadas entre-la�adas.
* Quem est� chamando deve abrir e fechar os arquivos explicitamente; csvgetline l�
apenas os arquivos abertos.
* A entrada e a divis�o est�o ligadas de forma emaranhada: cada chamada l� uma
linha e a divide em campos, independente do aplicativo precisar daquele servi�o.
* O valor de retorno � o n�mero de campos da linha; cada linha deve ser dividida
para calcular esse valor. Tamb�m n�o h� como distinguir os erros do final do
arquivo.
* N�o h� como alterar nenhuma dessas propriedades sem alterar o c�digo.
Essa longa, por�m incompleta, lista ilustra algumas das poss�veis op��es de
projeto. Cada decis�o est� entrela�ada em todo o c�digo. Ele � bom para uma tarefa
r�pida, como analisar um formato fixo a partir de uma fonte conhecida. Mas e se o
formato mudar, ou aparecer uma v�rgula dentro de uma string com aspas, ou se o
servidor produzir uma linha longa ou muitos campos?
Isso aparentemente � algo simples de resolver, uma vez que a "biblioteca" �
pequena e apenas um prot�tipo. Imagine, por�m, que ap�s ficar alguns meses ou anos
na prateleira, o c�digo se torne parte de um programa maior cuja especifica��o muda
com o passar do tempo. Como csvgetline vai se adaptar? Se aquele programa for usado
por outras pessoas, as op��es r�pidas feitas no projeto original podem criar
problemas que surgir�o mais tarde. Esse cen�rio � representativo da hist�ria de
muitas interfaces ruins. � triste o fato de que muito c�digo "r�pido e sujo" acaba
entrando no software de uso amplo, onde ele permanece sujo e quase sempre n�o t�o
r�pido quando deveria ser.
4.3 Uma biblioteca para as outras pessoas

Usando aquilo que aprendemos com o prot�tipo, agora queremos construir uma
biblioteca de uso geral. O requisito mais �bvio � que devemos tornar csvgetline
mais robusta, para que ela trate de linhas longas ou de muitos campos. Ela tamb�m
deve ser mais cuidadosa na an�lise dos campos.
Para criar uma interface que as outras pessoas possam usar, vamos considerar
as quest�es listadas no in�cio deste cap�tulo: interfaces, ocultamente de
informa��es, gerenciamento de recursos e tratamento de erros. O relacionamento
entre essas quest�es afeta bastante o projeto. Nossa separa��o para essas quest�es
� um pouco arbitr�ria, uma vez que elas est�o inter-relacionadas.

Interface. Optamos por tr�s opera��es b�sicas:


char *csvget1ine(FILE *): l� uma linha CSV nova
char *csvfield(int n): retorna o campo de n�mero n da linha atual
int csvnfield(void): retorna o n�mero de campos da linha atual
Qual valor de fun��o deve ser retornado por csvgetline? � desej�vel que se
retorne o m�ximo conveniente de informa��es �teis, o que sugere o retorno do n�mero
de campos, como no prot�tipo. Mas o n�mero de campos deve ser calculado mesmo
quando os campos n�o est�o sendo usados. Outro valor poss�vel � o comprimento da
linha de entrada, que � afetado quando a newline do final � preservada. Depois de
v�rias experi�ncias, resolvemos que csvgetline retornar� um ponteiro para a linha
original de entrada, ou NULL se o final do arquivo foi atingido.
Vamos remover a newline do final da linha retornada por csvgetline, uma vez
que ela pode ser facilmente restaurada se for preciso.
A defini��o de um campo � complicada. Tentamos fazer coincidir aquilo que
observamos empiricamente nas planilhas e em outros programas. Um campo � uma
sequ�ncia de zero ou mais caracteres. Os campos s�o separados por v�rgulas. Os
espa�os em branco no in�cio e no final s�o preservados. Um campo pode estar
inclu�do dentro de caracteres de aspas, caso em que ele pode conter v�rgulas. Um
campo com aspas pode conter caracteres de aspas, o que � representado por um
caractere de aspas; o campo CSV "x" "y" define a string x"y. Os campos podem ser
vazios; um campo especificado como " " � vazio e id�ntico �quele especificado pelas
v�rgulas adjacentes.
Os campos s�o numerados a partir do zero. E se o usu�rio pedir um campo n�o-
existente chamando csvfield(-1) ou csvfield(100000)? Poder�amos retornar " " (a
string vazia), porque ela pode ser impressa e comparada. Os programas que processam
os n�meros de vari�vel dos campos n�o teriam de tomar precau��es especiais para
lidar com os n�meros inexistentes. Mas essa op��o n�o fornece uma maneira de
distinguir o vazio do n�o-existente. Uma segunda op��o seria imprimir uma mensagem
de erro ou mesmo abortar. Vamos discutir em breve o motivo pelo qual isso n�o �
desej�vel. Resolvemos retornar NULL, o valor convencional para uma string n�o-
existente em C.

Ocultamente de informa��es. A biblioteca n�o limitar� o comprimento de entrada de


linha ou o n�mero de campos. Para conseguir isso, quem chama deve fornecer a
mem�ria ou quem � chamado (a biblioteca) deve aloc�-la. Quem chama a fun��o de
biblioteca fgets passa um array e um tamanho m�ximo. Se a linha for mais longa do
que o buffer, ela � dividida em partes. Esse comportamento n�o � satisfat�rio para
a interface CSV; portanto, nossa biblioteca alocar� a mem�ria quando descobrir que
mais mem�ria � necess�ria.
Assim sendo, apenas csvgetline sabe sobre o gerenciamento da mem�ria. Nada
sobre a maneira como ela organiza a mem�ria pode ser acessado de fora. A melhor
maneira de fornecer esse isolamento � usar a interface de fun��o: csvgetline l� a
linha seguinte, independente do seu tamanho, csvfield (n) retorna um ponteiro para
os bytes do campo de n�mero n da linha atual, e csvnfield retorna o n�mero de
campos da linha atual.
Precisaremos aumentar a mem�ria � medida que chegarem linhas mais longas ou
mais campos. Os detalhes de como isso � feito est�o ocultos nas fun��es csv;
nenhuma outra parte do programa sabe como isso funciona, por exemplo, se a
biblioteca usa arrays pequenos que podem ser aumentados, ou arrays muito grandes,
ou algo completamente diferente. A interface tamb�m n�o revela quando a mem�ria �
liberada.
Se o usu�rio chamar apenas csvgetline, n�o h� necessidade de dividi-la em
campos; as linhas podem ser divididas conforme a demanda. Outro detalhe da
implementa��o que � oculto do usu�rio � se a divis�o de campos � "ansiosa" (feita
imediatamente quando a linha est� pronta), "pregui�osa" (feita apenas quando um
campo ou contagem forem necess�rios) ou "muito pregui�osa" (somente o campo
solicitado � dividido).

Gerenciantento de recursos. Devemos resolver quem � respons�vel pelo


compartilhamento das informa��es, csvgetline retorna os dados originais ou faz uma
c�pia? Resolvemos que o valor de retorno de csvgetline � um ponteiro para a entrada
original, o qual � sobreposto quando a pr�xima linha � lida. Os campos ser�o
constru�dos em uma c�pia da linha de entrada, e csvfield retornar� um ponteiro para
o campo dentro da c�pia. Com essa organiza��o, o usu�rio deve fazer outra c�pia
quando uma determinada linha ou um campo devem ser salvos ou alterados, e �
responsabilidade do usu�rio liberar aquele armazenamento quando ele n�o � mais
necess�rio.
Quem abre e fecha o arquivo de entrada? Seja quem for, ele deve fazer o
fechamento correspondente: as tarefas coincidentes devem ser feitas no mesmo n�vel
ou lugar. Vamos assumir que csvgetline � chamada com um ponteiro FILE para um
arquivo j� aberto que quem chamou vai fechar, quando o proces-samento estiver
conclu�do.
O gerenciamento dos recursos compartilhados ou passados pelo limite entre
uma biblioteca e aquele de quem chama � uma tarefa dif�cil, e quase sempre h�
raz�es s�lidas mas conflitantes para preferir as diversas op��es de projeto. Os
erros e mal-entendidos sobre as responsabilidades do compartilha-mento s�o fonte
frequente de bugs.

Tratamento de erros. Como csvgetline retorna NULL, n�o h� uma maneira boa para
distinguir o final do arquivo de um erro como falta de mem�ria. Da mesma forma, o
acesso a um campo n�o-existente n�o causa erro. Por analogia com ferror, poder�amos
adicionar outra fun��o csvgeterror � interface para reportar o erro mais recente,
mas, por quest�es de simplicidade, vamos deix�-la de fora desta vers�o.
Como princ�pio, as rotinas de biblioteca n�o devem simplesmente morrer
quando ocorrer um erro. O status de erro deve ser retornado para quem chama para a
a��o apropriada. As rotinas de biblioteca tamb�m n�o devem imprimir mensagens ou
mostrar caixas de di�logo, uma vez que elas podem estar sendo executadas em um
ambiente onde uma mensagem interferiria em outra coisa. O tratamento de erros � um
t�pico que merece uma discuss�o pr�pria, o que ser� feito mais tarde neste
cap�tulo.

Especifica��o. As op��es feitas acima devem ser reunidas em um lugar como uma
especifica��o dos servi�os fornecidos por csvgetline e de como ela deve ser usada.
Em um projeto grande, a especifica��o precede a implementa��o, porque os
especificadores e implementadores geralmente s�o pessoas diferentes e podem estar
em organiza��es diferentes. Na pr�tica, por�m, o trabalho quase sempre � feito em
paralelo, com a especifica��o e o c�digo evoluindo juntos, embora eventualmente a
"especifica��o" seja escrita apenas depois do fato para descrever aproximadamente o
que o c�digo faz.
A melhor abordagem � escrever a especifica��o no in�cio e revis�-la � medida
que a implementa��o progride. Quanto mais precisa e cuidadosa for uma
especifica��o, maior a chance de que o programa resultante funcione bem. Mesmo nos
programas pessoais, � bom preparar uma especifica��o razo�vel-mente completa,
porque isso encoraja a considera��o das alternativas e registra as op��es feitas.
Para nossos prop�sitos, a especifica��o incluiria os prot�tipos de fun��o e
uma prescri��o detalhada do comportamento, das responsabilidades e suposi-��es:
Os campos s�o separados por v�rgulas.
Um campo pode estar entre caracteres de aspas "...".
Um campo com aspas pode conter v�rgulas, mas n�o pode conter newlines.
Um campo com aspas pode conter caracteres de aspas ", representados por
" ".
Os campos podem estar vazios; " " e uma string vazia representam um campo
vazio.
O espa�o em branco inicial e final � preservado.

char *csvgetline(FILE *f);


l� uma linha do arquivo de entrada aberto f;
assume que as linhas de entrada terminam com \r,\n,\r\n, ou EOF.
retorna o ponteiro para a linha, com o terminador removido, ou NULL se EOF
ocorreu.
a linha pode ter um comprimento arbitr�rio; retorna NULL se o limite de mem�ria �
excedido, a linha deve ser tratada como um armazenamento somente para leitura;
quem chama deve fazer uma c�pia para preservar ou alterar o conte�do.

char *csvfield(int n);


os campos s�o numerados a partir do 0.
retorna o campo de n�mero n da �ltima linha lida por csvgetline; retorna NULL se
n < 0 ou al�m do �ltimo campo.
os campos s�o separados por v�rgulas
os campos podem estar dentro de " ..."; tais aspas s�o removidas; dentro de
"..."," " s�o substitu�das por " e a v�rgula n�o � um separador.
nos campos sem aspas, as aspas s�o caracteres regulares.
pode haver um n�mero arbitr�rio de campos de qualquer comprimento; retorna NULL
se o limite de mem�ria � excedido.
o campo deve ser tratado como um armazenamento somente para leitura; quem
chama deve fazer uma c�pia para preservar ou alterar o conte�do.
comportamento n�o definido se chamado antes de csvgetline ser chamada.

int csvnfield(void);
retorna o n�mero de campos da �ltima linha lida por csvgetline.
comportamento indefinido se chamado antes de csvgetline ser chamada.

Essa especifica��o ainda deixa algumas quest�es em aberto. Por exemplo,


quais valores devem ser retornados por csvfield e csvnfield quando elas s�o
chamadas depois de csvgetline ter encontrado EOF? Como os campos mal formados devem
ser tratados? � dif�cil solucionar tudo isso mesmo em um sistema pequeno, e muito
dif�cil em um sistema grande, mas � importante tentar. Quase sempre as distra��es e
omiss�es n�o s�o descobertas antes que a implementa��o esteja em andamento.

O restante desta se��o cont�m uma implementa��o nova de csvgetline que


coincide com a especifica��o. A biblioteca est� dividida em dois arquivos, um
header csv.h que cont�m as declara��es de fun��o que representam a parte p�blica da
interface, e um arquivo de implementa��o csv.c que cont�m o c�digo. Os usu�rios
incluem csv.h em seu c�digo-fonte e vinculam seu c�digo compilado � vers�o
compilada de csv.c; a fonte n�o precisa estar vis�vel nunca.
Aqui temos o arquivo header:

/* csv.h: interface da biblioteca csv */


extern char *csvgetline(FILE *f); /* l� pr�xima linha de entrada */
extern char *csvfield(int n); /* retorna campo n */
extern int csvnfield(void); /* retorna o n�mero de campos */

As vari�veis internas que armazenam o texto e as fun��es internas como split


s�o declaradas static para que sejam vis�veis apenas dentro do arquivo que as
cont�m. Essa � a maneira mais simples de ocultar as informa��es em um programa em
C.

enum { NOMEM = -2 }; /* sem sinal de mem�ria */


static char *line = NULL; /* chars de entrada */
static char *sline = NULL; /* c�pia da linha usada por split */
static int maxline = 0; /* tamanho de line[] e sline[] */
static char **field = NULL; /* ponteiros de campo */
static int maxfield = 0; /* tamanho de field[] */
static int nfield = 0; /* n�mero de campos em field[] */

static char fieldsep[]= ","; /* chars separadores de campo */

As vari�veis tamb�m s�o inicializadas estaticamente. Esses valores iniciais


s�o usados para testar se os arrays ser�o criados ou aumentados.
Essas declara��es descrevem uma estrutura de dados simples. O array line
cont�m a linha de entrada; o array sline � criado copiando os caracteres de line e
encerrando cada campo. O array field indica as entradas de sline. Este diagrama
mostra o estado desses tr�s arrays depois que as linhas de entrada ab, "cd",
"e""f",,"g,h" foram processadas. Os elementos sombreados em sline n�o fazem parte
de nenhum campo.

Aqui temos a pr�pria fun��o csvgetline:

/* csvgetline: obt�m uma linha, aumenta conforme a necessidade */


/* entrada de exemplo: "LU",86.25,"11/4/1998","2:19PM",+4.0625 */
char *csvgetline(FILE *fin)
{
int i, c;
char *newl, *news;

if (line == NULL) { /* aloca na primeira chamada */


maxline = maxfield = 1;
line = (char *) malloc(maxline);
sline = (char *) malloc(maxline);
field = (char **) malloc(maxfield*sizeof(field[0]));
if (line == NULL || sline == NULL || field == NULL) {
reset();
return NULL; /* sem mem�ria */
}
}
for (i = 0; (c=getc(fin))!= EOF && !endofline(fin.c); i++) {
if (i >= maxline-1) { /* aumentar linha */
maxline *= 2; /* dobrar o tamanho atual */
newl = (char *) realloc(line, maxline);
news = (char *) realloc(sline, maxline);
if (newl == NULL || news == NULL) {
reset();
return NULL; /* sem mem�ria */
}
line = newl;
sline = news;
}
line[i] = c;
}
line[i] = '\0';
if (split() == NOMEM) {
reset();
return NULL; /* sem mem�ria */
}
return (c == EOF && i == 0) ? NULL : line;
}

Uma linha recebida � acumulada em line, a qual � aumentada conforme a necessidade


por uma chamada de realloc; o tamanho � dobrado a cada aumen-to, como na Se��o 2.6.
O array sline � mantido com o mesmo tamanho de line; csvgetline chama split para
criar os ponteiros de campo de um field de array separado, o qual tamb�m �
aumentado conforme a necessidade.
Como estamos personalizando, come�amos o array muito pequeno e o aumentamos
conforme a demanda, para garantir que o c�digo de aumento de array seja exercido.
Se a aloca��o falhar, chamamos reset para restaurar os globais ao seu estado
inicial, de modo que uma chamada subsequente a csvgetline tenha chances de sucesso:

/* reset: define as vari�veis de volta aos valores iniciais */


static void reset(void)
{
free(line); /* free(NULL) permitido pela ANSI C */
free(sline);
free(field);
line = NULL;
sline = NULL;
field = NULL;
maxline = maxfield = nfield = 0;
}

A fun��o endofline trata do problema de uma linha de entrada ser encer-rada


com um retorno de carro, uma newline, ambos, ou mesmo com um EOF:

/* endofline: verificar e consumir \r, \n, \r\n, or EOF */


static int endofline(FILE *fin, int c)
{
int eol;

eol = (c=='\r' || c=='\n');


if (c == '\r') {
c = getc(fin);
if (c != '\n' && c != EOF)
ungetc(c, fin); /* ler bem adiante, colocar c de volta */
}
return eol;
}

Uma fun��o separada � necess�ria, uma vez que as fun��es padr�o de entrada
n�o lidam com a rica variedade de formatos teimosos encontrados nas entradas reais.
Nosso prot�tipo usou strtok para encontrar o pr�ximo token pesquisando um
caractere separador, normalmente uma v�rgula, mas isso impossibilitou o tratamento
das v�rgulas com aspas. Uma grande altera��o na implementa��o de split se faz
necess�ria, embora sua interface n�o precise mudar. Pense nestas linhas de entrada:

"",,""
,"",
,,

Cada linha tem tr�s campos vazios. Verificar se split os analisa e �s ou-
tras entradas corretamente � algo que complica as coisas de forma significativa, um
exemplo de como os casos especiais e as condi��es-limite podem dominar um programa.
/* split: divide a linha em campos */
static int split(void)
{
char *p, **newf;
char *sepp; /* ponteiro para caractere separador tempor�rio */
int sepc; /* caractere separador tempor�rio */

nfield = 0;
if (line[0] == '\0')
return 0;
strcpy(sline, line);
p = sline;

do {
if (nfield >= maxfield) {
maxfield *= 2; /* dobra o tamanho atual */
newf = (char **) realloc(field,
maxfield * sizeof(field[0]));
if (newf == NULL)
return NOMEM;
field = newf;
}
if (*p == '"')
sepp = advquoted(++p); /* saltar aspas iniciais */
else
sepp = p + strcspn(p, fieldsep);
sepc = sepp[0];
sepp[0] = '\0'; /* encerrar campo */
field[nfield++] = p;
p = sepp + 1;
} while (sepc == ',');

return nfield;
}

O loop aumenta o array dos ponteiros de campo se for preciso, e depois chama uma
das duas fun��es para localizar e processar o pr�ximo campo. Se o campo come�ar com
aspas, advquoted encontra o campo e retorna um ponteiro para o separador que
encerra o campo. Caso contr�rio, para localizar a pr�xima v�rgula usamos a fun��o
de biblioteca strcspn(p,s), que pesquisa em uma string p a pr�xima ocorr�ncia de
qualquer caractere da string s; ela retorna o n�mero de caracteres que foi saltado.
As aspas dentro de um campo s�o representadas por duas aspas adjacentes, de
modo que advquoted as espreme em uma �nica; ela tamb�m remove as aspas que cercam o
campo. Alguma complexidade � adicionada por uma tentativa de lidar com entradas
plaus�veis que n�o coincidem com a especifica��o, tais como "abc"def. Em tais
casos, anexamos o que vier depois das segundas aspas at� o pr�ximo separador como
parte desse campo. O Microsoft Excel parece usar um algoritmo semelhante.

/* advquoted: campo com aspas; retornar ponteiro para o pr�ximo


separador */
static char *advquoted(char *p)
{
int i, j;

for (i = j = 0; p[j] != '\0'; i++, j++) {


if (p[j] == '"' && p[++j] != '"') {
/* copiar at� o pr�ximo separador ou \0 */
int k = strcspn(p+j, fieldsep);
memmove(p+i, p+j, k);
i += k;
j += k;
break;
}
p[i] = p[j];
}
p[i] = '\0';
return p+j;
}

Como a linha de entrada j� est� dividida, csvfield e csvnfield s�o triviais:

/* csvfield: retorna ponteiro para o n� campo */


char *csvfield(int n)
{
if (n < 0 || n >= nfield)
return NULL;
return field [n];
}

/* csvnfield: retorna o n�mero de campos */


int csvnfield(void)
{
return nfield;
}

Finalmente, podemos modificar o driver de teste para exercitar essa vers�o


da biblioteca. Como ele mant�m uma c�pia da linha de entrada, o que o prot�tipo n�o
faz, ele pode imprimir a linha original antes de imprimir os campos:

/* csvtest main: testa a biblioteca CSV */


int main(void)
{
int i; char *line;

while ((line = csvgetline(stdin)) != NULL) {


printf("line = '%s'\n", line);
for (i = 0; i < csvnfield(); i++)
printf("field[%d] = '%s'\n", i, csvfield(i));
}
return 0;
}

Isso conclui nossa vers�o em C. Ela lida arbitrariamente com entradas


grandes e faz algo sens�vel at� mesmo com os dados mais teimosos. O pre�o � que ela
� mais do que quatro vezes maior do que o primeiro prot�tipo e parte do c�digo �
complicada. Tal expans�o no tamanho e complexidade � um resultado t�pico da
passagem do prot�tipo para a produ��o.

� Exerc�cio 4-1. H� v�rios graus de "pregui�a" na divis�o de campos. Entre as


possibilidades est� dividir tudo de uma vez, mas somente quando alguns campos
s�o solicitados, dividir apenas o campo solicitado ou dividir at� o campo
solicitado. Enumere as possibilidades, avalie a dificuldade e os benef�cios em
potencial e depois escreva-os e me�a suas velocidades. ?

� Exerc�cio 4-2. Adicione um recurso para que os separadores sejam alterados (a)
para uma classe arbitr�ria de caracteres; (b) para separadores diferentes para
campos diferentes; (c) para uma express�o regular (consulte o Cap�tulo 9). Como
deve ser a interface? ?

� Exerc�cio 4-3. Preferimos usar a inicializa��o est�tica fornecida pela C como a


base de um deslocamento feito de uma s� vez: quando um ponteiro � NULL na entrada,
a inicializa��o � realizada. Outra possibilidade � exigir que o usu�rio chame uma
fun��o expl�cita de inicializa��o, a qual pode incluir os tamanhos iniciais
sugeridos para os arrays. Implemente uma vers�o que combina o melhor de ambas. Qual
� o papel de reset em sua implementa��o? ?

� Exerc�cio 4-4. Projete e implemente uma biblioteca para criar dados formatados
como CSV A vers�o mais simples pode tomar um array de strings e imprimi-los com
aspas e v�rgulas. Uma vers�o mais sofisticada usaria uma string de formato
semelhante a printf. O Cap�tulo 9 traz algumas sugest�es de nota��o. ?

4.4 Uma implementa��o em C++

Nesta se��o, vamos escrever uma vers�o em C++ da biblioteca CSV para abordar
algumas das limita��es restantes da vers�o em C. Isso exigir� algumas altera��es na
especifica��o, das quais a mais importante � que as fun��es lidar�o com as strings
da C++ em vez dos arrays de caracteres da C. O uso das strings da C++ resolver�
automaticamente algumas das quest�es de gerenciamento do armazenamento, uma vez que
as fun��es de biblioteca gerenciar�o a mem�ria para n�s. Em particular, as rotinas
de campo retornar�o strings que podem ser modificadas por quem chama, um projeto
mais flex�vel do que as vers�es anteriores.
Uma classe csv define a face p�blica, enquanto oculta as vari�veis e fun��es
da implementa��o. Como um objeto de classe cont�m todo o estado de uma inst�ncia,
podemos instanciar diversas vari�veis csv; cada uma � independente das outras, de
modo que v�rios fluxos de entrada CSV podem operar ao mesmo tempo.

class Csv { // l� e analisa os valores separados por v�rgulas


// entrada de exemplo: "LU",86.25,"11/4/1998","2:19PM",+4.0625

public:
Csv(istream& fin = cin, string sep = ",") :
fin(fin), fieldsep(sep) {}

int getline(string&);
string getfield(int n);
int getnfield() const { return nfield; }

private:
istream& fin; // ponteiro para o arquivo de entrada
string line; // linha de entrada
vector<string> field; // strings de campo
int nfield; // n�mero de campos
string fieldsep; // caracteres separadores
int split();
int endofline(char);
int advplain(const string& line, string& fld, int);
int advquoted(const string& line, string& fld, int);
};

Os par�metros padr�o do construtor s�o definidos para que um objeto csv padr�o seja
lido a partir do fluxo padr�o de entrada e use o separador de campo normal.
Qualquer um dos dois pode ser substitu�do por valores expl�citos.
Para gerenciar as strings, a classe usa as classes string e vector padr�o da
C++, em vez das strings no estilo da C. N�o h� estado n�o-existente para uma
string: "empty" significa apenas que o comprimento � zero, e n�o h� equivalente
para NULL, portanto n�s n�o podemos us�-lo como um sinal de final de arquivo. Assim
sendo, Csv::getline retorna a linha de entrada por meio de um argumento por
refer�ncia, reservando o pr�prio valor de fun��o para o final de arquivo e
relat�rios de erro.

// getline: obt�m uma linha, aumenta conforme a necessidade


int Csv::getline(string& str)
{
char c;

for (line = ""; fin.get(c) && !endofline(c); )


line += c;
split();
str = line;
return !fin.eof();
}

O operador += � sobrecarregado para anexar um caractere a uma string.


Pequenas altera��es s�o necess�rias em endofline. Novamente, temos de ler a
entrada um caractere de cada vez, pois nenhuma das rotinas de entrada padr�o pode
lidar com a variedade de entradas.

// endofline: verifica e consome \r, \n, \r\n, or EOF


int Csv::endofline(char c)
{
int eol;

eol = (c=='\r' || c=='\n');


if (c == '\r')
{
fin.get(c);
if (!fin.eof() && c != '\n')
fin.putback(c); // read too far
}
return eol;
}

Aqui temos a vers�o nova de split:

// split: divide as linhas em campos


int Csv::split()
{
string fld;
int i, j;

nfield = 0;
if (line.length() == 0)
return 0;
i = 0;

do {
if (i < line.length() && line[i] == "")
j = advquoted(line, fld, ++i); // saltar aspas
else
j = advplain(line, fld, i);
if (nfield >= field.size())
field.push_back(fld);
else
field[nfield] = fld;
nfield++;
i = j + 1;
} while (j < line.length());

return nfield;
}

Como strcspn n�o funciona nas strings da C++, n�s devemos alterar tanto
split quanto advquoted. A nova vers�o de advquoted usa a fun��o padr�o da C++
find_first_of para localizar a pr�xima ocorr�ncia de um caractere de separador. A
chamada s.find_first_of(fieldsep,j) pesquisa na string s a primeira inst�ncia de
qualquer caractere em fieldsep que ocorre em ou ap�s a posi��o j. Se ela n�o
encontrar uma inst�ncia, retorna um �ndice al�m do final da string, de modo que
devemos coloc�-la de novo dentro do intervalo. O loop interno for que se segue
anexa os caracteres at� o separador para o campo que est� sendo acumulado em fld.

// advquoted: campo com aspas;


// retorna o �ndice do pr�ximo separador
int Csv::advquoted(const string& s, string& fld, int i)
{
int j;

fld = "";
for (j = i; j < s.length(); j++) {
if (s[j] == "" && s[++j] != "") {
int k = s.find_first_of(fieldsep, j);
if (k > s.length()) // nenhum separador encontrado
k = s.length();
for (k -= j; k-- > 0; )
fld += s[j++];
break;
}
fld += s[j];
}
return j;
}

A fun��o find_first_of tamb�m � usada em uma fun��o nova advplain, a qual


avan�a sobre um campo simples sem aspas. Novamente, essa altera��o � necess�ria
porque as fun��es string da C como strcspn n�o podem ser aplicadas �s strings da C+
+, as quais s�o tipos de dados totalmente diferentes.

// advplain: campo sem aspas; retorna o �ndice do pr�ximo separador


int Csv::advplain(const string& s, string& fld, int i)
{
int j;

j = s.find_first_of(fieldsep, i); // procura o separador


if (j > s.length()) // nenhum encontrado
j = s.length();
fld = string(s, i,j-i);
return j;
}

Como antes, Csv::getfield � comum, enquanto que Csv::getnfield � t�o urto


que � implementado na defini��o da classe.

// getfield: retorna o campo n�


string Csv::getfield(int n)
{
if (n < O || n >= nfield)
return "";
else
return field[n];
}

Nosso programa de teste � uma variante simples daquela anterior:

// Csvtest main: testa a classe Csv


int main(void)
{
string line;
Csv csv;

while (csv.getline(line) != 0) {
cout " "line = '" " line " "'\n";
for (int i = 0; i < csv.getnfield(); i++)
cout " "field[" " i " "] = "' " csv.getfield(i) " '"\n";
}
return 0;
}

O uso � diferente daquele da vers�o em C, embora somente at� certo ponto.


Dependendo do compilador, a vers�o em C++� cerca de 40% a quatro vezes mais lenta
do que a vers�o em C em um arquivo de entrada grande com 30.000 linhas e cerca de
25 campos por linha. Como vimos ao comparar as vers�es da markov, essa varia��o �
um reflexo da maturidade da biblioteca. O programa-fonte em C++ � cerca de 20% mais
curto.

Exerc�cio 4-5. Aperfei�oe a implementa��o em C++ para sobrecarregar o subscripting


com operator[] para que os campos possam ser acessados como csv[i]. D

Exerc�cio 4-6. Escreva uma vers�o em Java da biblioteca CSV, depois compare as tr�s
implementa��es quanto � clareza, robustez e velocidade. D

Exerc�cio 4-7. Fa�a novamente o package da vers�o em C++ do c�digo CSV como um STL
iterator. D

Exerc�cio 4-8. A vers�o em C++ permite que v�rias inst�ncias independentes csv
operem simultaneamente sem interfer�ncia, um benef�cio do encapsulamento de todo o
estado em um objeto que pode ser instanciado v�rias vezes. Modifique a vers�o em C
para conseguir o mesmo efeito substituindo as estruturas de dados globais pelas
estruturas que s�o alocadas e inicializadas por uma fun��o csvnew expl�cita. D

4.5 Princ�pios da interface

Nas se��es anteriores vimos os detalhes de uma interface, que � o limite


detalhado entre o c�digo que fornece um servi�o e o c�digo que o usa. Uma interface
define aquilo que alguma parte do c�digo faz para seus usu�rios, como as fun��es e
talvez os membros de dados podem ser usados pelo restante do programa. Nossa
interface CSV fornece tr�s fun��es - l� uma linha, obt�m um campo e retorna o
n�mero de campos - que s�o as �nicas opera��es que podem ser executadas.
Para prosperar, uma interface deve estar bem adaptada � sua tarefa -
simples, geral, regular, previs�vel, robusta - e deve se adaptar bem aos seus
usu�rios e �s altera��es nas suas implementa��es. As boas interfaces seguem um
conjunto de princ�pios. Estes n�o s�o independentes, nem mesmo consistentes, mas
eles nos ajudam a descrever o que acontece na fronteira entre dois softwares.

Ocultar os detalhes da implementa��o. A implementa��o por tr�s da interface deve


ser oculta do restante do programa, para que ele possa ser alterado sem afetar ou
quebrar nada. Existem diversos termos para esse tipo de princ�pio de organiza��o:
ocultamento de informa��es, encapsulamento, abstra��o, modula-riza��o e outros que
se referem todos �s ideias relacionadas. Uma interface deve ocultar os detalhes da
implementa��o que s�o irrelevantes para o cliente (usu�rio) da interface. Os
detalhes que s�o invis�veis podem ser alterados sem afetar o cliente, talvez para
estender a interface, torn�-la mais eficiente ou mesmo substituir toda a sua
implementa��o.
As bibliotecas b�sicas da maioria das linguagens de programa��o forne-cem
exemplos conhecidos, embora nem sempre eles sejam bem projetados. A biblioteca
padr�o de E/S da C est� entre as mais conhecidas: algumas dezenas de fun��es que
abrem, fecham, l�em, gravam e manipulam os arquivos de outras formas. A
implementa��o da E/S de arquivo � oculta por tr�s de um tipo de dados FILE*, cujas
propriedades podem ser vistas (porque quase sempre elas s�o declaradas em
<stdio.h>), mas n�o se deve explor�-las.
Se o arquivo header n�o incluir a declara��o real de estrutura mas apenas o
nome da estrutura, isso tamb�m � chamado de tipo opaco, uma vez que suas
propriedades n�o s�o vis�veis e todas as opera��es ocorrem por meio de um ponteiro
para qualquer objeto real que esteja se escondendo.
Evite as vari�veis globais. Sempre que poss�vel � melhor passar as
refer�ncias para todos os dados por meio dos argumentos de fun��o.
N�o recomendamos os dados publicamente vis�veis em todos os formul�rios;
fica muito dif�cil manter a consist�ncia dos valores quando os usu�rios podem
alterar as vari�veis � vontade. As interfaces de fun��o facilitam a implanta��o das
regras de acesso, mas esse princ�pio quase sempre � violado. Os fluxos de E/O como
stdin e stdout quase sempre s�o definidos como elementos de um array global de
estruturas FILE:

extern FILE __iob[_NFILE];


#define stdin (&__iob[0])
#define stdout (&__iob[l])
#define stderr (&__iob[2])

Isso torna a implementa��o totalmente vis�vel. Isso tamb�m significa que �


poss�vel atribuir stdin, stdout ou stderr, muito embora elas se pare�am com
vari�veis. O nome peculiar __iob usa a conven��o ANSI C de dois caracteres de
sublinhado na frente para nomes privados que devem ser vis�veis, o que faz com que
os nomes tenham menos chances de causar conflitos em um programa.
As classes da C++ e Java s�o mecanismos melhores para ocultar as
informa��es; elas s�o importantes para o uso adequado daquelas linguagens. As
classes de cont�iner da C++ Standard Template Library que usamos no Cap�tulo 3
levam isso ainda mais adiante: al�m de algumas garantias de desempenho n�o h�
informa��es sobre a implementa��o e os criadores da biblioteca podem usar qualquer
mecanismo que quiserem.

Selecionar um conjunto ortogonal pequeno de primitivos. Uma interface deve fornecer


o m�ximo de funcionalidade necess�ria, mas nada al�m disso, e as fun��es n�o devem
se sobrepor excessivamente em suas capacidades. Muitas fun��es podem tornar a
biblioteca mais f�cil de usar - tudo de que algu�m precisa esta l� para ser usado.
Mas uma interface grande � mais dif�cil de escrever e atualizar, e um tamanho muito
grande tamb�m pode torn�-la dif�cil de aprender e usar. As "interfaces de programas
aplicativos", ou APIs, eventualmente s�o t�o imensas que n�o se espera que nenhum
mortal possa domin�-las.
Em nome da conveni�ncia, algumas interfaces fornecem v�rias maneiras de
fazer a mesma coisa, uma tend�ncia � qual devemos resistir. A biblioteca de E/S
padr�o da C fornece pelo menos quatro fun��es diferentes que gravar�o um �nico
caractere em um fluxo de sa�da:

char c;
putc(c, fp);
fputc(c, fp);
fprintf(fp, "%c", c);
fwrite(&c, sizeof(char), 1, fp);

Se o fluxo for stdout, existem v�rias outras possibilidades. Elas s�o convenien-
tes, mas nem todas s�o necess�rias.
As interfaces menores devem ser usadas em vez das grandes, pelo menos at�
que se tenham fortes evid�ncias da necessidade de mais fun��es. Fa�a uma coisa, e
fa�a-a bem. N�o aumente uma interface s� porque � poss�vel fazer isso, e n�o
conserte a interface quando os problemas est�o na implementa��o. Por exemplo, em
vez de ter memcpy por quest�es de velocidade e memmove por quest�es de seguran�a,
seria melhor ter uma fun��o que sempre foi segura e que � r�pida quando pode ser.

N�o saia do alcance do usu�rio. Uma fun��o de biblioteca n�o deve escrever arquivos
e vari�veis secretos ou alterar os dados globais, e deve ser circunspecta quanto �
modifica��o dos dados em seu chamador. A fun��o strtok falha em v�rios desses
crit�rios. � um pouco surpreendente que strtok escreva bytes null no meio de sua
string de entrada. Seu uso do ponteiro null como um sinal para retomar de onde
parou implica dados secretos mantidos entre as chamadas, uma fonte prov�vel de
bugs, e exclui os usos simult�neos da fun��o. Um projeto melhor seria fornecer uma
�nica fun��o que faz o token de uma string de entrada. Por quest�es semelhantes,
nossa segunda vers�o da C n�o pode ser usada para dois fluxos de entrada. Consulte
o Exerc�cio 4-8.
O uso de uma interface n�o deve exigir outra apenas para ser conveniente
para o criador ou implementador da interface. Em vez disso, fa�a a interface
autocontida, ou na sua falta, seja expl�cito sobre quais servi�os externos s�o
requeridos. Caso contr�rio, a carga da manuten��o fica por conta do cliente. Um
exemplo �bvio � o aborrecimento de gerenciar listas enormes de arquivos header na
fonte em C e C++; os arquivos header podem ter milhares de linhas de comprimento e
incluir dezenas de outros headers.

Fa�a a mesma coisa igual em todos os lugares. A consist�ncia e a regularidade s�o


importantes. Coisas relacionadas devem ser realizadas por meios relacio-nados.
As fun��es str... b�sicas da biblioteca C s�o f�ceis de usar sem
documenta��o, porque elas todas se comportam mais ou menos igual: os dados fluem da
direita para a esquerda, na mesma dire��o que em uma declara��o de atribui��o, e
todas retornam a string resultante. Por outro lado, na biblioteca C Standard I/O
fica dif�cil prever a ordem dos argumentos para as fun��es. Algumas t�m o argumento
FILE* primeiro, outras por �ltimo. Outras t�m diversas ordens para tamanho e n�mero
de elementos. Os algoritmos para os cont�ineres STL apresentam uma interface
bastante uniforme, portanto � f�cil prever como usar uma fun��o desconhecida.
A consist�ncia externa, comportando-se como alguma outra coisa, tam-b�m � um
objetivo. Por exemplo, as fun��es mem... foram criadas depois das fun��es str... na
C, mas pediram emprestado seu estilo. As fun��es de E/S padr�o fread e fwrite
seriam mais f�ceis de lembrar se fossem parecidas com as fun��es read e write nas
quais se basearam. As op��es da linha de comandos do Unix s�o introduzidas com um
sinal de menos, mas determinada letra de op��o pode significar coisas completamente
diferentes, mesmo entre programas relacionados.
Se os caracteres curingas, como o * de *.exe, forem todos expandidos por um
int�rprete de comandos, o comportamento � uniforme. Se eles forem expandidos por
programas individuais, provavelmente o comportamento ser� uniforme. Os browsers da
Web usam um �nico clique do mouse para seguir um link, mas outros aplicativos usam
dois cliques para iniciar um programa e seguir um link. O resultado � que muitas
pessoas clicam automaticamente duas vezes.
Esses princ�pios s�o mais f�ceis de seguir em alguns ambientes do que em
outros, mas eles continuam valendo para ambos. Por exemplo, � dif�cil ocultar os
detalhes de implementa��o na C, mas um bom programador n�o os explora, porque isso
torna os detalhes parte da interface e viola o princ�pio do oculta-mento de
informa��es. Coment�rios nos arquivos header, nomes com formas especiais (tais
como__iob) e outros s�o maneiras de incentivar o bom comporta-mento quando ele n�o
for implantado.
N�o h� limite para aquilo que podemos fazer para criar uma boa interface.
Mesmo as melhores interfaces de hoje podem eventualmente se tornar os proble-mas de
amanh�, mas o bom projeto pode fazer com que o amanh� demore um pouco mais para
chegar.

4.6 Gerenciamento de recursos

Um dos problemas mais dif�ceis da cria��o da interface de uma biblioteca (ou


de uma classe ou de um pacote) � gerenciar os recursos que s�o propriedade da
biblioteca ou que s�o compartilhados pela biblioteca e por aqueles que a chamam. O
recurso mais �bvio deles � a mem�ria - quem � respons�vel por alocar e liberar o
armazenamento? - mas outros recursos compartilhados exibem os arquivos abertos e o
estado das vari�veis cujos valores s�o de interesse co-mum. De forma geral, as
quest�es se classificam nas categorias da inicializa��o, manuten��o do estado,
compartilhamento, c�pia e limpeza.
O prot�tipo de nosso pacote CSV usou a inicializa��o est�tica para definir
os valores iniciais de ponteiros, contadores e outros. Mas essa op��o � limitante,
uma vez que ela evita a reinicializa��o das rotinas a seu estado inicial depois que
uma das fun��es foi chamada. Uma alternativa � fornecer uma fun��o de inicializa��o
que define todos os valores internos com os valores iniciais corretos. Isso permite
reinicializar, mas depende do usu�rio para chamar a inicializa��o explicitamente. A
fun��o reset da segunda vers�o poderia ser tornada p�blica para esse fim.
Em C++ e Java, os construtores s�o usados para inicializar os membros de
dados das classes. Construtores definidos adequadamente garantem que todos os
membros de dados sejam inicializados e que n�o haja como criar um objeto de classe
n�o-inicializada. Um grupo de construtores pode suportar diversos tipos de
inicializadores. N�s podemos fornecer csv com um construtor que tome um nome de
arquivo e outro que tome um fluxo de entrada.
E quanto �s c�pias das informa��es gerenciadas por uma biblioteca, tal como
as linhas de entrada e os campos? Nosso programa csvgetline em C fornece acesso
direto �s strings de entrada (linhas e campos), retornando ponteiros para elas.
Esse acesso n�o-restrito tem diversas desvantagens. � poss�vel para o usu�rio
sobrepor mem�ria de modo a tornar as outras informa��es inv�lidas. Por exemplo, uma
express�o como

strcpy(csvfield(1), csvfield(2));

falharia de v�rias maneiras, principalmente sobrepondo o in�cio do campo 2 se ele


fosse mais longo do que o campo 1. O usu�rio da biblioteca deve fazer uma c�pia de
todas as informa��es a serem preservadas al�m da pr�xima chamada de csvgetline; na
sequ�ncia abaixo, o ponteiro seria inv�lido no final se a segunda csvgetline
causasse uma realoca��o de seu buffer de linha.

char *p;

csvgetline(fin);
p = csvfield(1);
csvgetline(fin); /* p poderia ser invalido aqui */

A vers�o em C++ � mais segura porque as strings s�o c�pias que podem ser alteradas
� vontade.
A Java usa refer�ncias para se referir aos objetos, ou seja, toda entidade
diferente dos tipos b�sicos como int. Isso � mais eficiente do que fazer uma c�pia,
mas � poss�vel se enganar e achar que uma refer�ncia � uma c�pia; tive-mos um bug
desse tipo numa primeira vers�o de nosso programa markov em Java, e essa quest�o �
fonte constante de bugs que envolvem strings em C. Os m�todos clones fornecem uma
maneira de fazer uma c�pia quando for necess�rio.
O outro lado da inicializa��o de nossa constru��o � a finaliza��o ou
destrui��o - limpar e recuperar recursos quando alguma entidade n�o � mais
necess�ria. Isso � particularmente importante para a mem�ria, uma vez que um
programa que falha na recupera��o de mem�ria n�o utilizada eventualmente n�o ser�
executado. O software mais moderno tem uma embara�osa tend�ncia a apresentar essa
falha. Problemas relacionados ocorrem quando os arquivos abertos devem ser
fechados: se os dados est�o em buffer, este pode ter de ser esvaziado (e sua
mem�ria ter de ser retomada). Para as fun��es de biblioteca da C padr�o, o
"esvaziamento" acontece automaticamente quando o programa � encerrado normalmente,
mas de outra forma ele deve ser programado. A fun��o padr�o da C e C++ atexit
fornece uma maneira de obter o controle imedia-tamente antes do encerramento normal
de um programa; os implementadores de interface podem usar esse recurso para
programar a limpeza.

Liberar um recurso na mesma camada em que foi alocado. Uma maneira de controlar a
aloca��o e retomada de recursos � fazer com que a mesma biblioteca, pacote, ou
interface que aloca um recurso seja respons�vel pela sua libera��o. Outra maneira
de dizer isso � que o estado de aloca��o de um recurso n�o deve ser alterado em
toda a interface. Nossas bibliotecas CSV l�em dados de arquivos que j� foram
abertos, de modo que elas os deixam abertos quando terminam. Aquele que chama a
biblioteca precisa fechar os arquivos.
Os construtores e desconstrutores da C++ ajudam a implantar essa regra.
Quando uma inst�ncia de classe sai do escopo ou � explicitamente destru�da, o
destrutor � chamado. Ele pode esvaziar os buffers, recuperar mem�ria, redefinir
valores e fazer tudo o mais que for necess�rio. A Java n�o fornece um mecanismo
equivalente. Embora seja poss�vel definir um m�todo de finaliza��o para uma classe,
n�o h� garantia de que ele ser� executado, muito menos em determinada hora,
portanto as a��es de limpeza n�o podem ter ocorr�ncia garantida, embora quase
sempre seja razo�vel supor que elas ocorrer�o.
A Java fornece ajuda consider�vel com o gerenciamento da mem�ria porque ela
tem a coleta de lixo incorporada. � medida que um programa � executado, ele aloca
objetos novos. N�o h� como desaloc�-los explicitamente, mas o sistema do tempo de
execu��o controla quais objetos ainda est�o em uso e quais n�o est�o, e retorna
periodicamente aqueles n�o-utilizados para o pool de mem�ria dispon�vel. H� uma
variedade de t�cnicas para se fazer a coleta de lixo. Alguns esquemas controlam o
n�mero de usos de cada objeto, sua contagem de refer�ncia, e liberam um objeto
quando sua contagem de refer�ncia chega a zero. Essa t�cnica pode ser usada
explicitamente em C e C++ para gerenciar os objetos compartilhados. Os outros
algoritmos seguem periodicamente uma trilha a partir do pool de aloca��o para todos
os objetos referenciados. Os objetos que s�o encontrados dessa forma ainda est�o em
uso; os objetos que n�o s�o referidos por outro objeto n�o est�o em uso e podem ser
retomados.
A exist�ncia da coleta autom�tica de lixo n�o significa que n�o haja
quest�es de gerenciamento da mem�ria em um projeto. Ainda temos de deter-minar se
as interfaces retornam refer�ncias para os objetos compartilhados ou c�pias deles,
e isso afeta todo o programa. A coleta de lixo n�o � gr�tis - h� um custo para
manter as informa��es e retomar a mem�ria n�o-utilizada, e a coleta pode ocorrer em
momentos imprevis�veis.
Todos esses problemas tornam-se mais complicados quando uma biblioteca deve
ser usada em um ambiente onde mais de um encadeamento de controle pode estar
executando suas rotinas ao mesmo tempo, como em um programa Java multiencadeado.
Para evitar problemas, � preciso escrever c�digo reentrante, o que significa
que ele funciona independente do n�mero de execu��es simult�neas. O c�digo
reentrante evita as vari�veis globais, vari�veis locais est�ticas e todas as outras
vari�veis que poderiam ser modificadas enquanto outro encadeamento as estivesse
usando. O segredo do bom projeto de multiencadeamento � separar os componentes de
modo que eles compartilhem nada, exceto por meio de interfaces bem definidas. As
bibliotecas que inadvertidamente exp�em as vari�-veis ao compartilhamento destroem
o modelo. (Em um programa multien-cadeado, strtok � um desastre, assim como para as
outras fun��es da biblioteca C que armazenam valores na mem�ria est�tica interna.)
Se as vari�veis pudes-sem ser compartilhadas, elas deveriam estar protegidas por
algum tipo de mecanismo de bloqueio para garantir que apenas uma thread de cada vez
as acessasse. As classes s�o uma grande ajuda aqui porque elas fornecem um foco
para discutir o compartilhamento e bloqueio de modelos. Os m�todos sincroni-zados
em Java fornecem uma maneira de um encadeamento bloquear toda uma classe ou
inst�ncia de uma classe contra a modifica��o simult�nea feita por algum outro
encadeamento; os blocos sincronizados permitem que apenas um encadeamento de cada
vez execute uma se��o do c�digo.
O multiencadeamento aumenta a complexidade das quest�es de programa��o, al�m
de ser um t�pico grande demais para ser discutido com detalhes aqui.

4.7 Abort, Retry, Fail?

Nos cap�tulos anteriores, usamos fun��es como eprintf e estrdup para lidar
com os erros exibindo uma mensagem antes de encerrar a execu��o. Por exemplo,
eprintf se comporta como fprintf (stderr,...), mas sai do programa com um status de
erro depois de report�-lo. Ele usa o header <stdarg.h> e a rotina de biblioteca
vfprintf para imprimir os argumentos representados pelo ... do prot�tipo. A
biblioteca stdarg deve ser inicializada por uma chamada para va_start e encerrada
por va_end. Vamos usar mais dessa interface no Cap�tulo 9.

#include <stdarg.h>
#include <string,h>
#include <errno.h>

/* eprintf: imprime mensagem de erro e sai */


void eprintf(char *fmt, ...)
{
va_list args;

fflush(stdout);
if (progname() != NULL)
fprintf(stderr, "%s: ", progname());

va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);

if (fmt[0] != '\0' && fmt[strlen(fmt)-1] == ':')


fprintf(stderr, " %s", strerror(errno));
fprintf(stderr, "\n");
exit(2); /* valor convencional para execu��o falha */
}

Se o argumento de formato terminar com dois pontos, eprintf chama a fun��o padr�o
da C strerror, a qual retorna uma string contendo todas as informa��es adicionais
de erro do sistema que possam estar dispon�veis. Tamb�m escrevemos weprintf,
semelhante a eprintf, que exibe um aviso mas n�o sai. A interface do tipo printf �
conveniente para construir strings que podem ser impressas ou exibidas em uma caixa
de di�logo.
Da mesma forma, estrdup tenta fazer uma c�pia de uma string e sai com uma
mensagem (por meio de eprintf) se ela ficar sem mem�ria:

/* estrdup: duplica uma string, reporta se houver erro */


char *estrdup(char *s)
{
char *t;

t = (char *)malloc(strlen(s)+1);
if (t == NULL)
eprintf("estrdup(\"%.20s\") failed:", s);
strcpy(t, s);
return t;
}

e emalloc fornece um servi�o semelhante para as chamadas a malloc:

/* emalloc: malloc e reporta se houver erro */


void *emalloc(size_t n)
{
void *p;

p = malloc(n);
if (p == NULL)
eprintf("malloc of %u bytes failed:", n);
return p;
}

Um arquivo header coincidente chamado eprintf.h declara essas fun��es:

/* eprintf.h: fun��es error wrapper */


extern void eprintf(char *, ...);
extern void weprintf(char *, ...);
extern char *estrdup(char *);
extern void *emalloc(size_t);
extern void *erealloc(void *, size_t);
extern char *progname(void);
extern void setprogname(char *);

Esse header � inclu�do em todo arquivo que chama uma das fun��es de erro. Cada
mensagem de erro tamb�m inclui o nome do programa se ele foi definido por quem
chama; isso � definido e recuperado pelas fun��es comuns setprogname e progname,
declaradas no arquivo header e definidas no arquivo-fonte com eprintf:

static char *name = NULL; /* nome do programa para as mensagens */


/* setprogname: define nome armazenado do programa */
void setprogname(char *str)
{
name = estrdup(str);
}

/* progname: retorna nome armazenado do programa */


char *progname(void)
{
return name;
}

O uso t�pico se parece com o seguinte:

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


{
setprogname("markov");
f = fopen(argv[i], "r");
if (f == NULL)
eprintf("can't open %s:", argv[i]);
}

que imprime sa�da assim:

markov: can't open psalm.txt: No such file or directory

Achamos que essas fun��es de wrapper s�o convenientes para nosso uso
pr�prio, uma vez que elas unificam o tratamento de erros, e sua pr�pria exist�ncia
nos encoraja a pegar os erros, em vez de ignor�-los. N�o h� nada de especial em
nosso projeto, por�m, e voc� pode preferir alguma variante para seus pr�prios
programas.
Suponhamos que, em vez de escrever fun��es para nosso uso pr�prio, n�s
estejamos criando uma biblioteca para outras pessoas usarem em seus programas. O
que uma fun��o dessa biblioteca deveria fazer se ocorresse um erro irrecuper�vel?
As fun��es que escrevemos anteriormente neste cap�tulo exibem uma mensagem e
morrem. Esse � um comportamento aceito em muitos programas, particularmente nas
ferramentas isoladas pequenas e nos aplicativos. Nos outros programas, por�m, sair
� errado, uma vez que isso evita que o restante do programa tente alguma
recupera��o. Por exemplo, um processador de texto deve se recuperar dos erros para
n�o perder o documento que voc� est� digitando. Em algumas situa��es uma rotina de
biblioteca n�o deve nem exibir uma mensagem, uma vez que o programa pode estar
sendo executado em um ambiente no qual uma mensagem vai interferir com os dados
exibidos ou desaparecer� sem deixar vest�gios. Uma alternativa �til � registrar a
sa�da de diagn�stico em um "arquivo de registro" expl�cito, onde ela pode ser
monitorada de forma independente.

Detectar erros em um n�vel baixo, lidar com eles em um n�vel alto. Como princ�pio
geral, os erros devem ser detectados no n�vel mais baixo poss�vel, mas tratados em
um n�vel alto. Na maioria dos casos, quem chama deve determinar como lidar com os
erros, n�o quem � chamado. As rotinas de biblioteca podem ajudar nisso atrav�s da
falha. Esse racioc�nio nos levou a retornar NULL para um campo n�o-existente em vez
de abortar. Do mesmo modo, csvgetline retorna NULL independente do n�mero de vezes
que ela � chamada ap�s o primeiro final de arquivo.
Os valores de retorno apropriados nem sempre s�o t�o �bvios, como vimos na
discuss�o anterior sobre o que csvgetline deveria retornar. Queremos retornar o
m�ximo poss�vel de informa��es �teis, mas em uma forma cujo uso seja f�cil para o
restante do programa. Em C, C++ e Java, isso significa retornar alguma coisa como o
valor da fun��o, e talvez outros valores por meio dos argumentos de refer�ncia
(ponteiros). Muitas fun��es de biblioteca dependem da habilidade de distinguir os
valores normais dos valores de erro. As fun��es de entrada getchar retornam uma
char para os dados v�lidos, e algum valor n�o char como EOF para o final de arquivo
ou erro.
Esse mecanismo n�o funciona se os valores de retorno legais da fun��o
ocuparem todos os valores poss�veis. Por exemplo, uma fun��o matem�tica como log
pode retornar qualquer n�mero de ponto flutuante. No ponto flutuante IEEE, um valor
especial chamado NaN ("not a number") indica um erro e pode ser retornado como um
sinal de erro.
Algumas linguagens, tais como a Perl e Tcl, fornecem uma maneira barata de
agrupar dois ou mais valores em um tuple. Em tais linguagens, um valor de fun��o e
qualquer estado de erro podem ser facilmente retornados juntos. A C++ STL fornece
um tipo de dados pair que tamb�m pode ser usado dessa forma.
Se poss�vel, � desej�vel distinguir os diversos valores excepcionais como
final de arquivo e estados de erro, em vez de coloc�-los juntos em um �nico valor.
Se os valores n�o puderem ser separados facilmente, outra op��o seria retornar um
valor �nico de "exce��o" e fornecer outra fun��o que retornasse mais detalhes sobre
o �ltimo erro.
Essa � a abordagem usada pelo Unix e na biblioteca padr�o C, onde muitas
chamadas de sistema e fun��es de biblioteca retornam -1 mas tamb�m definem uma
vari�vel global chamada errno que codifica o erro espec�fico; strerror retorna uma
string associada ao n�mero do erro. Em nosso sistema, este programa:

#include <stdio.h>
#include <string.h>
#inc1ude <errno.h>
#include <math.h>

/* errno main: testa errno */


int main(void)
{
double f;
errno = 0; /* limpa estado de erro */
f = log(-1.23);
printf("%f %d %s\n", f, errno, strerror(errno));
return 0;
}

imprime:

nan0xl0000000 33 Domain error

Como mostramos, errno deve ser declarado primeiro. Depois se ocorrer um erro, errno
ser� definido com um valor diferente de zero.

Usar exce��es somente nas situa��es excepcionais. Algumas linguagens fornecem


exce��es para pegar situa��es incomuns e fazer a recupera��o. Elas fornecem um
fluxo alternativo de controle quando algo de ruim acontece. As exce��es n�o devem
ser usadas para lidar com valores de retorno esperados. A leitura de um arquivo
eventualmente produzir� um final de arquivo; isso deve ser tratado com um valor de
retorno e n�o por uma exce��o. Em Java, escrevemos:

String fname = "someFileName";


try {
FileInputStream in = new FileInputStream(fname);
int c;
while ((c = in.read()) != -1)
System.out.print((char) c);
in.close();
} catch (FileNotFoundException e) {
System.err.println(fname + " not found");
} catch (IOException e) {
System.err.println("IOException: " + e);
e.printStackTrace();
}

O loop l� os caracteres at� o final do arquivo, um evento esperado que �


sinalizado por um valor de retorno de -1 de read. Se o arquivo n�o puder ser
aberto, ele levanta uma exce��o, em vez de definir o fluxo de entrada como NULL,
como seria feito na C ou C++. Finalmente, se algum outro erro de E/S ocorrer no
bloco try, ele tamb�m � excepcional, e � pego pela cl�usula IOException.
As exce��es quase sempre s�o superutilizadas. Como elas distorcem o fluxo do
controle, elas podem levar a constru��es complicadas que est�o propensas a bugs. �
excepcional a abertura de um arquivo falhar; a gera��o de uma exce��o neste caso
nos parece excesso de engenharia. As exce��es s�o melhor reservadas para os eventos
verdadeiramente inesperados, tais como o enchimento total dos sistemas de arquivos
ou os erros de ponto flutuante.
Nos programas em C, o par de fun��es setjmp e longjmp fornece um servi�o de
n�vel bem mais inferior sobre o qual um mecanismo de exce��o pode ser constru�do,
mas ele � suficientemente antigo para n�o termos de falar dele.
E a recupera��o dos recursos quando ocorre um erro? Uma biblioteca deve
tentar uma recupera��o quando algo sai errado? Nem sempre esse � o caso, mas isso
seria bom pois ter�amos certeza de que ela deixaria no estado mais limpo e
inalterado poss�vel. Certamente o armazenamento n�o utilizado seria retomado. Se as
vari�veis ainda pudessem ser acessadas, elas deveriam ser definidas em valores
sensitivos. Uma fonte comum de bugs � tentar usar um ponteiro que aponta para
armazenamento liberado. Se o c�digo de tratamento de erros definir ponteiros em
zero depois de liberar aquilo para o qual eles apontam, isso n�o passar�
desapercebido. A fun��o reset da segunda vers�o da biblioteca CSV foi uma tentativa
de abordar essas quest�es. Em geral, elas visam manter a biblioteca utiliz�vel ap�s
a ocorr�ncia de um erro.

4.8 Interfaces de usu�rio

At� aqui falamos principalmente sobre as interfaces entre os componentes de


um programa ou entre os programas. Mas h� outro tipo importante de interface,
aquela entre um programa e seus usu�rios humanos.
A maioria dos programas de exemplo deste livro se baseia no texto; portanto,
suas interfaces de usu�rio tendem a ser diretas. Como discutimos na se��o anterior,
os erros deveriam ser detectados e reportados, e a recupera��o tentada onde isso
fizer sentido. A sa�da de erros deve incluir todas as informa��es dispon�veis e
deve ser o mais significativa poss�vel tendo em vista o contexto. Um diagn�stico
n�o deve dizer:

estrdup failed

quando ele poderia dizer

markov: estrdup("Derrida") failed: Memory limit reached

N�o custa nada adicionar as informa��es extras como fizemos em estrdup, e isso pode
ajudar o usu�rio a identificar um problema ou fornecer entrada v�lida.
Os programas devem exibir informa��es sobre o uso adequado quando um erro
for cometido, como mostram fun��es como:

/* usage: imprime mensagem de uso e sai */


void usage(void)
{
fprintf(stderr, "usage: %s [-d] [-n nwords]"
" [-s seed] [files ...]\n", progname());
exit(2);
}

O nome do programa identifica a fonte da mensagem, o que � particularmente


importante se essa ela for parte de um processo maior. Se um programa apresentar
uma mensagem que diz apenas syntax error ou estrdup failed, o usu�rio pode n�o ter
a menor id�ia de quem disse isso.
O texto das mensagens de erro, avisos e caixas de di�logo deve declarar a
forma da entrada v�lida. N�o diga que um par�metro � grande demais; reporte o
intervalo v�lido dos valores. Sempre que poss�vel, o texto deve ser uma entrada
v�lida por si mesmo, tal como a linha de comando completa com o par�metro definido
adequadamente. Al�m de direcionar os usu�rios quanto ao uso adequado, tal sa�da
pode ser capturada em um arquivo ou por um movimento do mouse e depois usada para
executar algum outro processo. Isso indica uma fraqueza das caixas de di�logo: seu
conte�do � dif�cil de entender para uso futuro.
Uma maneira efetiva de criar uma boa interface de usu�rio para entrada �
criar uma linguagem especializada para definir par�metros, controlar a��es e assim
or diante. Uma boa nota��o pode tornar um programa f�cil de usar, e ajudar a
organizar uma implementa��o. As interfaces baseadas na linguagem s�o o assunto o
Cap�tulo 9.
A programa��o defensiva, ou seja, ter certeza de que um programa n�o est�
vulner�vel � entrada ruim, � importante tanto para proteger os usu�rios contra si
mesmos e tamb�m como um mecanismo de seguran�a. Isso � discutido com mais detalhes
no Cap�tulo 6, o qual fala sobre os testes de programas.
Para a maioria das pessoas, as interfaces gr�ficas s�o a interface de
usu�rio para seus computadores. As interfaces gr�ficas de usu�rio s�o um t�pico
enorme, portanto vamos dizer apenas algumas coisas relativas a este livro.
Primeiro, as interfaces gr�ficas s�o dif�ceis de criar e ficarem "certas", uma vez
que sua adequa��o e sucesso dependem muito do comportamento e das expectativas
humanas. Em segundo lugar, e em termos pr�ticos, quando um sistema tem uma
interface de usu�rio, geralmente existe mais c�digo para lidar com a intera��o do
usu�rio do que nos algoritmos que fazem o trabalho.
No entanto, princ�pios conhecidos se aplicam tanto ao projeto externo quanto
implementa��o interna do software da interface gr�fica. Do ponto de vista do
usu�rio, as quest�es de estilo como simplicidade, clareza, regularidade, unifor-
midade, familiaridade e restri��o todas contribuem para uma interface que � f�cil
de usar. A falta de tais propriedades geralmente acompanha as interfaces desa-
grad�veis ou esquisitas.
A uniformidade e regularidade s�o desej�veis, incluindo o uso consistente de
termos, unidades, formatos, layouts, fontes, cores, tamanhos e todas as outras
op��es que um sistema gr�fico disponibiliza. Quantas palavras diferentes s�o usadas
para sair de um programa ou fechar uma janela? As op��es variam de Abandonar a
control-Z, com pelo menos uma d�zia delas no meio. Essa falta de consist�ncia �
confusa para um nativo da l�ngua e frustrante para os outros.
Dentro do c�digo de gr�ficos, as interfaces s�o particularmente impor-
tantes, uma vez que esses sistemas s�o grandes, complicados e orientados por um
modelo de entrada diferente da digitaliza��o de texto seq�encial. A programa��o
orientada para o objeto excede nas interfaces gr�ficas de usu�rio, uma vez que
fornece uma maneira de encapsular todo o estado e os comportamentos das janelas,
usando a heran�a para combinar as similaridades nas classes base, separando as
diferen�as nas classes derivadas.

Leitura suplementar

Embora poucos de seus detalhes t�cnicos estejam atualizados, The Mythical


Man Month, de Frederick P. Brooks, Jr. (Addison-Wesley, 1975; edi��o de anivers�rio
1995), � uma leitura deliciosa e cont�m ideias sobre o desenvolvimento de software
que s�o t�o valiosas hoje quanto na �poca da publica��o original.
Quase todos os livros sobre programa��o t�m alguma coisa �til a dizer sobre
o projeto de interface. Um livro pr�tico baseado em experi�ncia conseguida a duras
penas � o Large-Scale C++ Software Design, de John Lakos (Addison-Wesley, 1996), o
qual discute como construir e gerenciar programas verdadeiramente grandes em C++. O
livro C Interfaces and Implementations, de David Hanson (Addison-Wesley, 1997), �
um bom tratamento para os programas em C.
O livro Rapid Development (Microsoft Press, 1996), de Steve McConnell, � uma
descri��o excelente de como construir software em equipes, com �nfase para o papel
da "prototipa��o".
Existem v�rios livros interessantes sobre o projeto das interfaces gr�ficas
de usu�rio, com uma variedade de perspectivas diferentes. Sugerimos os livros
Designing Visual Interfaces: Communication Oriented Techniques, de Kevin Mullet e
Darrell Sano (Prentice Hall, 1995), Designing the User Interface: Strategies for
Effective Human-Computer Interaction, de Ben Shneiderman (3a edi��o, Addison-
Wesley, 1997), About Face: The Essentials of User Interface Design, de Alan Cooper
(IDG, 1995) e User Interface Design, de Harold Thimbleby (Addison-Wesley, 1990).
5
Depurando

bug.
b. Um defeito ou falha em uma m�quina, plano, ou similar. Origem EUA 1889 Pall Mall
Gaz. 11 de mar�o. 1/1 Sr. Edison, fui informado e fiquei as duas �ltimas noites
descobrindo 'um bug' em seu fon�grafo - uma express�o para solucionar uma
dificuldade, a qual implica algum inseto imagin�rio que tenha entrado secretamente
e esteja causando todo o problema.
Oxford English Dictionary, 2a edi��o

Nos quatro �ltimos cap�tulos n�s apresentamos bastante c�digo, e


pretend�amos que todo ele funcionasse desde o in�cio. Naturalmente isso n�o foi
verdadeiro. Ocorreram muitos bugs. A palavra "bug" n�o se originou com os
programadores, mas certamente � um dos termos mais comuns em computa��o. Por que o
software tem de ser t�o dif�cil?
Um motivo � que a complexidade de um programa est� relacionada ao n�mero de
maneiras pelas quais seus componentes podem interagir, e o software est� cheio de
componentes e intera��es. Muitas t�cnicas tentam reduzir as conex�es entre os
componentes para que haja menos partes para interagir. Os exemplos incluem o
ocultamento de informa��es, a abstra��o e as interfaces, al�m dos recursos de
linguagem que os suportam. Existem tamb�m t�cnicas para garantir a integridade do
projeto de um software - provas de programa, modelo, an�lise de requisitos,
verifica��o formal - mas nenhuma delas ainda alterou o modo pelo qual o software �
constru�do. Elas t�m tido sucesso apenas com os problemas pequenos. A verdade � que
sempre haver� erros que encontramos testando e eliminamos, depurando.
Os bons programadores sabem que passam tanto tempo depurando quanto passam
escrevendo e, assim, eles tentam aprender com os pr�prios erros. Todo bug que voc�
encontra pode ensin�-lo a evitar que um bug semelhante ocorra novamente ou como
reconhec�-lo se ele ocorrer novamente.
A depura��o � dif�cil e pode levar muito tempo e exigir um tempo
imprevis�vel, portanto o objetivo � evitar isso ao m�ximo. As t�cnicas que ajudam a
reduzir o tempo de depura��o incluem o bom projeto, bom estilo, testes de
condi��es-limite, verifica��o de declara��es e sanidade do c�digo, programa��o
defensiva, interfaces bem projetadas, dados globais limitados e ferramentas de
verifica��o. Um quilo de preven��o vale um grama de cura.
Qual � o papel da linguagem? Uma das principais for�as na evolu��o das
linguagens de programa��o tem sido a tentativa de evitar os bugs por meio dos
recursos da linguagem. Alguns recursos tornam os erros de classes menos prov�veis:
a verifica��o de intervalo nos subscritos, os ponteiros restritos ou nenhum
ponteiro, a coleta de lixo, os tipos de dados de string, E/S digitada e verifica��o
de tipo forte. Do outro lado da moeda, alguns recursos est�o propensos a erros,
como as declara��es goto, as vari�veis globais, os ponteiros irrestritos e as
convers�es de tipo autom�ticas. Os programadores devem conhecer as partes
potencialmente arriscadas de suas linguagens e tomar cuidado extra quando us�-las.
Eles tamb�m devem ativar todas as verifica��es do compilador e prestar aten��o aos
avisos.
Cada recurso de linguagem que evita algum problema tem seu pr�prio custo. Se
uma linguagem de n�vel mais alto faz os bugs simples desaparecem automaticamente, o
pre�o � que ela facilita a cria��o de bugs de n�vel mais alto. Nenhuma linguagem
evita que voc� cometa erros.
Embora tor�amos pelo contr�rio, grande parte do tempo de programa��o � gasto
testando e depurando. Neste cap�tulo, vamos discutir como tornar seu tempo de
depura��o o mais curto e produtivo poss�vel. Vamos voltar aos testes no Cap�tulo 6.

5.1 Depuradores
Os compiladores das principais linguagens geralmente v�m com depuradores
sofisticados, quase sempre inclu�dos em um ambiente de desenvolvimento que integra
a cria��o e edi��o do c�digo-fonte, compila��o, execu��o e depura��o, tudo em um
�nico sistema. Os depuradores incluem interfaces gr�ficas que verificam cada
declara��o ou fun��o de um programa, parando em determinadas linhas ou quando
ocorre uma condi��o espec�fica. Eles tamb�m fornecem recursos de formata��o e
exibi��o dos valores das vari�veis.
Um depurador pode ser invocado diretamente quando se conhece a exist�ncia de
um problema. Alguns depuradores assumem automaticamente quando algo errado acontece
durante a execu��o do programa. Geralmente � f�cil descobrir onde o programa estava
sendo executado quando morreu, examinar a sequ�ncia de fun��es que estavam ativas
(o stack trace), e exibir os valores das vari�veis locais e globais. Essas
informa��es podem ser suficientes para identificar um bug. Caso contr�rio, os
pontos de interrup��o e o escalona-mento possibilitam a reexecu��o de um programa
com falhas, uma etapa de cada vez, para encontrar o primeiro lugar no qual algo
saiu errado.
No ambiente certo e nas m�os de um usu�rio experiente, um bom depurador pode
tornar a depura��o efetiva e eficiente, se n�o exatamente indolor. Com tais
ferramentas poderosas � disposi��o, por que algu�m depuraria sem elas? Por que
precisamos de todo um cap�tulo sobre a depura��o?
Existem v�rias boas raz�es, algumas objetivas e outras baseadas na
experi�ncia pessoal. Algumas linguagens n�o muito conhecidas n�o t�m depurador ou
fornecem capacidades de depura��o apenas rudimentares. Os depuradores s�o
dependentes do sistema; portanto, voc� talvez n�o tenha acesso ao depurador
conhecido de um sistema ao trabalhar em outro. Os depuradores n�o lidam bem com
alguns programas: os programas multiprocesso ou multien-cadeados, sistemas
operacionais e sistemas distribu�dos quase sempre devem ser depurados por
abordagens de n�vel mais baixo. Em tais situa��es, voc� est� por conta pr�pria, sem
muita ajuda al�m das declara��es impressas e de sua pr�pria experi�ncia e
habilidade para raciocinar sobre o c�digo.
Como op��o pessoal, tendemos a n�o usar os depuradores al�m da obten��o de
um rastreamento de pilha ou do valor de uma ou duas vari�veis. Um motivo para isso
� que � f�cil se perder em meio aos detalhes das estruturas de dados complicadas e
do fluxo de controle. Achamos o passo a passo de um programa menos produtivo do que
pensar bastante e adicionar declara��es de sa�da e c�digo autoverificador nos
locais cr�ticos. Clicar nas declara��es leva mais tempo do que examinar a sa�da dos
displays colocados com crit�rio. � preciso menos tempo para resolver onde colocar
as declara��es de impress�o do que para fazer o passo a passo da se��o cr�tica do
c�digo, mesmo assumindo que sabemos onde ela est�. Mais importante ainda � o fato
de que as declara��es de depura��o permanecem no programa; as sess�es do depurador
s�o transit�rias.
A prova cega com um depurador provavelmente n�o � produtiva. � mais �til
usar o depurador para descobrir o estado do programa quando ele falha, depois
pensar em como a falha poderia ter acontecido. Os depuradores podem ser programas
antigos e dif�ceis, e particularmente no caso de iniciantes eles podem confundir
mais do que ajudar. Se voc� fizer a pergunta errada, eles provavelmente lhe dar�o
uma resposta, mas voc� talvez n�o saiba que ela � enganosa.
Um depurador pode ser de enorme valor, por�m, e voc� deve incluir um em seu
kit de ferramentas de depura��o. Ele pode ser a sua primeira alternativa. De
qualquer maneira, se voc� n�o tem um depurador, ou se est� parado com um problema
particularmente dif�cil, as t�cnicas deste cap�tulo o ajudar�o a depurar efetiva e
eficientemente. Elas tamb�m devem tornar mais produtivo o uso que voc� faz do seu
depurador, uma vez que dizem respeito a como raciocinar sobre os erros e as
prov�veis causas.

5.2 Boas pistas, bugs f�ceis

Opa! Algo est� muito errado. Meu programa deu pane, ou imprimiu coisa sem
sentido, ou parece estar sendo executado indefinidamente. E agora?
Os iniciantes tendem a culpar o compilador, a biblioteca, ou qualquer outra
coisa que n�o seja o seu pr�prio c�digo. Os programadores experientes adorariam
fazer o mesmo, mas eles sabem que na realidade a maioria dos problemas � culpa
deles mesmos.
Felizmente, a maioria dos bugs � simples e pode ser encontrada com t�cnicas
simples. Examine a evid�ncia na sa�da errada e tente descobrir como ela poderia ter
sido produzida. Veja toda a sa�da da depura��o antes da pane. Se poss�vel, obtenha
um rastreamento de pilha de um depurador. Agora voc� sabe alguma coisa daquilo que
aconteceu e onde. Pare um pouco para refletir. Como isso poderia ter acontecido?
Volte o racioc�nio at� o estado do programa em pane para determinar o que poderia
ter causado aquilo.
A depura��o envolve o racioc�nio reverso, como a solu��o de crimes
misteriosos. Alguma coisa imposs�vel ocorreu, e a �nica informa��o s�lida � que ela
realmente ocorreu. Assim sendo, devemos pensar inversamente a partir do resultado
para descobrir os motivos. Depois que tivermos a explica��o completa, saberemos o
que consertar e, ao mesmo tempo, descobriremos algumas outras coisas que n�o
esper�vamos.

Procure padr�es conhecidos. Pergunte a si mesmo se esse � um padr�o conhecido. "Eu


j� vi isso antes" quase sempre � o in�cio da compreens�o ou mesmo toda a resposta.
Os bugs comuns t�m assinaturas distintivas. Por exemplo, os programadores novatos
em C quase sempre escrevem

? int n;
? scanf("%d", n);

em vez de

int n;
scanf("%d", &n);

e isso geralmente causa uma tentativa de acessar mem�ria fora dos limites quando
uma linha de entrada � lida. As pessoas que ensinam C reconhecem o sintoma
instantaneamente.
Os tipos trocados e as convers�es em printf e scanf s�o uma fonte eterna de
bugs f�ceis:

? int n = 1;
? double d = PI;
? printf("%d %f\n", d, n);

A assinatura deste erro eventualmente � a apari��o de valores absurdos: inteiros


imensos ou valores de ponto flutuante incrivelmente grandes ou pequenos. Em um Sun
SPARC, a sa�da desse programa � um n�mero imenso e astron�mico (deve ser dobrado
para caber):

1074340347 268156158598852001534108794260233396350\
1936585971793218047714963795307788611480564140\
0796821289594743537151163524101175474084764156\ 422771408323839623430144.000000

Outro erro comum � usar %f em vez de %lf para ler um double com scanf.
Alguns compiladores pegam tais erros verificando se as strings de formato e os
tipos dos argumentos scanf e printf coincidem; se todos os avisos estiverem
ativados, para o printf acima, o compilador GNU gcc reporta que:

x.c:9: warning: int format, double arg (arg 2)


x.c:9: warning: double format, different type arg (arg 3)
A falha na inicializa��o de uma vari�vel local faz surgir outro erro
distinto. O resultado quase sempre � um valor extremamente grande, o lixo deixado
do valor anterior que estava armazenado na mesma localiza��o de mem�ria. Alguns
compiladores o avisam, embora voc� talvez tenha de ativar a verifica��o no tempo de
compila��o, e eles nunca pegam todos os casos. A mem�ria retornada por alocadores
como malloc, realloc e new tamb�m deve ser lixo; portanto, tenha certeza de t�-la
inicializado.

Examine a altera��o mais recente. Qual foi a �ltima altera��o? Se voc� est�
alterando apenas uma coisa de cada vez � medida que o programa evolui, o bug muito
provavelmente est� no c�digo novo ou foi exposto por ele. Uma olhada cuidadosa nas
altera��es recentes ajuda a localizar o problema. Se o bug aparecer na vers�o nova
e n�o na antiga, o c�digo novo faz parte do problema. Isso significa que voc� deve
preservar pelo menos a vers�o anterior do programa que acredita estar correta, para
poder comparar os comportamentos. Isso tamb�m significa que voc� deve manter os
registros das altera��es feitas e dos bugs consertados, para n�o ter de redescobrir
essas informa��es vitais enquanto estiver tentando consertar um bug. Os sistemas de
controle do c�digo-fonte e outros mecanismos de hist�rico tamb�m ajudam.

N�o cometa o mesmo erro duas vezes. Depois de consertar um bug, pergunte se voc�
poderia ter cometido o mesmo erro em algum outro lugar. Isso aconteceu com um de
n�s alguns dias antes de come�armos a escrever este cap�tulo. O programa era um
prot�tipo r�pido para um colega, e inclu�a um modelo para argumentos opcionais:

? for (i = 1; i < argc; i++) {


? if (argv[i][0] != '-') /* op��es encerradas */
? break;
? switch (argv[i][1]) {
? case 'o': /* nome do arquivo de sa�da */
? outname = argv[i];
? break;
? case 'f':
? from = atoi(argv[i]);
? break;
? case 't':
? to = atoi(argv[i]);
? break;
? ...

Logo depois que nosso colega o experimentou, ele reportou que o nome do arquivo de
sa�da sempre tinha o prefixo -o. Isso foi embara�oso mas f�cil de resolver; o
c�digo deveria dizer:

outname = &argv[i][2];

Assim sendo, isso foi consertado e enviado novamente. Outro relat�rio voltou
dizendo que o programa n�o lidava adequadamente com um argumento como -f 123: o
valor num�rico convertido era sempre zero. Esse � o mesmo erro; o pr�ximo case da
chave deveria dizer:

from = atoi(&argv[i][2]);

Como o autor ainda estava com pressa, ele n�o percebeu que o mesmo problema ocorreu
outras duas vezes e foi necess�ria outra rodada para que todos os erros
fundamentalmente id�nticos fossem consertados.
O c�digo f�cil pode ter bugs se sua familiaridade nos fizer baixar a guarda.
Mesmo quando o c�digo � t�o simples que voc� poderia escrev�-lo dormindo, n�o
adorme�a enquanto estiver escrevendo o c�digo.
Depure agora, n�o mais tarde. Estar correndo demais tamb�m pode ser prejudicial em
outras situa��es. N�o ignore uma pane quando ela acontecer. Descubra-a
imediatamente, uma vez que ela pode n�o acontecer novamente at� ser tarde demais.
Um exemplo famoso ocorreu na miss�o Pathfinder a Marte. Depois da aterrissagem com
falhas em julho de 1997, os computadores da espa�onave tendiam a ser
reinicializados mais ou menos uma vez por dia e os motores eram obstru�dos. Depois
que eles descobriram o problema, perceberam que j� haviam visto aquele problema
acontecer antes. Durante os testes de pr�-lan�amento as reinicializa��es haviam
ocorrido, mas eles as ignoraram porque os engenheiros estavam trabalhando em
problemas n�o-relacionados. Assim sendo, eles foram for�ados a lidar com o problema
mais tarde quando a m�quina estava a dez milh�es de milhas de dist�ncia e o
conserto era muito mais dif�cil.

Obtenha um rastreamento de pilha. Embora os depuradores possam testar a execu��o


dos programas, um de seus usos mais comuns � o exame do estado de um programa ap�s
a morte. O n�mero da linha de c�digo com falha, quase sempre parte de um
rastreamento de pilha, � a informa��o de depura��o mais �til. Valores improv�veis
para os argumentos tamb�m s�o uma grande pista (ponteiros zero, inteiros imensos
quando deveriam ser pequenos, ou negativos quando deveriam ser positivos, strings
de caracteres que n�o s�o alfab�ticas).
Aqui temos um exemplo t�pico, baseado na discuss�o de classifica��o do
Cap�tulo 2. Para classificar um array de inteiros, n�s devemos chamar qsort com a
fun��o de compara��o de inteiro icmp:

int arr[N];
qsort(arr, N, sizeof(arr[0]), icmp);

mas suponhamos que ela inadvertidamente passasse o nome da fun��o de compara��o de


string scmp:

? int arr[N];
? qsort(arr, N, sizeof(arr[0]), scmp);

Um compilador n�o pode detectar o erro dos tipos aqui; portanto, podemos esperar o
desastre. Quando executamos o programa, ele d� pane tentando acessar uma
localiza��o de mem�ria ilegal. A execu��o do depurador dbx produz um rastreamento
de pilha como este, o qual foi editado para se ajustar:

0 strcmp(0xla2, Oxlc2) ["strcmp.s":31]


1 scmp(p1 = 0x10001048, p2 = 0xl000l05c) ["badqs.c":13]
2 qst(0x10001048, 0x10001074, 0x400520, 0x4) ["qsort.c":147]
3 qsort(0x10001048, Ox1c2, 0x4, 0x400b20) ["qsort.c":63]
4 main() ["badqs.c":45]
5 __istart() ["crtltinit.s":13]

Isso diz que o programa morreu em strcmp; por inspe��o, os dois ponteiros passados
para strcmp s�o muito pequenos, um sinal claro de problemas. O rastreamento de
pilha d� uma pista dos n�meros de linha onde cada fun��o foi chamada. A linha 13 de
nosso arquivo de teste badqs.c � a chamada

return strcmp(vl, v2);

a qual identifica a chamada com falha e indica o erro.


Um depurador tamb�m pode ser usado para exibir valores de vari�veis locais
ou globais que dar�o informa��es adicionais sobre o que saiu errado.

Leia antes de digitar. Uma t�cnica de depura��o efetiva mas mal apreciada � ler
cuidadosamente o c�digo e consider�-lo por algum tempo sem fazer altera��es. H� uma
urg�ncia poderosa em ir at� o teclado e come�ar a modificar o programa para ver se
o bug vai embora. Mas provavelmente voc� n�o vai saber qual � o problema e mudar� a
coisa errada, talvez criando outro problema. Uma listagem da parte cr�tica do
programa no papel pode lhe dar uma perspectiva diferente daquela que voc� tem na
tela, e o incentiva a refletir mais. Entretanto, n�o torne as listagens uma rotina.
A impress�o completa de um programa s� desperdi�a �rvores, uma vez que � dif�cil
ver a estrutura quando ela est� dispersa em muitas p�ginas, e a listagem se tornar�
obsoleta no momento em que voc� come�ar novamente a edi��o.
Fa�a uma pausa. Eventualmente aquilo que voc� v� no c�digo-fonte � aquilo
que voc� queria dizer e n�o o que voc� escreveu, e algum tempo longe vai tornar
seus enganos mais suaves e ajudar o c�digo a falar por si mesmo.
Resista ao impulso de come�ar a digitar. Vale a pena pensar um pouco.

Explique seu c�digo para outra pessoa. Outra t�cnica efetiva � explicar seu c�digo
para outra pessoa. Isso quase sempre far� com que voc� explique o pr�prio bug. �s
vezes n�o � preciso mais do que algumas senten�as, seguidas por uma declara��o do
tipo "Deixa pr� l�, eu j� sei o que est� errado. Desculpe incomod�-lo." Isso
funciona muito bem, e voc� pode at� mesmo usar n�o-programadores como ouvintes. O
centro de computa��o de uma universidade mantinha um ursinho de pel�cia perto da
help desk. Os alunos que tinham bugs misteriosos deviam explic�-lo para o urso
antes de falar com um consultor humano.

5.3 Sem pistas, bugs dif�ceis

"Eu n�o tenho nenhuma pista. O que est� acontecendo?" Quando voc� realmente
n�o tem ideia do que pode estar errado, a vida fica mais dif�cil.

Torne o bug reproduz�vel. A primeira etapa � ter certeza de que voc� pode fazer o
bug aparecer quando solicitado. � frustrante procurar um bug que n�o acontece
sempre. Gaste algum tempo construindo entrada e defini��es de par�metro que
certamente causar�o o problema, depois crie um bot�o ou uma seq��ncia de teclas que
o reproduza. Se for um bug dif�cil, voc� far� com que ele aconte�a v�rias vezes
enquanto detecta o problema, portanto economizar� tempo fazendo com que ele se
reproduza facilmente.
Se n�o for poss�vel fazer o bug acontecer sempre, tente entender por que
isso acontece. Ser� que algum conjunto de condi��es faz com que ele aconte�a com
mais frequ�ncia do que outros? Mesmo que n�o consiga faz�-lo aparecer sempre, se
puder diminuir o tempo que passa esperando por ele, voc� poder� encontr�-lo mais
r�pido.
Se um programa fornecer a sa�da da depura��o, ative-a. Os programas de
simula��o como o programa de cadeia Markov, do Cap�tulo 3, devem incluir uma op��o
que produz informa��es de depura��o, tais como a semente do gerador de n�mero
aleat�rio para que aquela sa�da possa ser reproduzida. Outra op��o seria permitir a
defini��o da semente. Muitos programas incluem essas op��es e � bom incluir
recursos semelhantes em seus pr�prios programas.

Divida e conquiste. A entrada que faz o programa falhar pode ser menor ou mais
espec�fica? Diminua as possibilidades criando a menor entrada onde o bug continua
aparecendo. Quais altera��es fazem o erro desaparecer? Tente encontrar casos de
teste que focalizam o erro. Cada caso de teste visa uma sa�da definitiva que
confirma ou nega uma hip�tese espec�fica sobre o que est� errado.
Continue na pesquisa bin�ria. Jogue fora metade da sa�da e veja se a sa�da
continua errada. Caso contr�rio, volte ao estado anterior e descarte a outra metade
da sa�da. O mesmo processo de pesquisa bin�ria pode ser usado no pr�prio texto do
programa: elimine alguma parte do programa que n�o deveria ter nenhum
relacionamento com o bug e veja se ele ainda est� l�. Um editor com o recurso
desfazer � �til para reduzir casos de teste e programas grandes, sem perder o bug.

Estude a numerologia das falhas. Eventualmente um padr�o da numerologia de exemplos


com falhas d� uma pista que especifica a pesquisa. Encontramos alguns erros de
ortografia em uma se��o escrita recentemente deste livro, em que letras eventuais
haviam simplesmente desaparecido. Isso era um mist�rio. O texto havia sido criado
com o recurso de recortar e colar de outro arquivo; portanto, parecia que havia
algo de errado com os comandos de recortar e colar do editor de texto. Mas onde
come�ar a procurar o problema? Para obter pistas n�s olhamos os dados, e notamos
que os caracteres que faltavam pareciam estar distribu�dos de maneira uniforme em
todo o texto. Medimos os intervalos e descobrimos que a dist�ncia entre os
caracteres que faltavam era sempre de 1.023 bytes, um valor n�o aleat�rio suspeito.
Uma pesquisa dos n�meros pr�ximos a 1.024 no c�digo-fonte do editor localizou
alguns candidatos. Um deles era um c�digo novo, portanto n�s o examinamos primeiro,
e o bug foi f�cil de detectar, um erro "off-by-one" cl�ssico no qual um byte null
sobrep�e o �ltimo caractere de um buffer de 1.024 bytes.
O estudo dos padr�es dos n�meros relacionados � falha nos apontou
diretamente o bug. O tempo decorrido? Alguns minutos de mist�rio, cinco minutos
para olhar os dados e descobrir o padr�o dos caracteres que estavam faltando, um
minuto para pesquisar os prov�veis locais para conserto e outro minuto para
identificar e eliminar o bug. Este n�o seria encontrado com um depurador, urna vez
que ele envolvia dois programas multiprocessos, orientados pelos cliques do mouse,
e que se comunicavam por meio de um sistema de arquivos.

Exiba a sa�da para localizar sua pesquisa. Se voc� n�o entende o que o programa
est� fazendo, o acr�scimo de declara��es para exibir mais informa��es pode ser a
maneira mais f�cil e barata de descobrir. Coloque-as para verificar sua compreens�o
e refinar suas id�ias sobre o que est� errado. Por exemplo, exiba "n�o consigo
chegar aqui" se achar que n�o � poss�vel atingir determinado ponto do c�digo.
Depois se voc� vir essa mensagem, mova as declara��es de sa�da de volta na dire��o
do in�cio para descobrir onde as coisas come�aram a dar errado. Ou ent�o exiba
mensagens com "cheguei aqui" indo adiante, para localizar o �ltimo local onde as
coisas pareciam estar funcionando. Cada mensagem deve ser distinta para voc� saber
qual est� vendo.
Exiba as mensagens em um formato fixo compacto para que elas sejam f�ceis de
serem examinadas a olho nu ou com programas como a ferramenta de coincid�ncia de
padr�es grep. (Um programa do tipo grep � valioso para pesquisar texto. O Cap�tulo
9 inclui uma implementa��o simples.) Se voc� est� exibindo o valor de uma vari�vel,
formate-o sempre da mesma maneira. Em C e C++, exiba os ponteiros como n�meros
hexadecimais com %x ou %p. Isso vai ajud�-lo a ver se dois ponteiros t�m o mesmo
valor ou se est�o relacionados. Aprenda a ler os valores de ponteiros e reconhecer
os prov�veis e n�o prov�veis, como zero, n�meros negativos, n�meros �mpares e
n�meros pequenos. A familiaridade com a forma dos endere�os tamb�m compensa quando
voc� usa um depurador.
Se a sa�da for potencialmente volumosa, talvez seja suficiente imprimir
sa�das de �nica letra como A, B, ..., como uma exibi��o compacta do lugar aonde o
programa foi.

Escreva c�digo autoverificador. Se precisar de mais informa��es, voc� pode escrever


sua pr�pria fun��o de verifica��o para testar uma condi��o, fazer o dump das
vari�veis relevantes e abortar o programa:

/* check: testa a condi��o, imprime e morre */


void check(char *s)
{
if (var1 > var2) {
printf("%s: var1 %d var2 %d\n", s, var1, var2);
fflush(stdout); /* verifica se toda a entrada est� out */
abort(); /* sinaliza o encerramento anormal */
}
}
Escrevemos check para chamar abort, uma fun��o padr�o da biblioteca C que faz com
que a execu��o do programa seja encerrada anormalmente para an�lise com um
depurador. Em um aplicativo diferente, voc� poderia usar check para execu��o ap�s a
impress�o.
A seguir, adicione chamadas para check onde elas sejam �teis para o seu
c�digo:

check("before suspect");
/* ... suspect code ... */
check("after suspect");

Ap�s um bug ser consertado, n�o jogue fora check. Deixe-a na fonte,
comentada ou controlada por uma op��o de depura��o, para que ela possa ser ativada
novamente quando aparecer o pr�ximo problema dif�cil.
Para os problemas mais dif�ceis, check pode evoluir para fazer a verifica��o
e exibir as estruturas de dados. Essa abordagem pode ser generalizada para rotinas
que executam verifica��es constantes de consist�ncia nas estruturas de dados e em
outras informa��es. Em um programa com estruturas de dados complicadas, � bom
escrever essas verifica��es antes que os problemas apare�am, como componentes do
programa, para que elas possam ser ativadas quando o problema come�ar. N�o as use
apenas quando depurar. Deixe-as instaladas durante todos os est�gios de
desenvolvimento de programa. Se elas n�o forem caras, seria bom deix�-las sempre
ativadas. Os programas grandes, como sistemas de telefonia, quase sempre dedicam
uma quantidade significativa de c�digo para "auditar" os subsistemas que monitoram
as informa��es e o equipamento, e que reportam ou at� mesmo solucionam os
problemas, caso eles ocorram.

Escreva um arquivo de registro. Outra t�tica � escrever um arquivo de registro


contendo um fluxo de formato fixo com sa�da de depura��o. Quando ocorre uma pane,
aquilo que aconteceu antes da pane � registrado. Os servidores da Web e outros
programas de rede mant�m registros extensos do tr�fego para monitorarem eles mesmos
e seus clientes. Este fragmento (editado para se ajustar) vem de um sistema local:

[Sun Dec 27 16:19:24 1998]


HTTPd: access to /usr/local/httpd/cgi-bin/test.html
failed for ml.cs.bell-labs.com,
reason: client denied by server (CGI non-executable)
from http://m2.cs.bell-labs.com/cgi-bin/test.pl

Verifique se esvaziou os buffers de E/S para que os registros finais apare�am no


arquivo de registro. As fun��es de sa�da como printf normalmente fazem o buffer de
sua sa�da para imprimi-la de modo eficiente. O encerramento anormal pode descartar
essa entrada do buffer. Em C, uma chamada para fflush garante que toda a sa�da seja
gravada antes do programa morrer. H� fun��es f l ush an�logas para os fluxos de
sa�da em C++ e Java. Ou ent�o, se puder arcar com a overhead, voc� pode evitar
totalmente o problema do esvaziamento usando E/S sem buffer para os arquivos de
registro. As fun��es padr�o setbuf e setvbuf controlam o buffering; setbuf (f p,
NULL) desliga o buffering no fluxo f p. Os fluxos padr�o de erro (stderr, cerr,
System.err) normalmente s�o "unbuffered" como padr�o.
Desenhe uma figura. �s vezes as figuras s�o mais efetivas do que o texto para
testar e depurar. As figuras s�o particularmente �teis para entender as estruturas
de dados, como vimos no Cap�tulo 2 e, � claro, quando se escreve software gr�fico,
mas elas podem ser usadas em todos os tipos de programas. As plotagens de dispers�o
exibem os valores mal colocados de forma mais efetiva do que colunas de n�meros. Um
histograma de dados revela as anomalias nas notas de exames, nos n�meros
aleat�rios, nos tamanhos de bucket dos alocadores e das tabelas hash, e outros.
Se voc� n�o entender o que est� acontecendo dentro do seu programa, tente anotar as
estruturas de dados com dados estat�sticos e plote o resultado. Os gr�ficos
seguintes plotam, para o programa markov em C do Cap�tulo 3, os comprimentos de
cadeia hash do eixo x e o n�mero de elementos das cadeias daquele comprimento no
eixo y. Os dados de entrada s�o nosso teste padr�o, o Livro dos Salmos (42.685
palavras, 22.482 prefixos). Os dois primeiros par�grafos s�o para os bons
multiplicadores hash de 31 e 37, e o terceiro � para o multiplicador estranho de
128. Nos dois primeiros casos, nenhuma cadeia � mais longa do que 15 ou 16
elementos, e a maioria dos elementos est� nas cadeias de comprimento 5 ou 6. No
terceiro, a distribui��o � mais ampla, a cadeia mais longa tem 187 elementos e
existem milhares de elementos nas cadeias mais longas do que 20.

Use as ferramentas. Fa�a um bom uso dos recursos do ambiente no qual voc� est�
depurando. Por exemplo, um programa de compara��o de arquivos, como diff, compara
as sa�das de execu��es bem-sucedidas e fracassadas de depura��o, para voc� poder
especificar o que mudou. Se a sua sa�da de depura��o � longa, use grep para
pesquisar ou um editor para examinar. Resista � tenta��o de enviar a sa�da da
depura��o para uma impressora: os computadores examinam sa�da volumosa melhor do
que as pessoas. Use os scripts shell e outras ferramentas para automatizar o
processamento da sa�da das execu��es de depura��o.
Escreva programas simples para testar hip�teses ou confirmar seu
entendimento de como alguma coisa funciona. Por exemplo, � v�lido liberar um
ponteiro NULL?

int main(void)
{
free(NULL);
return 0;
}

Programas de controle do c�digo-fonte, como o RCS, controlam as vers�es do


c�digo para voc� ver o que mudou e reverter para as vers�es anteriores e restaurar
um estado conhecido. Al�m de indicar o que mudou recentemente, eles tamb�m podem
identificar se��es de c�digo que t�m um hist�rico longo de modifica��es freq�entes,
e esse � um bom lugar para os bugs se esconderem.

Mantenha registros. Se a pesquisa de um bug continuar durante um determinado


per�odo de tempo, voc� vai come�ar a perder o controle daquilo que j� tentou e do
que descobriu. Se voc� registrar seus testes e resultados, tem menos chance de
esquecer alguma coisa ou achar que verificou alguma possibilidade quando ainda n�o
o fez. O ato de escrever ajuda voc� a se lembrar do problema da pr�xima vez em que
algo semelhante acontecer, e tamb�m ajudar� quando estiver explicando o problema
para algu�m.

5.4 �ltimos recursos

O que voc� faz quando nenhum desses conselhos ajudar? Talvez esteja na hora
de usar um bom depurador para fazer o passo a passo do programa. Caso seu modelo
mental de como alguma coisa funciona esteja simplesmente errado, de modo que voc�
esteja procurando num lugar totalmente errado, ou procurando no lugar certo sem ver
o problema, um depurador vai for��-lo a pensar de forma diferente. Esses bugs de
"modelo mental" est�o entre aqueles mais dif�ceis de achar. O aux�lio mec�nico �
valioso.
Eventualmente o engano � simples: preced�ncia incorreta de operador, ou
operador errado, ou recuo que n�o coincide com a estrutura real, ou um erro de
escopo onde um nome local oculta um nome global ou um nome global que se intromete
em um escopo local. Por exemplo, os programadores quase sempre se esquecem de que &
e | t�m preced�ncia mais baixa do que == e !=. Eles escrevem
? if (x & 1 == 0)
? ...

e n�o podem descobrir por que isso � sempre falso. Eventualmente uma deslizada do
dedo converte um �nico = em dois ou vice-versa:

? while ((c == getchar()) != EOF)


? if (c = '\n')
? break;

Ou c�digo extra � deixado para tr�s durante a edi��o:

? for (i = 0; i < n; i++);


? a[i++] = 0;

Ou a digita��o apressada cria um problema:

? switch (c) {
? case '<':
? mode = LESS;
? break;
? case '>':
? mode = GREATER;
? break;
? defualt:
? mode = EQUAL;
? break;
? }

Eventualmente o erro pode envolver argumentos na ordem errada em uma


situa��o onde a verifica��o de digita��o n�o pode ajudar, como quando se escreve

? memset(p, n, 0); /* armazena n 0's em p */

em vez de

memset(p, 0, n); /* armazena n 0's em p */

Outras vezes algo muda bem atr�s de voc� - vari�veis globais ou


compartilhadas s�o modificadas e voc� n�o percebe que alguma outra rotina pode
toc�-las.
Ou ent�o o seu algoritmo ou estrutura de dados tem uma falha fatal e voc�
simplesmente n�o consegue v�-la. Ao preparar o material das listas vinculadas, n�s
escrevemos um pacote de fun��es de lista para criar elementos novos, vincul�-los na
frente ou atr�s das listas, e assim por diante. Essas fun��es aparecem no Cap�tulo
2. � claro que escrevemos um programa de teste para ter certeza de que tudo estava
correto. Os primeiros testes funcionaram mas um falhou espetacularmente! Em
ess�ncia, este era o programa de teste:

? while (scanf("%s %d", name, &value) != EOF) {


? p = newitem(name, value);
? list1 = addfront(list1, p);
? list2 = addend(list2, p);
? }
? for (p = list1; p != NULL; p = p->next)
? printf("%s %d\n", p->name, p->value);

Foi surpreendentemente dif�cil ver que o primeiro loop estava colocando o mesmo n�
p em ambas as listas, de modo que os ponteiros estavam misturados quando n�s
imprimimos.
� dif�cil encontrar esse tipo de bug, porque seu c�rebro leva voc� direto na
dire��o contr�ria do erro. Assim sendo, um depurador � de grande ajuda, uma vez que
ele for�a voc� a seguir na dire��o contr�ria, acompanhar aquilo que o programa est�
fazendo, e n�o aquilo que voc� acha que ele est� fazendo. Quase sempre o problema
b�sico � algo de errado com a estrutura de todo o programa, e para ver o erro voc�
precisa retornar �s suas suposi��es iniciais.
Observe, por falar nisso, que no exemplo de lista o erro estava no c�digo de
teste, o que tornou o bug mais dif�cil ainda de encontrar. � frustrante a
facilidade com que se perde tempo ca�ando bugs que n�o est�o l�, porque o programa
de teste est� errado, ou testando a vers�o errada do programa, ou n�o atualizando
ou recompilando antes de testar.
Se voc� n�o encontrar um bug depois de ter um trabalho consider�vel, fa�a
uma pausa. Limpe sua mente, fa�a outra coisa. Fale com um amigo e pe�a ajuda. A
resposta pode aparecer do nada, mas se n�o aparecer voc� n�o ficar� parado no mesmo
lugar na pr�xima sess�o de depura��o.
Depois de algum tempo, o problema realmente est� no compilador, biblioteca,
sistema operacional ou mesmo no hardware, particularmente se algo mudou no ambiente
antes de aparecer um bug. Voc� nunca deve come�ar culpando um desses itens, mas
quando tudo o mais foi eliminado, isso pode ser tudo o que lhe restou. Certa vez
n�s tivemos de mover um programa grande de formata��o de texto de seu lar original
em um Unix, para um PC. O programa compilava sem nenhum incidente, mas se
comportava de forma extremamente estranha: ele soltava aproximadamente cada segundo
caractere de sua entrada. Nossa primeira ideia foi que isso deveria ser alguma
propriedade usando inteiros de 16 bits em vez de 32 bits, ou quem sabe algum
problema estranho de ordem de byte. Mas imprimindo os caracteres vistos pelo loop
main, n�s finalmente detectamos um erro no arquivo de header padr�o ctype.h
fornecido pelo fabricante do compilador. Ele implementava isprint como uma macro de
fun��o:

? #define isprint(c) ((c) >= 040 && (c) < 0177)

e o loop de entrada main era basicamente:

? while (isprint(c = getchar()))


? ...

Toda vez que um caractere de entrada estava em branco (40 octal, uma maneira
ruim de escrever ' ') ou maior do que, o que acontecia na maior parte do tempo,
getchar era chamada uma segunda vez porque a macro avaliava seu argumento duas
vezes, e o primeiro caractere de entrada desaparecia para sempre. O c�digo original
n�o era t�o limpo quanto deveria ser - h� muita coisa na condi��o de loop - mas o
arquivo de header do fabricante estava sem d�vida errado.
Hoje em dia ainda se encontram casos desse problema. Esta macro vem dos
arquivos de header atuais de um fabricante diferente:

? #define __iscsym(c) (isalnum(c) || ((c) == '_'))

Os "vazamentos" de mem�ria - a falha em retomar a mem�ria que n�o est� sendo


mais usada - s�o uma fonte significativa de comportamento estranho. Outro problema
� esquecer de fechar arquivos, at� que a tabela de arquivos abertos esteja cheia e
o programa n�o consiga mais abrir nenhum. Os programas com vazamentos tendem a
falhar misteriosamente porque eles ficam sem nenhum recurso, mas a falha espec�fica
n�o pode ser reproduzida.
Eventualmente o pr�prio hardware est� ruim. A falha de ponto flutuante do
processador Pentium 1994 que fez com que determinados c�lculos produzissem
respostas erradas foi um bug altamente divulgado e caro no projeto do hardware,
mas, depois de identificado, ele pode obviamente ser reproduzido. Um dos bugs mais
estranhos que j� vimos envolvia um programa de calculadora, h� muito tempo em um
sistema de dois processadores. �s vezes a express�o 1/2 era impressa como 0.5 e
outras vezes ela era impressa como um va�or consistente mas totalmente errado, tal
como 0.7432; n�o havia um padr�o quanto a uma resposta certa ou errada. O problema
foi atribu�do a uma falha na unidade de ponto flutuante de um dos processadores.
Como o programa de calculadora era executado aleatoriamente em um processador e no
outro, as respostas eram corretas ou absurdas.
Muitos anos atr�s n�s usamos uma m�quina cuja temperatura interna poderia
ser estimada pelo n�mero de bits de ordem baixa que ela obtinha de forma errada nos
c�lculos de ponto flutuante. Uma das placas de circuito estava solta. � medida que
a m�quina esquentava, a placa se movimentava para fora do seu soquete, e mais bits
de dados eram desconectados do backplane.

5.5 Bugs que n�o podem ser reproduzidos

Os bugs que n�o param quietos s�o os mais dif�ceis de lidar, e geralmente o
problema n�o � t�o �bvio quanto uma falha de hardware. O simples fato de que o
comportamento n�o � determinista s�o informa��es por si s�. Isso significa que o
erro n�o pode ser uma falha em seu algoritmo, mas que de alguma maneira seu c�digo
est� usando informa��es que mudam toda vez que o programa � executado.
Verifique se todas as vari�veis foram inicializadas. Voc� pode estar pegando
um valor aleat�rio de alguma coisa que estava armazenada anteriormente na mesma
localiza��o de mem�ria. As vari�veis locais das fun��es e a mem�ria obtida dos
alocadores provavelmente s�o os culpados em C e C++. Defina todas as vari�veis com
valores conhecidos; se houver uma raiz de n�mero aleat�rio que normalmente seja
definida a partir da hora do dia, force-a a ser uma constante, como zero.
Se o bug muda de comportamento ou mesmo desaparece quando o c�digo de
depura��o � adicionado, ele pode ser um erro de aloca��o de mem�ria - em alguma
parte voc� gravou fora da mem�ria alocada, e a adi��o do c�digo de depura��o muda o
layout do armazenamento de forma suficiente para alterar o efeito do bug. A maioria
das fun��es de sa�da, de printf at� as janelas de di�logo, alocam mem�ria elas
mesmas, complicando mais ainda as coisas.
Se o local da pane parecer estar longe de qualquer coisa que poderia estar
errada, o problema muito provavelmente est� na sobreposi��o da mem�ria por meio do
armazenamento em uma localiza��o de mem�ria que s� ser� usada muito mais tarde. �s
vezes o problema pode ser um ponteiro pendente, no qual um ponteiro para uma
vari�vel local � retornado inadvertidamente de uma fun��o, e depois usado. O
retorno do endere�o de uma vari�vel local � receita para um desastre atrasado:

? char *msg(int n, char *s)


? {
? char buf[100];
?
? sprintf(buf, "error %d: %s\n", n, s);
? return buf;
? }

Na hora em que o ponteiro retornado por msg for usado ele n�o apontar� mais para
armazenamento significativo. Voc� deve alocar o armazenamento com malloc, usar um
array static, ou requerer que quem chama forne�a o espa�o.
A utiliza��o de um valor alocado dinamicamente ap�s ele ter sido liberado
tem sintomas similares. N�s mencionamos isso no Cap�tulo 2 quando escrevemos
freeall. Este c�digo est� errado:

? for (p = listp; p != NULL; p = p->next)


? free(p);

Depois que a mem�ria for liberada, ela n�o deve ser usada pois seu conte�do pode
ser alterado e n�o h� garantias de que p->next continuar� apontando para o lugar
certo.
Em algumas implementa��es de malloc e free, a libera��o de um item duas
vezes corrompe as estruturas internas de dados mas n�o causa problemas at� muito
mais tarde, quando uma chamada subsequente esbarra na bagun�a feita anteriormente.
Alguns alocadores v�m com op��es de depura��o que podem ser definidas para
verificar a consist�ncia da arena a cada chamada. Voc� os ativa se tiver um bug
n�o-determinista. Se n�o fizer isso, voc� pode escrever seu pr�prio alocador que
faz parte de sua pr�pria verifica��o de consist�ncia ou registra todas as chamadas
para an�lise separada. Um alocador que n�o tem de ser executado rapidamente � f�cil
de escrever; portanto, essa estrat�gia � poss�vel quando a situa��o � extrema.
Existem tamb�m excelentes produtos comerciais que verificam o gerenciamento da
mem�ria e pegam erros e vazamentos. Escrevendo seu pr�prio malloc e free voc� pode
ter alguns de seus benef�cios caso n�o tenha acesso a eles. Quando um programa
funciona para uma pessoa mas falha para outra, algo deve depender do ambiente
externo do programa. Isso poderia incluir os arquivos lidos pelo programa, as
permiss�es de arquivo, as vari�veis de ambiente, o caminho de pesquisa dos
comandos, os padr�es ou arquivos de inicializa��o. � dif�cil ser um consultor
nessas situa��es, uma vez que voc� precisa se tornar a outra pessoa para duplicar o
ambiente do programa quebrado.

� Exerc�cio 5-1. Escreva uma vers�o de malloc e free que pode ser usada para
depurar os problemas de gerenciamento de armazenamento. Uma abordagem � verificar
todo o espa�o de trabalho de cada chamada de malloc e free; outra � escrever
informa��es de registro que podem ser processadas por outro programa. De qualquer
maneira, adicione marcadores no in�cio e final de cada bloco alocado para detectar
os excedentes de ambos os lados. ?

5.6 Ferramentas de depura��o

Os depuradores n�o s�o as �nicas ferramentas que ajudam a encontrar bugs.


Uma variedade de programas pode nos ajudar a perambular pela volumosa sa�da para
selecionar partes importantes, encontrar anomalias ou reorganizar dados para tornar
mais f�cil a verifica��o do que est� acontecendo. Muitos desses programas fazem
parte do kit de ferramentas padr�o. Alguns s�o escritos para ajudar a encontrar
determinado bug ou analisar um programa espec�fico.
Nesta se��o n�s vamos descrever um programa simples chamado strings que �
particularmente �til para ver os arquivos que em sua maior parte t�m caracteres
n�o-imprim�veis, tais como execut�veis ou formatos bin�rios misteriosos preferidos
por alguns processadores de texto. Quase sempre h� informa��es valiosas ocultas
dentro deles, como o texto de um documento, ou as mensagens de erro e op��es n�o-
documentadas, ou os nomes de arquivos e diret�rios, ou os nomes das fun��es que um
programa pode chamar.
N�s tamb�m achamos strings �teis para localizar texto em arquivos bin�rios.
Os arquivos de imagem quase sempre cont�m strings ASCII que identificam o programa
que as criou, e arquivos compactados (tais como os arquivos zip) podem conter nomes
de arquivo; strings vai encontr�-los tamb�m.
Os sistemas Unix j� fornecem uma implementa��o de strings, embora ela seja
um pouco diferente desta. Ela reconhece quando sua entrada � um programa e examina
apenas o texto e segmentos de dados, ignorando a tabela de s�mbolos. Sua op��o -a a
for�a a ler todo o arquivo.
Na verdade, strings extrai o texto ASCII de um arquivo bin�rio para que o
texto possa ser lido ou processado por outros programas. Se uma mensagem de erro
n�o tiver identifica��o, pode n�o ficar evidente qual programa a produziu,
muito menos por qu�. Nesse caso, a pesquisa nos prov�veis diret�rios com um comando
como este

% strings *.exe *.dll | grep 'mensagem misteriosa'


poderia localizar o produtor.
A fun��o strings l� um arquivo e imprime todas as execu��es de pelo menos
caracteres imprim�veis MINLEN = 6.

/* strings: extract printable strings from stream */


void strings(char *name, FILE *fin)
{
int c, i;
char buf[BUFSIZ];

do { /* uma vez para cada string */


for (i = 0; (c = getc(fin)) != EOF; ) {
if (!isprint(c))
break;
buf[i++] = c;
if (i >= BUFSIZ)
break;
}
if (i >= MINLEN) /* imprime se for suficientemente longo */
printf("%s:%.*s\n), name, i, buf);
} while (c != EOF);
}

A string de formato %.*s do formato printf assume o comprimento de string do


pr�ximo argumento (i), uma vez que a string (buf) n�o � terminada por null.
O loop do-while encontra e depois imprime cada string terminada com EOF. A
verifica��o do final de arquivo na parte inferior permite que getc e os loops de
string compartilhem uma condi��o de encerramento e permite que uma �nica printf
trate do final da string, final de arquivo e string longa demais.
Um loop externo de quest�o padr�o com um teste na parte superior, ou um
�nico loop getc com corpo mais complexo exigiria a duplica��o de printf. Essa
fun��o iniciou a vida assim, mas ela tinha um bug na declara��o printf. N�s
consertamos isso em um lugar mas nos esquecemos de consertar em outros dois. ("Eu
cometi o mesmo erro em algum outro lugar?") Nesse ponto, ficou claro que o programa
precisava ser rescrito para que houvesse menos c�digo em duplicata, o que levou ao
do-while.
A rotina main chama a fun��o strings de cada um de seus arquivos de
argumento:

/* strings main: encontra strings imprim�veis nos arquivos */


int main(int argc, char *argv[])
{
int i;
FILE *fin;

setprogname("strings");
if (argc == 1)
eprintf("usage: strings filenames");
else {
for (i = 1; i < argc; i++) {
if ((fin = fopen(argv[i], "rb")) == NULL)
weprintf("can't open %s:", argv[i]);
else {
strings(argv[i], fin);
fclose(fin);
}
}
}
return 0;
}

Voc� deve estar surpreso com o fato de que strings n�o l� sua entrada padr�o
quando nenhum arquivo � nomeado. Originalmente ela lia. Para explicar por que ela
n�o acessa agora, precisamos contar uma hist�ria de depura��o.
O caso de teste �bvio de strings � executar o pr�prio programa. Isso
funcionou bem para o Unix, mas no Windows 95 o comando

C:\> strings <strings.exe

produziu exatamente cinco linhas de sa�da:

!This program cannot be run in DOS mode.


'.rdata
@.data
.idata
.reloc

A primeira linha se parece com uma mensagem de erro e n�s perdemos algum
tempo at� perceber que ela � na verdade uma string do programa, e a sa�da est�
correta, pelo menos at� agora. N�o � novidade ter uma sess�o de depura��o
descarrilhada devido a um mal entendido na fonte de uma mensagem.
Mas deveria haver mais sa�da. Onde ela est�? Tarde da noite, a luz finalmente
apagou. ("Eu j� vi isso antes!") Esse � um problema de portabilidade que � descrito
com mais detalhes no Cap�tulo 8. Originalmente n�s escrevemos o programa para ler
apenas a partir de sua entrada padr�o usando getchar. No Windows, por�m, getchar
retorna EOF quando encontra determinado byte (0x1A ou control-Z) na entrada no modo
texto e isso estava causando o encerramento prematuro.
Esse � um comportamento absolutamente legal, mas n�o aquele que est�vamos
esperando dada nossa experi�ncia com o Unix. A solu��o � abrir o arquivo no modo
bin�rio usando o modo "rb". Mas stdin j� est� aberta e n�o h� nenhuma maneira
padr�o de alterar seu modo. (Fun��es como fdopen ou setmode poderiam ser usadas,
mas elas n�o fazem parte do padr�o C.) Finalmente enfrentamos um conjunto de
alternativas desagrad�veis: for�ar o usu�rio a fornecer um nome de arquivo para que
ele funcione adequadamente no Windows, mas que n�o seja convencional do Unix;
produzir silenciosamente as respostas erradas quando um usu�rio do Windows tentar
ler a entrada padr�o; ou usar a compila��o condicional para fazer o comportamento
se adaptar aos diferentes sistemas, ao pre�o da portabilidade reduzida. Preferimos
a primeira op��o para que o mesmo programa funcione da mesma forma em qualquer
lugar.

� Exerc�cio 5-2. O programa strings imprime strings com MINLEN ou mais caracteres,
o que eventualmente produz mais sa�da do que � �til. Forne�a strings com um
argumento opcional para definir o comprimento m�nimo de string. ?

� Exerc�cio 5-3. Escreva vis, que copia entrada para sa�da, exceto por exibir bytes
n�o-imprim�veis como backspaces, caracteres de controle e caracteres n�o-ASCII como
\Xhh onde hh � a representa��o hexadecimal do byte n�o-imprim�vel. Comparada com
strings, vis � mais �til para examinar as entradas que cont�m apenas alguns
caracteres n�o-imprim�veis. ?

� Exerc�cio 5-4. O que vis produz quando a entrada � \XOA? Como voc� tornaria a
sa�da de vis n�o-amb�gua? ?

� Exerc�cio 5-5. Estenda vis para processar uma sequ�ncia de arquivos, dobrar as
linhas longas em qualquer coluna desejada e remover totalmente todos os caracteres
n�o-imprim�veis. Quais outros recursos poderiam ser consistentes com o papel do
programa? ?
5.7 Os buas das outras pessoas

Realisticamente, a maioria dos programadores n�o tem a divers�o de


desenvolver um sistema totalmente novo desde o in�cio. Em vez disso, eles passam
grande parte de seu tempo usando, atualizando, modificando e, assim
inevitavelmente, depurando o c�digo escrito por outras pessoas.
Ao depurar c�digo alheio, tudo que dissemos sobre como depurar seu pr�prio
c�digo se aplica. Antes de come�ar, por�m, voc� deve entender um pouco o modo como
o programa est� organizado e como os programadores originais pensaram e escreveram.
O termo usado em um projeto de software grande � "desceberta", o que n�o � uma
met�fora ruim. A tarefa � descobrir o que est� acontecendo em alguma coisa que n�o
foi voc� que escreveu.
Essa � uma situa��o na qual as ferramentas podem ajudar significativamente.
Os programas de pesquisa de texto, como grep, podem encontrar todas as ocorr�ncias
de nomes. Os referenciadores cruzados d�o alguma ideia sobre a estrutura do
programa. Uma exibi��o do gr�fico das chamadas de fun��o � valiosa quando n�o �
muito grande. O passo a passo de um programa uma fun��o de cada vez com um
depurador pode revelar a sequ�ncia dos eventos. Um hist�rico de revis�o do programa
pode dar algumas pistas mostrando o que foi feito no programa ao longo do tempo. As
altera��es frequentes quase sempre s�o um sinal de c�digo que n�o foi entendido ou
que est� sujeito a requisitos que mudam e, assim, potencialmente sujeitos a bugs.
�s vezes voc� precisa controlar os erros do software pelo qual n�o �
respons�vel e do qual n�o tem o c�digo-fonte. Nesse caso, a tarefa � identificar e
caracterizar o bug suficientemente bem para poder report�-lo com precis�o e, ao
mesmo tempo, encontrar um modo de "contorn�-lo" para evitar o problema.
Se voc� achar que encontrou um bug no programa de outra pessoa, a primeira
etapa � ter certeza absoluta de que ele � um bug verdadeiro, para voc� n�o precisar
desperdi�ar o tempo do autor e perder sua pr�pria credibilidade.
Quando voc� encontrar um bug de compilador, verifique se o erro est�
realmente no compilador e n�o no seu pr�prio c�digo. Por exemplo, se a C ou C++ n�o
especifica que uma opera��o right shift preenche com zero bits (logical shift) ou
propaga o bit de sinal (arithmetic shift), ent�o os novatos podem achar que esse �
um erro quando uma constru��o como

? i = -1;
? printf("%d\n", i " 1);

resulta em uma resposta inesperada. Mas essa � uma quest�o de portabilidade, porque
essa declara��o pode se comportar de forma verdadeiramente diferente em sistemas
diferentes. Tente nosso teste em v�rios sistemas e verifique se entendeu o que
acontece; verifique a defini��o da linguagem para ter certeza.
Verifique se o bug � novo. Voc� tem a vers�o mais recente do programa?
Existe uma lista de fixes dos bugs? A maior parte do software passa por v�rias
vers�es. Se voc� achar um bug na vers�o 4.0b1, ele pode ser consertado ou
substitu�do por um novo na vers�o 4.04b2. Em qualquer caso, poucos programadores
t�m muito entusiasmo para consertar bugs em qualquer coisa que n�o seja a vers�o
atual de um programa.
Finalmente, coloque-se no lugar da pessoa que vai receber o seu relat�rio.
Voc� quer dar ao propriet�rio o melhor caso de teste que puder gerenciar. N�o vai
ajudar muito se o bug s� puder ser demonstrado com entradas grandes, ou em um
ambiente elaborado, ou ainda com v�rios arquivos de suporte. Diminua o teste a um
caso m�nimo e autocontido. Inclua outras informa��es que poderiam ser relevantes,
como a vers�o do pr�prio programa, e a do compilador, sistema aperacional e
hardware. Para a vers�o com bug de isprint mencionada na Se��o 5.4, n�s poder�amos
fornecer isto como um programa de teste:

/* testa o bug de isprint no programa */


int main(void)
{
int c;

while (isprint(c = getchar()) || c != EOF)


printf("%c", c);
return 0;
}

Toda linha de texto imprim�vel servir� como um caso de teste, uma vez que a sa�da
conter� apenas metade da entrada:

% echo 1234567890 | isprint_test


24680
%

Os melhores relat�rios de bugs s�o aqueles que s� precisam de uma ou duas linhas de
entrada em um sistema simples para demonstrar a falha, e que incluam um fix. Envie
o tipo de relat�rio de bug que voc� gostaria de receber.

5.8 Resumo

Com a atitude certa a depura��o pode ser divertida, como solucionar um


quebra-cabe�a, mas, gostando ou n�o, a depura��o � uma arte que vamos praticar
regularmente. Mesmo assim, seria bom se os bugs n�o acontecessem; portanto, n�s
tentamos evit�-los escrevendo c�digo bem antes de mais nada. O c�digo bem escrito
tem menos bugs desde o in�cio e aqueles que permanecem s�o mais f�ceis de achar.
Depois que um bug � visto, a primeira coisa a fazer � pensar muito sobre as
pistas que ele apresenta. Como ele poderia ter sido criado? Isso � alguma coisa
conhecida? Alguma coisa acabou de ser mudada no programa? H� algo de especial nos
dados de entrada que o provocaram? Alguns casos de teste bem escolhidos e algumas
declara��es de impress�o no c�digo podem ser suficientes.
Se n�o houver boas pistas, a considera��o cuidadosa ainda pode ser a melhor
primeira etapa, a ser seguida pelas tentativas sistem�ticas de especificar a
localiza��o do problema. Uma etapa � cortar os dados de entrada para criar uma
entrada pequena com a falha. Outra � cortar o c�digo para eliminar as regi�es que
podem n�o estar relacionadas. � poss�vel inserir o c�digo de verifica��o que �
ativado apenas depois que o programa executou um certo n�mero de etapas, novamente
para tentar localizar o problema. Todos esses s�o casos de uma estrat�gia geral,
dividir e conquistar, a qual � t�o efetiva na depura��o quanto na pol�tica e na
guerra.
Use tamb�m outro tipo de aux�lio. Explicar seu c�digo para outra pessoa
(mesmo que seja um ursinho de pel�cia) tem uma efici�ncia maravilhosa. Use um
depurador para obter um rastreamento de pilha. Use algumas das ferramentas
comerciais que verificam os vazamentos de mem�ria, as viola��es dos limites de
array, o c�digo suspeito e outros. Fa�a o passo a passo do seu programa quando
estiver claro que voc� est� com um quadro mental errado do funcionamento do c�digo.
Conhe�a si mesmo e os tipos de erros que voc� comete. Depois de encontrar e
consertar um bug, verifique se eliminou outros bugs semelhantes. Pense no que
aconteceu para evitar que o erro seja cometido novamente.

Leitura suplementar

Os livros Writing Solid Code (Microsoft Press, 1993), de Steve Maguire, e


Code Complete (Microsoft Press, 1993), de Steve McConnell, t�m ambos bons conselhos
sobre a depura��o.
6

Testando

Na pr�tica computacional comum, manual ou por meio de m�quinas, o cliente deve


verificar cada etapa dos c�lculos, e, quando um erro � encontrado deve localiz�-lo
com um processo inverso que come�a no primeiro ponto no qual o erro foi observado.
Norbert Wiener, Cybernetics

Os termos testar e depurar quase sempre s�o utilizados na mesma frase, mas
eles n�o s�o a mesma coisa. Para simplificar, a depura��o � aquilo que voc� faz
quando sabe que um programa est� com problemas. O teste � uma tentativa determinada
e sistem�tica de quebrar um programa que voc� acha que est� funcionando.
Edsger Dijkstra � o autor da famosa observa��o de que o teste pode
demonstrar a presen�a de bugs, mas n�o sua aus�ncia. Sua esperan�a � de que os
programas possam ser corretos por constru��o, para que n�o haja erros, nem a
necessidade de testar. Embora esse seja um bom objetivo, ele ainda n�o � realista
no caso de programas substanciais. Assim sendo, neste cap�tulo vamos ver como
testar para encontrar os erros r�pida, eficiente e efetivamente.
Um bom ponto de partida � pensar nos problemas em potencial enquanto voc�
cria o c�digo. O teste sistem�tico, desde os mais f�ceis at� os mais elaborados,
ajuda a garantir que os programas come�am a vida funcionando corretamente e
permanecem corretos enquanto crescem. A automa��o ajuda a eliminar os processos
manuais e encoraja o teste mais extenso. Al�m disso, existem muitos truques do
mercado que os programadores aprenderam com a experi�ncia.
Uma maneira de escrever c�digo sem bugs � ger�-lo por meio de um programa.
Quando alguma tarefa de programa��o � t�o bem entendida que escrever o c�digo
parece algo mec�nico, ent�o ela deve ser mecanizada. Um caso comum ocorre quando um
programa pode ser gerado a partir de uma especifica��o em alguma linguagem
especializada. Por exemplo, compilamos linguagens de alto n�vel no c�digo assembly;
usamos express�es regulares para especificar os padr�es de texto; usamos nota��es
como SUM(A1:A50) para representar opera��es em um intervalo de c�lulas de uma
planilha. Em tais casos, quando o gerador ou tradutor est� correto e quando a
especifica��o tamb�m est� correta, o programa resultante tamb�m estar� correto.
Vamos falar desse rico t�pico com mais detalhes no Cap�tulo 9. Neste cap�tulo vamos
falar brevemente sobre as maneiras de criar testes a partir de especifica��es
compactas.

6.1 Teste enquanto escreve o c�digo

Quanto mais cedo um problema for encontrado, melhor. Se pensar


sistematicamente sobre aquilo que est� escrevendo enquanto escreve, voc� pode
verificar as propriedades simples do programa enquanto ele est� sendo constru�do, e
como resultado o seu c�digo ter� passado por uma rodada de testes antes mesmo de
ser compilado. Determinados tipos de bugs nunca ganham vida.

Teste o c�digo nos seus limites. Uma t�cnica � o teste da condi��o limite: � medida
que cada parte do c�digo � escrita - um loop ou uma declara��o condicional, por
exemplo - verifique imediatamente se a condi��o se ramifica na dire��o certa ou se
o loop tem o n�mero adequado de repeti��es. Esse processo � chamado de teste da
condi��o limite porque voc� est� testando os limites naturais dentro do programa e
dos dados, tais como entrada n�o-existente ou vazia, um �nico item de entrada, um
array exatamente cheio e assim por diante. A ideia � que a maioria dos bugs ocorre
nos limites. Quando um c�digo vai falhar, provavelmente ele falha em um limite. Da
mesma forma, se ele funcionar nos seus limites, ele deve funcionar em outros
lugares tamb�m.
Este fragmento, seguindo o modelo de fgets, l� os caracteres at� encontrar
uma linha nova ou encher um buffer:

? int i;
? char s[MAX];
?
? for (i = 0; (s[i] = getchar()) != '\n' && i < MAX-1; ++i)
? ;
? s[--i] = '\0';

Imagine que voc� acabou de escrever esse loop. Agora simule-o mentalmente
enquanto ele l� uma linha. O primeiro limite a ser testado � o mais simples: uma
linha vazia. Se voc� come�ar com uma linha que cont�m apenas uma �nica linha nova,
fica f�cil ver que o loop p�ra na primeira itera��o com i definido como zero, de
modo que a �ltima linha decrementa i para -1 e, assim, escreve um byte null em s[-
1] que est� antes do in�cio do array. O teste da condi��o de limite encontra o
erro.
Se reescrevermos o loop para usar o idioma convencional para preencher um
array com caracteres de entrada, ele vai ficar assim:

? for (i = 0; i < MAX-1; i++)


? if ((s[i] = getchar()) == '\n')
? break;
? s[i] = '\0';

Repetindo o teste de limite original � f�cil verificar que uma linha com
apenas uma linha nova � tratada corretamente: i � zero, o primeiro caractere de
entrada quebra o loop, e '\0' � armazenado em s[0]. Uma verifica��o semelhante nas
entradas de um e dois caracteres seguidos por uma linha nova nos d� confian�a de
que o loop funciona perto do limite.
Existem outras condi��es de limite a serem verificadas. Se a entrada
contiver uma linha longa ou n�o tiver linhas novas, isso � protegido pela
verifica��o de que i permanece menor do que MAX-1. Mas e se a entrada estiver
vazia, ent�o a primeira chamada de getchar retorna EOF? Devemos verificar isso:

? for (i = 0; i < MAX-1; i++)


? if ((s[i] = getchar()) == '\n' || s[i] == EOF)
? break;
? s[i] = '\0';

O teste da condi��o de limite pode detectar muitos bugs, mas n�o todos eles.
Vamos voltar a esse exemplo no Cap�tulo 8, onde mostraremos que ele ainda tem um
bug de portabilidade.
A pr�xima etapa � verificar a entrada no outro limite, onde o array est�
quase cheio, exatamente cheio, e al�m de cheio, particularmente quando a linha nova
chega ao mesmo tempo. N�o vamos dar os detalhes aqui, mas esse � um bom exerc�cio.
Pensar em limites nos leva � quest�o do que fazer quando o buffer fica cheio antes
de ocorrer um '\n'; essa lacuna na especifica��o deve ser solucionada cedo, e o
teste dos limites ajuda a identific�-la.
A verifica��o da condi��o de limite � efetiva para encontrar erros off-by-
one. Com pr�tica, ela se torna uma segunda natureza, e muitos bugs comuns s�o
eliminados antes mesmo de acontecerem.

Teste as pr� e p�s-condi��es. Outra forma de eliminar problemas � verificar se as


propriedades esperadas ou necess�rias mantidas antes (precondi��o) ou depois (p�s-
condi��o) de alguma parte do c�digo s�o executadas. Ter certeza de que os valores
de entrada est�o dentro do intervalo � um exemplo comum do teste de uma
precondi��o. Esta fun��o para calcular a m�dia de n elementos de um array tem um
problema quando n � menor do que ou igual a zero:
? double avg(double a[], int n)
? {
? int i;
? double sum;
?
? sum = 0.0;
? for (i = 0; i < n; i++)
? sum += a[i];
? return sum / n;
? }

O que avg faria se n fosse zero? Um array sem elementos � um conceito


significativo, embora seu valor m�dio n�o seja. avg deve deixar que o sistema pegue
a divis�o por zero? Deve abortar? Reclamar? Retornar silenciosamente algum valor
in�cuo? E se n for negativo, o que � absurdo mas n�o imposs�vel? Como sugerimos no
Cap�tulo 4, provavelmente preferir�amos retornar 0 como a m�dia se n fosse menor do
que ou igual a zero:

return n <= 0 ? 0.0 : sum/n;

mas n�o existe apenas uma �nica resposta correta.


A resposta que certamente est� errada � ignorar o problema. Um artigo de
novembro de 1998 na revista Scientific American descreve um incidente a bordo do
USS Yorktown, um cruzador guiado como um m�ssil. Um membro da tripula��o inseriu
por engano um zero em um valor de data, o que resultou em uma divis�o por zero, um
erro que em cascata eventualmente causou o fechamento do sistema de propuls�o do
navio. O Yorktown morreu na �gua durante algumas horas porque um programa n�o
verificou se a entrada era v�lida.

Use afirma��es. C e C++ fornecem um recurso de afirma��o em <assert.h> que


incentiva o acr�scimo de testes de pr� e p�s-condi��o. Como uma declara��o falha
aborta o programa, geralmente elas s�o reservadas para situa��es em que uma falha �
realmente inesperada e n�o existe recupera��o. Poder�amos aumentar o c�digo acima
com uma afirma��o antes do loop:

assert(n > 0);

Se a afirma��o for violada, ela far� o programa abortar com uma mensagem padr�o:

Assertion failed: n > 0, file avgtest.c, line 7


Abort(crash)

As afirma��es s�o particularmente �teis para validar propriedades de


interfaces porque elas chamam a aten��o para as inconsist�ncias entre quem chama e
quem � chamado e podem at� indicar de quem � a culpa. Se a afirma��o de que n �
maior do que zero falhar quando a fun��o for chamada, ela aponta o dedo para quem
chamou, em vez da pr�pria avg, como a fonte do problema. Se uma interface mudar mas
nos esquecermos de consertar alguma rotina que dependa dela, uma afirma��o pode
pegar o erro antes de ele causar problemas reais.

Programe defensivamente. Uma t�cnica �til � adicionar o c�digo para lidar com casos
"que n�o podem acontecer", as situa��es nas quais n�o � logicamente poss�vel que
alguma coisa aconte�a mas (devido a alguma falha em outra parte) que de alguma
forma pode acontecer. Um exemplo seria adicionar um teste para os comprimentos de
array zero ou negativos de avg. Outro exemplo seria um programa que processasse
notas esperar que n�o houvesse valores negativos ou enormes, mas que os testasse
mesmo assim:

if (grade < 0 || grade > 100) /* n�o pode acontecer */


letter = '?';
else if (grade >= 90)
letter = 'A';
else
...

Este � um exemplo de programa��o defensiva: verificar se um programa se


protege contra o uso incorreto ou os dados ilegais. Os ponteiros null, subscritos
fora do intervalo, divis�o por zero e outros erros podem ser detectados cedo e
avisados ou desviados. A programa��o defensiva tamb�m poderia ter pego o problema
de divis�o por zero do Yorktown.

Verifique o retorno de erro. Uma defesa quase sempre desprezada � verificar os


retornos de erro das fun��es de biblioteca e chamadas de sistema. Os erros dos
valores de retorno das rotinas de entrada, tais como fread e fscanf, sempre devem
ser verificados, assim como toda chamada de abertura de arquivo, tal como fopen. Se
uma leitura ou abertura de arquivo falhar, o c�lculo n�o poder� continuar
corretamente.
A verifica��o do c�digo de retorno das fun��es de sa�da, tais como fprintf
ou fwrite pegar� o erro que resulta da tentativa de gravar um arquivo quando n�o h�
mais espa�o em disco. Talvez seja suficiente verificar o valor de retorno de
fclose, o qual retorna EOF se algum erro ocorreu durante alguma opera��o, e zero no
caso contr�rio.

fp = fopen(outfile, "w");
while (...) /* grava a sa�da em outfile */
fprintf(fp, ...);
if (fclose(fp) == EOF) { /* algum erro? */
/* algum erro de entrada ocorreu */
}

Os erros de sa�da podem ser s�rios. Se o arquivo que est� sendo gravado for
a vers�o nova de um arquivo precioso, essa verifica��o evitar� que voc� tenha de
remover o arquivo antigo se o novo n�o foi gravado com sucesso.

O esfor�o de testar o c�digo enquanto o escreve � m�nimo e tem uma


compensa��o enorme. Pense em testar enquanto escreve um programa para mais tarde
ter um c�digo melhor, porque ent�o voc� saber� melhor o que o c�digo deveria fazer.
Se esperar at� que alguma coisa quebre, voc� precisar� descobrir tudo de novo, o
que leva tempo, e os fixes ser�o menos completos e mais fr�geis porque seu
entendimento renovado provavelmente ser� incompleto.

� Exerc�cio 6-1. Verifique estes exemplos em seus limites, depois conserte-os


conforme a necessidade e de acordo com os princ�pios de estilo do Cap�tulo 1 e os
conselhos dados neste cap�tulo.
(a) Este exemplo deve calcular fatoriais:

? int factorial(int n)
? {
? int fac;
? fac = 1;
? while (n--)
? fac *= n;
? return fac;
? }

(b) Este deve imprimir cada um dos caracteres de uma string em uma linha:

? i = 0;
? do {
? putchar(s[i++]);
? putchar('\n');
? } while (s[i] != '\0');

(c) Este exemplo deve copiar uma string da origem no destino:

? void strcpy(char *dest, char *src)


? {
? int i;
?
? for (i = 0; src[i] != '\0'; i++)
? dest[i] = src[i];
? }

(d) Outra c�pia de string que tenta copiar n caracteres de s para t:

? void strncpy(char *t, char *s, int n)


? {
? while (n > O && *s != '\0') {
? *t = *s;
? t++;
? s++;
? n-;
? }
? }

(e) Uma compara��o num�rica:

? if (i > j)
? printf("%d is greater than %d.\n", i, j);
? else
? printf("%d is smaller than %d.\n", i, j);

(f) Um teste de classe de caractere:

? if (c >= 'A' && c <= 'Z') {


? if (c <= 'L')
? cout " "primeira metade do alfabeto";
? else
? cout " "segunda metade do alfabeto";
? }
?

� Exerc�cio 6-2. Estamos escrevendo este livro no final de 1998, e o problema do


Ano 2000 talvez seja o maior problema de condi��o de limite que j� existiu.
(a) Quais datas voc� deve usar para verificar se um sistema pode funcionar no ano
2000? Supondo que os testes sejam de execu��o cara, em qual ordem voc� realizaria
seus testes ap�s tentar o pr�prio 1o. de janeiro de 2000?
(b) Como voc� testaria a fun��o padr�o ctime, a qual retorna uma representa��o de
string da data na seguinte forma:

Fri Dec 31 23:58:27 EST 1999\n\0

Suponhamos que o seu programa chame ctime. Como voc� escreveria seu c�digo para
defender-se contra uma implementa��o falha?
(c) Descreva como voc� testaria um programa de calend�rio que imprime uma sa�da
como esta:
January 2000
SMTuWThFS12345678910111213141516171819202122232425262728293031
(d) Quais outros limites de tempo voc� consegue descobrir nos sistemas que usa, e
como voc� testaria para ver se eles s�o tratados corretamente?
?

6.2 Teste sistem�tico

� importante testar um programa sistematicamente para saber em cada etapa o


que voc� est� testando e quais s�o os resultados esperados. Voc� precisa ser
organizado para n�o ignorar nada, e deve manter os registros para saber quanto fez.

Teste incrementalmente. O teste deve andar lado a lado com a constru��o do


programa. � muito mais dif�cil e demorado escrever um programa inteiro e depois
test�-lo de uma s� vez do que adotar uma abordagem incremental. Escreva parte de um
programa, teste-a, adicione mais c�digo, teste-o e assim por diante. Se voc� tem
dois pacotes que foram escritos e testados independentemente, teste para ver se
eles funcionam juntos quando voc� finalmente os conectar.
Por exemplo, quando est�vamos testando os programas CSV do Cap�tulo 4, a
primeira etapa foi escrever o c�digo suficiente para ler a entrada; isso nos
permitiu validar o processamento da entrada. A pr�xima etapa foi dividir as linhas
de entrada em v�rgulas. Depois que essas partes estavam funcionando, passamos para
os campos com aspas, e depois gradualmente testamos tudo.

Teste primeiro as partes simples. A abordagem incremental tamb�m se aplica ao modo


como voc� testa os recursos. Os testes devem focalizar primeiro os recursos mais
simples e executados de um programa. Voc� s� deve continuar quando eles estiverem
funcionando adequadamente. Dessa forma, em cada est�gio, voc� exp�e mais material
para o teste e aumenta a sua confian�a de que os mecanismos b�sicos est�o
funcionando corretamente. Os testes f�ceis encontram os bugs f�ceis. Cada teste faz
o m�nimo para investigar o pr�ximo problema em potencial. Embora cada bug seja mais
dif�cil de descobrir do que o bug anterior, ele n�o precisa ser necessariamente
mais dif�cil de consertar.
Nesta se��o, vamos falar sobre as maneiras de selecionar testes efetivos e
em qual ordem eles devem ser aplicados; nas duas se��es seguintes, vamos falar
sobre como mecanizar o processo para que ele seja executado de forma eficiente. A
primeira etapa, pelo menos no caso dos programas pequenos ou das fun��es
individuais, � uma extens�o do teste da condi��o de limite que descrevemos na se��o
anterior: o teste sistem�tico dos casos pequenos.
Suponhamos que voc� tenha uma fun��o que executa a pesquisa bin�ria em um
array de inteiros. Come�ar�amos com estes testes, organizados na ordem crescente de
complexidade:
� Pesquise um array sem elementos
� Pesquise um array com um elemento e um valor comum que �:
- menor do que o elemento �nico do array
- igual ao elemento �nico
- maior do que o elemento �nico
� Pesquise um array com dois elementos e valores experimentais que:
- verificam todas as cinco posi��es poss�veis
� Verifique o comportamento com elementos em duplicata no array e valores
experimentais:
- menores do que o valor do array
- iguais ao valor
- maiores do que o valor
� Pesquise um array com tr�s elementos assim como o de dois elementos
� Pesquise um array com quatro elementos assim como o de dois e tr�s
elementos
Se a fun��o passar ilesa, � prov�vel que ela esteja em boas condi��es, mas
ainda poderia ser mais testada.
Esse conjunto de testes � suficientemente pequeno para ser executado � m�o,
mas � melhor criar um andaime de testes para mecanizar o processo. O seguinte
programa de driver � t�o simples quanto aquilo que podemos gerenciar. Ele l� as
linhas de entrada contendo uma chave a ser pesquisada e um tamanho de array; ele
cria um array daquele tamanho contendo os valores l, 3, 5, ... e pesquisa a chave
no array.

/* bintest main: andaime para testar binsearch */


int main(void)
{
int i, key, nelem, arr[1000];

while (scanf("%d %d", &key, &nelem) != EOF) {


for (i = 0; i < nelem; i++)
arr[i] = 2*i + 1;
printf("%d\n", binsearch(key, arr, nelem));
}
return 0;
}

Isso � simples mas mostra que um andaime de teste �til n�o precisa ser
grande, e ele pode ser facilmente estendido para executar mais desses testes e
exibir menos interven��o manual.

Saiba qual sa�da deve esperar. Para todos os testes, � preciso conhecer a resposta
certa. Se voc� n�o souber a resposta certa estar� perdendo seu tempo. Isso pode
parecer �bvio, pois para muitos programas � f�cil saber se o programa est�
funcionando. Por exemplo, se a c�pia de um arquivo � ou n�o � uma c�pia; se a sa�da
de uma classifica��o est� ou n�o classificada, ela tamb�m deve ser uma permuta da
entrada original.
A maioria dos programas � mais dif�cil de ser caracterizada - os
compiladores (a sa�da traduz adequadamente a entrada?), os algoritmos num�ricos (a
resposta est� dentro da toler�ncia a erros?), os gr�ficos (os pixels est�o nos
lugares certos?) e assim por diante. Nesses casos � particularmente importante
validar a sa�da por meio da compara��o com valores conhecidos.
� Para testar um compilador, compile e execute os arquivos de teste. Os
programas de teste, por sua vez, devem gerar sa�da, e seus resultados devem ser
comparados �queles conhecidos.
� Para testar um programa num�rico, gere casos de teste que exploram os
lados do algoritmo, casos f�ceis e tamb�m dif�ceis. Onde for poss�vel, escreva
c�digo para verificar quais propriedades de sa�da est�o sadias. Por exemplo, a
continuidade da sa�da de um integrador num�rico pode ser testada, assim como sua
concord�ncia com as solu��es de forma fechada.
� Para testar um programa gr�fico, n�o basta ver se ele pode desenhar uma
caixa. Em vez disso � preciso ler a caixa novamente pela tela e verificar se os
lados est�o exatamente onde deveriam estar.
Se o programa tiver um inverso, verifique se seu aplicativo recupera a
entrada. A criptografia e a decriptografia s�o inversas; portanto, se voc�
criptografar alguma coisa e n�o conseguir decriptograf�-la, algo est� errado. Da
mesma forma, a compacta��o sem perda e os algoritmos de expans�o devem ser
inversos. Os programas que juntam arquivos devem extra�-los sem altera��es. �s
vezes podem existir v�rios m�todos para a invers�o; portanto, verifique todas as
combina��es.

Verifique as propriedades de conserva��o. Muitos programas preservam algumas


propriedades de suas entradas. Ferramentas como o wc (conta linhas, palavras e
caracteres) e sum (calcula um checksum) podem verificar se as sa�das t�m o mesmo
tamanho, o mesmo n�mero de palavras, os mesmos bytes na mesma ordem, e assim por
diante. Outros programas comparam a identidade de arquivos (cmp) ou reportam
diferen�as (diff). Esses programas ou outros semelhantes est�o dispon�veis para a
maioria dos ambientes e vale a pena adquiri-los.
Um programa de frequ�ncia de bytes pode ser usado para verificar a
conserva��o dos dados e tamb�m detectar anomalias como caracteres n�o-texto em
arquivos que supostamente deveriam ser apenas texto. Aqui temos a vers�o daquilo
que chamamos de freq:

#include <stdio.h>
#include <ctype.h>
#include <limits.h>

unsigned long count[UCHAR_MAX+l];

/* freq main: exibe as contagens de frequ�ncia de bytes */


int main(void)
{
int c;
while ((c = getchar()) != EOF)
count[c]++;
for (c = 0; c <= UCHAR_MAX; c++)
if (count[c] != 0)
printf("%.2x %c %lu\n",
c, isprint(c) ? c : '-', count[c]);
return 0;
}

As propriedades de conserva��o tamb�m podem ser verificadas dentro de un


programa. Uma fun��o que conta os elementos de uma estrutura de dados fornece uma
verifica��o comum de consist�ncia. Uma tabela hash deve ter a propriedade de fazer
com que todo elemento nela inserido possa ser recuperado. Essa condi��o � f�cil de
verificar com uma fun��o que faz o dump do conte�do da tabela para un arquivo ou
array. A qualquer momento, o n�mero de inser��es em uma estrutura de dados menos o
n�mero de exclus�es deve ser igual ao n�mero de elemento contidos, uma condi��o
f�cil de verificar.

Compare as implementa��es independentes. As implementa��es independentes de uma


biblioteca ou programa produzem as mesmas respostas. Por exemplo, dois compiladores
devem produzir programas que se comportam da mesma maneira na mesma m�quina, pelo
menos na maioria das situa��es.
Eventualmente uma resposta pode ser calculada de duas maneiras diferentes,
ou voc� pode escrever uma vers�o comum de um programa para usar como uma compara��o
lenta mas independente. Se dois programas n�o-relacionados obt�m as mesmas
respostas, h� boas chances de que eles estejam corretos. Se eles obtiverem
respostas diferentes, pelo menos uma delas est� errada.
Um dos autores certa vez trabalhou com outra pessoa em um compilador para
uma m�quina nova. O trabalho de depura��o do c�digo gerado pelo compilador foi
dividido: uma pessoa escreveu o software que codificava as instru��es para a
m�quina de destino, e a outra pessoa escrevia o desassemblador para o depurador.
Isso significava que n�o havia a chance de algum erro de interpreta��o ou
implementa��o do conjunto de instru��es ser duplicado entre os dois componentes.
Quando o compilador codificava errado uma instru��o, certamente o desassemblador
deveria notar. Toda a primeira sa�da do compilador foi executada no desassemblador
e verificada nos documentos impressos de depura��o do pr�prio compilador. Essa
estrat�gia funcionou bem na pr�tica, pegando instantaneamente os erros de ambas as
partes. A �nica depura��o dif�cil e demorada ocorreu quando ambas as pessoas
interpretaram uma frase amb�gua na descri��o da arquitetura da mesma maneira
incorreta.
Me�a a cobertura do teste. Um objetivo dos testes � verificar se cada declara��o de
um programa foi executada em algum instante durante a sequ�ncia de testes. O teste
n�o pode ser considerado completo, a menos que toda linha do programa tenha passado
pelo menos por um teste. A cobertura completa quase sempre � muito dif�cil de
conseguir. Mesmo deixando de lado as declara��es que "n�o podem acontecer" �
dif�cil usar as entradas normais para for�ar um programa a passar pelas declara��es
em particular.
Existem ferramentas comerciais para medir a cobertura. Os profilers, quase
sempre inclu�dos como parte das su�tes de compilador, fornecem um modo de calcular
uma contagem de frequ�ncia de declara��es para cada declara��o do programa que
indica a cobertura conseguida pelos testes espec�ficos.
N�s testamos o programa Markov, do Cap�tulo 3, com uma combina��o dessas
t�cnicas. A �ltima se��o deste cap�tulo descreve esses testes com detalhes.

� Exerc�cio 6-3. Descreva como voc� testaria freq. ?

� Exerc�cio 6-4. Crie e implemente uma vers�o de freq que me�a as freq��ncias de
outros tipos de valores de dados, tais como inteiros de 32 bits ou n�meros de ponto
flutuante. Voc� pode fazer com que uma vers�o do programa lide com uma variedade de
tipos de forma elegante? ?

6.3 Automa��o de testes

� cansativo e pouco confi�vel fazer muitos testes � m�o. O teste adequado


envolve muitos testes, muitas entradas e muitas compara��es de sa�da. O teste deve,
portanto, ser feito por programas, os quais n�o ficam cansados ou s�o descuidados.
Vale a pena perder tempo escrevendo um script ou programa comum que encapsula todos
os testes, portanto uma su�te completa de testes pode ser executada apertando um
�nico bot�o. Quanto mais f�cil for a execu��o de uma su�te de testes, maior a
chance de voc� execut�-la e menor a probabilidade de voc� deixar de execut�-la
quando o tempo for curto. Escrevemos uma su�te de testes que verifica todos os
programas que escrevemos para este livro, e que foi executada sempre que fizemos
altera��es. Partes da su�te s�o executadas automaticamente depois de cada
compila��o bem-sucedida.

Teste da regress�o autom�tica. A forma mais b�sica de automa��o � o teste da


regress�o, que executa uma seq��ncia de testes que compara a vers�o nova de alguma
coisa com a vers�o anterior. Ao consertar problemas existe a tend�ncia natural de
verificar apenas se o fix funciona. � f�cil ignorar a possibilidade de que o fix
quebrou alguma outra coisa. A inten��o do teste de regress�o � garantir que o
comportamento n�o seja alterado, exceto das maneiras esperadas.
Alguns sistemas s�o ricos em ferramentas que ajudam em tal automa��o. As
linguagens de cria��o de scripts nos permitem escrever scripts curtos para executar
sequ�ncias de testes. No Unix, os comparadores de arquivos como diff e cmp comparam
as sa�das; sort junta os elementos comuns; grep filtra as sa�das do teste; wc, sum
e freq resumem as sa�das. Juntos, eles facilitam a cria��o de andaimes de teste ad
hoc, talvez n�o em n�mero suficiente para os programas grandes, mas totalmente
adequados para a programa��o atualizada por um indiv�duo ou grupo pequeno.
Aqui temos um script para o teste de regress�o de um programa de aplicativo
chamado ka. Ele executa as vers�es antiga (old_ka) e nova (new_ka) em um n�mero
grande de arquivos de dados de teste diferentes, e reclama sobre aqueles nos quais
as sa�das n�o s�o id�nticas. Ele foi escrito para um shell Unix, mas pode ser
facilmente transcrito para a Perl ou outra linguagem de cria��o de scripts:

for i in ka_data.* # loop nos arquivos de dados de teste


do
old_ka $i >out1 # executa a vers�o antiga
new_ka $i >out2 # executa a vers�o nova
if ! cmp -s out1 out2 # compara os arquivos de sa�da
then
echo $i: BAD # diferente: imprimir mensagem de erro
fi
done

Um script de teste geralmente deveria ser executado silenciosamente,


produzindo sa�da somente quando algo inesperado ocorresse, como faz esse script.
Poder�amos ent�o optar por imprimir cada nome de arquivo enquanto ele fosse
testado, e acompanh�-lo de uma mensagem de erro quando alguma coisa estivesse
errada. Tais indica��es de progresso ajudam a identificar problemas como um loop
infinito ou um script de teste que n�o est� executando os testes certos, mas a
conversa extra � aborrecida quando os testes est�o sendo executados adequadamente.
O argumento -s faz com que cmp reporte o status mas n�o produza sa�da.
Quando os arquivos comparam da mesma forma, cmp retorna um status true, !cmp �
false, e nada � impresso. Quando as sa�das antiga e nova diferem, por�m, cmp
retorna false e o nome do arquivo e um aviso s�o impressos.
H� a suposi��o impl�cita no teste de regress�o de que a vers�o anterior do
programa calcula a resposta certa. Isso deve ser verificado cuidadosamente no
in�cio do tempo, e a constante deve ser mantida escrupulosamente. Se uma resposta
errada entrar em um teste de regress�o, ela � muito dif�cil de ser detectada e tudo
que dependa dela ficar� errado dali por diante. Uma boa pr�tica � verificar o
pr�prio teste de regress�o periodicamente para ter certeza de que ele ainda est�
v�lido.

Crie testes autocontidos. Os testes autocontidos que t�m suas pr�prias entradas e
sa�das esperadas fornecem um complemento para os testes de regress�o. Nossa
experi�ncia testando o Awk pode ser instrutiva. Muitas constru��es de linguagem s�o
testadas pela execu��o de entradas especificadas feita por pequenos programas, e
pela verifica��o de que a sa�da certa ser� produzida. A seguinte parte de uma
grande cole��o de testes diversos verifica uma express�o complicada de incremento.
Esse teste executa a nova vers�o do Awk (newawk) em um programa curto Awk para
produzir sa�da em um arquivo, grava a sa�da correia em outro arquivo com echo,
compara os arquivos e reporta um erro quando eles diferem.

# teste de incremento de campo: $i++ significa ($i)++, n�o

echo 3 5 | newawk '{i = 1; print $i++; print $1, i}' >out1


echo '3
4 1' >out2 # resposta correta

if ! cmp -s out1 out2 # as sa�das s�o diferentes


then
echo 'BAD: field increment test failed'
fi

O primeiro coment�rio faz parte da entrada do teste. Ele documenta o que


est� sendo testado.
�s vezes � poss�vel construir um n�mero grande de testes com pouco esfor�o.
Para as express�es simples, n�s criamos uma linguagem pequena e especializada para
descrever os testes, dados de entrada e sa�das esperadas. Aqui temos uma sequ�ncia
curta que testa algumas das maneiras pelas quais o valor num�rico 1 pode ser
representado no Awk:

try (if ($1 == 1) print "yes"; else print "no"}


l yes
1.0 yes
1EO yes
0.1E1 yes
10E-1 yes
01 yes
+1 yes
10E-2 no
10 no

A primeira linha � um programa a ser testado (tudo depois da palavra try).


Cada linha subsequente � um conjunto de entradas e a sa�da esperada, separados por
tabula��es. O primeiro teste diz que se o primeiro campo de entrada � 1, a sa�da
deve ser yes. Os sete primeiros testes devem sempre imprimir yes e os dois �ltimos
testes devem imprimir no.
Um programa Awk (qual mais?) converte cada teste em um programa Awk
completo, depois executa cada entrada por meio dele e compara a sa�da real com a
sa�da esperada. Ele reporta somente aqueles casos nos quais a resposta est� errada.
Mecanismos semelhantes s�o usados para testar os comandos de coincid�ncia e
substitui��o de express�es normais. Um pouco de linguagem para escrever testes
facilita a cria��o de muitos deles. O uso de um programa para escrever um programa
para testar um programa traz um resultado muito bom. (O Cap�tulo 9 fala mais sobre
essas linguagens e sobre o uso dos programas que escrevem programas.)
Em geral, existem cerca de milhares de testes para o Awk. Todo o conjunto
pode ser executado com um �nico comando, e, se tudo correr bem, nenhuma sa�da �
produzida. Sempre que um recurso � adicionado ou um bug � consertado, novos testes
s�o adicionados para verificar a opera��o correta. Sempre que um programa �
alterado, mesmo que de forma simples, toda a su�te de testes � executada. Isso leva
apenas alguns minutos. �s vezes ele pega erros totalmente inesperados, e j� salvou
os autores do Awk de muitas situa��es p�blicas embara�osas.
O que voc� faria ao descobrir um erro? Se ele n�o fosse encontrado por um
teste existente, voc� poderia criar um teste novo que descobrisse o problema e
verificasse o teste executando-o com a vers�o quebrada do c�digo. O erro poderia
sugerir outros testes ou toda uma classe nova de coisas a serem verificadas. Ou
quem sabe seria poss�vel acrescentar defesas ao programa, as quais pegariam o erro
internamente.
Nunca jogue um teste fora. Ele pode ajud�-lo a resolver se um relat�rio de
bug � v�lido ou se ele descreve alguma coisa que j� foi consertada. Mantenha um
registro dos bugs, altera��es e fixes. Esse registro o ajudar� a identificar os
problemas antigos e consertar os novos. Na maioria da produ��o comercial de
programa��o tais registros s�o obrigat�rios. No caso da sua programa��o pessoal,
eles s�o um investimento baixo com retorno repetido.

� Exerc�cio 6-5. Crie uma su�te de testes para printf, usando o m�ximo poss�vel de
aux�lios t�cnicos. ?

6.4 Andaimes de teste

Nossa discuss�o at� aqui se baseou no teste de um �nico programa isolado em


sua forma conclu�da. Esse n�o � o �nico tipo de automa��o de teste que existe. Nem
� a maneira mais prov�vel de testar partes de um programa grande durante a
constru��o, particularmente quando voc� faz parte de uma equipe. Nem � a maneira
mais efetiva de testar componentes pequenos que est�o enterrados dentro de alguma
coisa maior.
Para testar um componente isoladamente, em geral � preciso criar algum tipo
de estrutura ou andaime que forne�a suporte e interface suficientes para o restante
do sistema no qual ser� executada a parte que est� sendo testada. No in�cio deste
cap�tulo, mostramos um exemplo pequeno que testa a pesquisa bin�ria.
� f�cil construir andaimes para testar fun��es matem�ticas, fun��es de
string, rotinas de classifica��o e assim por diante, uma vez que os andaimes
provavelmente consistem em sua maior parte na configura��o de par�metros de
entrada, na chamada das fun��es a serem testadas e, depois, na verifica��o dos
resultados. A cria��o de um andaime para testar um programa parcialmente conclu�do
� uma tarefa maior.
Para ilustrar isso, vamos ver a constru��o de um teste para memset, uma das
fun��es mem... da biblioteca padr�o da C/C++. Essas fun��es quase sempre s�o
sobrepostas na linguagem assembly de uma m�quina espec�fica, uma vez que seu
desempenho � importante. Quanto mais cuidadosamente ajustadas elas estiverem,
por�m, maior a chance de que elas estejam erradas e, assim, devem ser testadas de
forma mais completa.
A primeira etapa � fornecer as vers�es mais simples poss�veis em C que
funcionam. Elas fornecem um benchmark para o desempenho e, mais importante, para a
precis�o. Para mover-se para um ambiente novo, � preciso ter vers�es simples e us�-
las at� que vers�es ajustadas estejam funcionando.
A fun��o memset(s,c,n) define n bytes de mem�ria para o byte c, come�ando no
endere�o s, e retorna s. Essa fun��o � f�cil se a velocidade n�o for importante:

/* memset: define os primeiros n bytes de s at� c */


void *memset(void *s, int c, size_t n)
{
size_t i;
char *p;

p = (char *) s;
for (i = 0; i < n; i++)
p[i] = c;
return s;
}

Mas quando a velocidade � importante s�o usados truques, tais como escrever
palavras completas de 32 ou 64 bits de cada vez. Eles podem levar a bugs e,
portanto, o teste amplo � obrigat�rio.
O teste se baseia em uma combina��o de verifica��es exaustivas e de
condi��es de limite nos prov�veis pontos de falha. Para memset, os limites incluem
os valores �bvios de n, tais como zero, um e dois, mas tamb�m os valores que s�o
pot�ncias de dois ou valores pr�ximos, incluindo os pequenos e os grandes, tais
como 216, que corresponde a um limite natural em muitas m�quinas, uma palavra de 16
bits. As pot�ncias de dois merecem aten��o porque uma maneira de tornar memset mais
r�pida � definir v�rios bytes ao mesmo tempo. Isso pode ser feito por meio de
instru��es especiais ou tentando armazenar uma palavra em vez de um byte de cada
vez. Da mesma forma, n�s queremos verificar as origens dos arrays com uma variedade
de alinhamentos, caso haja algum erro baseado no endere�o ou comprimento inicial.
Vamos colocar o array de destino dentro de um array maior, criando assim uma zona
de buffer ou margem de seguran�a em cada lado. Essa � uma maneira f�cil de variar o
alinhamento.
Tamb�m queremos verificar uma variedade de valores para c, incluindo zero,
0x7F (o maior valor signed, assumindo bytes de 8 bits), 0x80 e 0xFF (testando os
erros potenciais que envolvem caracteres signed e unsigned), e alguns valores bem
maiores do que um byte (verifique se apenas um byte � usado). Tamb�m dever�amos
inicializar a mem�ria em algum padr�o conhecido que fosse diferente de qualquer um
desses valores de caracteres para que pud�ssemos verificar se memset foi gravada
fora da �rea v�lida.
Podemos usar a implementa��o simples como um padr�o de compara��o em um
teste que aloca dois arrays, depois compara os comportamentos em combina��es de n,
c e faz o deslocamento dentro do array:

big = maximum left margin + maximum n + maximum right margin


s0 = malloc(big)
s1 = malloc(big)
for each combination of test parameters n, c, and offset:
set all of s0 and s1 to known pattern
run slow memset(s0 + offset, c, n)
run fast memset(s1 + offset, c, n)
check return values
compare all of s0 and s1 byte by byte

Um erro que faz com que memset seja gravada fora dos limites de seu array
provavelmente afeta os bytes pr�ximos do in�cio ou final do array, de modo que
deixar uma zona de buffer facilita a visualiza��o dos bytes danificados, e torna
menos prov�vel um erro que sobreponha alguma outra parte do programa. Para
verificar a grava��o fora dos limites, comparamos todos os bytes de s0 e s1, e n�o
apenas os bytes n que deveriam ter sido gravados.
Assim sendo, um conjunto razo�vel de testes incluiria todas as combina��es
de:

offset = 10, 11.....20


c = 0, 1, Ox7F, 0x80, OxFF, 0x11223344
n = 0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17,
31, 32, 33, ... , 65535, 65536, 65537

Os valores de n incluiriam pelo menos 2i-l, 2i e 2i+1 para i de 0 a 16.


Esses valores n�o devem aparecer na parte principal do andaime de teste, mas
devem aparecer em arrays que poderiam ser criados � m�o ou com um programa. A sua
gera��o autom�tica � melhor, isso facilita a especifica��o de mais pot�ncias de
dois ou a inclus�o de mais deslocamentos e mais caracteres.
Esses testes dar�o a memset um exerc�cio completo e levam muito pouco tempo
mesmo para serem criados, sem contar a sua execu��o, uma vez que h� menos de 3.500
casos para os valores acima. Os testes s�o completamente port�veis; portanto, eles
podem ser levados para um ambiente novo sempre que preciso.
Como um aviso vamos considerar a seguinte hist�ria. Certa vez demos uma
c�pia de um teste de memset para algu�m que estava desenvolvendo um sistema
operacional e bibliotecas para um processador novo. Meses depois, n�s (os autores
do teste original) come�amos a usar a m�quina e tivemos um aplicativo grande que
falhou em sua su�te de testes. Atribu�mos o problema a um bug sutil que envolvia a
extens�o de sinal na implementa��o da linguagem assembly para memset. Por motivos
desconhecidos, o implementador da biblioteca havia alterado o testador de memset de
modo que ele n�o verificou os valores de c acima de 0x7F. � claro que o bug foi
isolado pela execu��o do testador original depois que percebemos que memset estava
sob suspeita.
Fun��es como memset s�o suscet�veis a testes exaustivos porque elas s�o
suficientemente simples para que se prove que os casos de teste exercitem todos os
caminhos poss�veis de execu��o pelo c�digo, dando assim cobertura completa. Por
exemplo, � poss�vel testar memmove para todas as combina��es de sobreposi��o,
dire��o e alinhamento. Isso n�o � exaustivo no sentido de testar todas as opera��es
de c�pia poss�veis, mas � um teste exaustivo dos representantes de cada tipo de
situa��o distinta de entrada.
Assim como acontece em qualquer m�todo de testes, os andaimes de teste
precisam da resposta correta para verificar as opera��es que est�o testando. Uma
t�cnica importante, a qual usamos no teste de memset, � a compara��o de uma vers�o
simples que se acredita ser correta com rela��o a uma vers�o nova que pode estar
incorreta. Isso pode ser feito em est�gios, como mostra o exemplo a seguir.
Um dos autores implementou uma biblioteca de gr�ficos de varredura que
envolvia um operador que copiava blocos de pixels de uma imagem para outra.
Dependendo dos par�metros, a opera��o poderia ser uma simples c�pia de mem�ria, ou
ela poderia exigir a convers�o de valores de pixel de um espa�o de cor para outro,
ou poderia requerer o "lado a lado", onde a entrada � copiada repetidamente em toda
uma �rea retangular, ou em uma combina��o desses e de outros recursos. A
especifica��o do operador foi simples, mas uma implementa��o eficiente exigiria
muito c�digo especial em muitos casos. Para ter certeza de que todo o c�digo estava
certo precis�vamos de uma estrat�gia de testes s�lida.
Em primeiro lugar, o c�digo simples foi escrito � m�o para executar a
opera��o correta em um �nico pixel. Isso foi usado para testar o tratamento dado
pela vers�o da biblioteca a um �nico pixel. Depois que esse est�gio estava
funcionando, a biblioteca podia ser confi�vel para opera��es de um �nico pixel.
A seguir, o c�digo escrito � m�o usou a biblioteca um pixel de cada vez para
construir uma vers�o bastante lenta do operador que funcionava em uma �nica linha
horizontal de pixels, e que foi comparada ao tratamento bem mais eficiente que a
biblioteca dava para uma linha. Com isso funcionando, a biblioteca era confi�vel
para linhas horizontais.
Essa seq��ncia continuou usando linhas para construir ret�ngulos, ret�ngulos
para construir "ladrilhos", e assim por diante. Nesse meio tempo, muitos bugs foram
encontrados, incluindo alguns no pr�prio testador, mas isso faz parte da efici�ncia
do m�todo: est�vamos testando duas implementa��es independentes e, ao mesmo tempo,
construindo a confian�a em ambos. Se um teste falhava, o testador imprimia uma
an�lise detalhada para auxiliar na compreens�o do que estava errado, e tamb�m para
verificar se o testador em si estava funcionando bem.
Como a biblioteca foi modificada e portada ao longo dos anos, o testador
continuava sendo valioso para encontrar os bugs.
Devido � sua abordagem de camada por camada, esse testador precisava ser
executado desde o in�cio todas as vezes, a fim de verificar sua pr�pria confian�a
na biblioteca. Por acaso, o testador n�o era exaustivo, mas sim probabil�stico: ele
gerava casos de teste aleat�rios, os quais em execu��es suficientemente longas
eventualmente exploravam cada parte do c�digo. Com o n�mero imenso de poss�veis
casos de teste essa estrat�gia era mais eficiente do que tentar construir um teste
completo � m�o, e muito mais eficiente do que o teste exaustivo.

� Exerc�cio 6-6. Crie o andaime de teste para memset conforme indicamos acima. ?

� Exerc�cio 6-7. Crie testes para o restante da fam�lia mem... ?

� Exerc�cio 6-8. Especifique um regime de testes para rotinas num�ricas como sqrt,
sin e assim por diante, como no caso de match.h. Quais valores de entrada fazem
sentido? Quais verifica��es independentes podem ser executadas? ?

� Exerc�cio 6-9. Defina mecanismos para testar as fun��es da fam�lia C str..., como
strcmp. Algumas dessas fun��es, particularmente os tokenizers como strtok e
strcspn, s�o particularmente mais complicadas do que a fam�lia mem...; portanto,
ser�o necess�rios testes mais sofisticados. ?

6.5 Testes de carga

Outra t�cnica efetiva de testes s�o os volumes altos de entrada gerada por
m�quina. A entrada gerada por m�quina estressa os programas de forma diferente da
entrada escrita pelas pessoas. O volume mais alto em si tende a dividir as coisas
porque entradas muito grandes causam o overflow dos buffers de entrada, arrays e
contadores, e s�o efetivas para encontrar armazenamento n�o-verificado de tamanho
fixo dentro de um programa. As pessoas tendem a evitar os casos 64 "imposs�veis"
como entradas vazias ou entrada que esteja fora de ordem ou fora de intervalo, e
provavelmente n�o criam nomes muito longos ou valores de dados imensos. Os
computadores, ao contr�rio, produzem sa�da estritamente segundo seus programas e
n�o t�m a menor id�ia do que deve ser evitado.
Para ilustrar, aqui temos uma �nica linha de sa�da produzida pelo compilador
Microsoft Visual C++ Vers�o 5.0 durante a compila��o da implementa��o C++ STL da
markov; n�s editamos a linha para que ela se ajustasse:

xtree(114) : warning C4786: 'std::_Tree<std::deque<std::


basic_string<char,std::char_traits<char>,std::allocator
<char>>,std::allocator<std::basic_string<char,std::
... 1420 characters omitted
allocator<char>>>>>>::iterator' : identifier was
truncated to '255' characters in the debug Information

O compilador est� avisando que ele gerou um nome de vari�vel que tem 1.594
caracteres de comprimento, mas apenas 255 caracteres t�m de ser preservados como
informa��es de depura��o. Nem todos os programas se defendem contra tais strings
extraordinariamente longas.
As entradas aleat�rias (n�o necessariamente legais) s�o outra forma de
atacar um programa na esperan�a de dividir alguma coisa. Essa � uma extens�o l�gica
do racioc�nio "pessoas n�o fazem isso". Por exemplo, alguns compiladores comerciais
em C s�o testados com programas gerados aleatoriamente mas sintaticamente v�lidos.
O truque � usar a especifica��o do problema - neste caso, o padr�o C - para
orientar um programa que produz dados de teste v�lidos, mas estranhos.
Tais testes dependem da detec��o por parte de verifica��es incorporadas e
defesas no programa, uma vez que talvez n�o seja poss�vel verificar se o programa
est� produzindo a sa�da certa. O objetivo � mais provocar uma pane ou um "n�o �
poss�vel" do que descobrir os erros diretos. Essa tamb�m � uma boa maneira de
testar se o c�digo de tratamento de erros funciona. Com entrada sens�vel, a maioria
dos erros n�o acontece e o c�digo para lidar com eles n�o � exercido. Por natureza,
os bugs tendem a se ocultar nesses cantos. Em algum ponto, por�m, esse tipo de
teste consegue retornos menores: ele encontra problemas que s�o t�o improv�veis na
vida real que n�o valem a pena serem consertados.
Alguns testes se baseiam em entradas explicitamente maliciosas. Os ataques �
seguran�a quase sempre usam entradas grandes ou ilegais que sobrep�em dados
preciosos. � bom procurar esses pontos fracos. Algumas fun��es padr�o de biblioteca
s�o vulner�veis a esse tipo de ataque. Por exemplo, a fun��o padr�o de biblioteca
gets n�o fornece uma maneira de limitar o tamanho de uma linha de entrada, portanto
ela nunca deve ser usada. No seu lugar, use sempre fgets(buf,sizeof(buf),stdin).
Uma simples scanf("%s",buf) tamb�m n�o limita o comprimento de uma linha de
entrada; portanto, ela deve ser usada com um comprimento espec�fico, tal como
scanf("%20s",buf). Na Se��o 3.3 mostramos como abordar esse problema para um
tamanho de buffer geral.
Qualquer rotina que pode receber valores de fora do programa, direta ou
indiretamente, deve validar seus valores de entrada antes de us�-los. O seguinte
programa tirado de um livro de texto deve ler um inteiro digitado por um usu�rio
e avisar quando o inteiro for muito longo. Seu objetivo � demonstrar como superar o
problema de gets, mas a solu��o nem sempre funciona.

? #define MAXNUM 10
?
? int main(void)
? {
? char num[MAXNUM];
?
? memset(num, 0, sizeof(num));
? printf("Type a number: ");
? gets(num);
? if (num[MAXNUM-1] != 0)
? printf("Number too big.\n");
? /* ... */
? }

Quando o n�mero de entrada tem dez d�gitos de comprimento, ele sobrep�e o �ltimo
zero do array num com um valor n�o-zero e, em teoria, isso ser� detectado depois do
retorno de gets. Infelizmente, isso n�o � suficiente. Um invasor mal-intencionado
pode fornecer uma string de entrada at� mais longa que sobreponha algum valor
cr�tico, talvez o endere�o de retorno da chamada, para que o programa nunca retorne
para a declara��o if, mas sim execute algo prejudicial. Assim sendo, esse tipo de
entrada n�o-verificada � um problema de seguran�a em potencial.
Para que voc� n�o ache que esse � um exemplo irrelevante de um livro de
texto, aqui temos outro exemplo. Em julho de 1998 um erro desse tipo foi descoberto
em v�rios programas importantes de correio eletr�nico. Como reportou o New York
Times:
A falha de seguran�a � causada por aquilo que � conhecido como o "erro de overflow
de buffer" Os programadores devem incluir c�digo em seu software para verificar se
os dados recebidos s�o de um tipo seguro e se as unidades que est�o chegando t�m o
comprimento certo. Quando uma unidade de dados � muito longa, ela pode exceder o
"buffer" - a parte de mem�ria separada para cont�-la. Nesse caso, o programa de e-
mail d� pane, e um programador hostil pode enganar o computador para executar um
programa invasor em seu lugar.
Esse tamb�m foi um dos ataques do famoso incidente "Internet Worm", de 1998.
Os programas que analisam formul�rios HTML tamb�m podem ficar vulner�veis
aos ataques que armazenam strings de entrada muito longas em arrays pequenos:

? static char query[1024];


?
? char *read_form(void)
? {
? int qsize;
?
? qsize = atoi(getenv("CONTENT_LENGTH"));
? fread(query, qsize, 1, stdin);
? return query;
? }

O c�digo assume que a entrada nunca ter� mais do que 1.024 bytes de
comprimento, portanto, assim como em gets, ela est� aberta para um ataque que
exceda seu buffer.
Os tipos mais conhecidos de overflow tamb�m podem causar problemas. Quando
os inteiros fazem o overflow silenciosamente, o resultado pode ser desastroso. Veja
esta aloca��o

? char *p;
? p = (char *) malloc(x * y * z);

Quando o produto de x, y e z faz o overflow, a chamada a malloc pode


produzir um array de tamanho razo�vel, mas p[x] pode se referir a mem�ria fora da
regi�o de aloca��o. Suponhamos que ints tenha 16 bits e x, y, e z tenham 41 cada
um. Ent�o, x*y*z � 68.921, o que � 3.385 m�dulo 216. Assim sendo, a chamada de
malloc aloca somente 3.385 bytes. Toda refer�ncia com um subscrito al�m desse valor
estar� fora do limite.
A convers�o entre tipos � outra fonte de overflow, e pegar o erro talvez n�o
seja suficiente. O foguete Ariane 5 explodiu em seu primeiro v�o em junho de 1996
porque o pacote de navega��o foi herdado do Ariane 4 sem testes apropriados. O novo
foguete voava mais r�pido, o que resultou em valores maiores para algumas vari�veis
do software de navega��o. Logo ap�s o lan�amento, uma tentativa de converter um
n�mero de ponto flutuante de 64 bits para um inteiro signed de 16 bits gerou um
overflow. O erro foi pego, mas o c�digo que o pegou preferiu fechar o subsistema. O
foguete saiu do curso e explodiu. Infelizmente, o c�digo que falhou gerava
informa��es de refer�ncia inercial que s� eram �teis antes da decolagem. Se ele
estivesse desligado no momento do lan�amento n�o haveria problemas.
Em um n�vel mais mundano, as entradas bin�rias �s vezes quebram programas
que esperam entradas de texto, particularmente quando eles assumem que a entrada
est� no conjunto de caracteres ASCII de 7 bits. � instrutivo e �s vezes simples
passar uma entrada bin�ria (tal como um programa compilado) para um programa
desconhecido que espera entrada de texto.
Os bons casos de teste quase sempre podem ser usados com uma variedade de
programas. Por exemplo, qualquer programa que l� arquivos deve ser testado em um
arquivo vazio. Qualquer programa que l� texto deve ser testado em arquivos
bin�rios. Todo programa que l� linhas de texto deve ser testado em linhas imensas e
linhas vazias e entrada sem nenhum caractere de newline. � bom manter uma cole��o
desses arquivos � m�o, para poder testar qualquer programa com eles sem ter de
recriar os testes. Ou ent�o escrever um programa para criar arquivos de teste sob
demanda.
Quando Steve Bourne estava escrevendo seu shell do Unix (que veio a ser
conhecido como o Bourne shell), ele criou um diret�rio de 254 arquivos com nomes de
um caractere, um para cada valor de byte exceto ' \0' e a barra, os dois caracteres
que n�o podem aparecer nos nomes de arquivo do Unix. Ele usou esse diret�rio para
todos os tipos de testes de coincid�ncia de padr�o e tokeniza��o. (Obviamente, o
diret�rio de testes foi criado por um programa.) Anos depois, aquele diret�rio se
tornou a ru�na dos programas que percorrem �rvores de arquivos, porque ele os
testava para a destrui��o.

� Exerc�cio 6-10. Tente criar um arquivo que d� pane em seu editor de texto,
compilador ou outro programa preferido. ?

6.6 Dicas para testes

Os testadores experientes usam muitos truques e t�cnicas para tornar seu


trabalho mais produtivo. Esta se��o inclui alguns de nossos preferidos.
Os programas devem verificar os limites de array (quando a linguagem n�o faz
isso por eles), mas o c�digo de verifica��o n�o deve ser testado quando os tamanhos
de array s�o grandes comparados � entrada t�pica. Para exercer as verifica��es,
torne temporariamente os tamanhos de array bem pequenos, o que � mais f�cil do que
criar casos de teste grandes. Usamos um truque relacionado no c�digo de aumento de
array, do Cap�tulo 2, e na biblioteca CSV, do Cap�tulo 4. Na verdade, n�s deixamos
os valores iniciais pequenos no lugar, uma vez que o custo inicial adicional �
desprez�vel.
Fa�a com que a fun��o hash retorne uma constante e todo elemento seja
instalado no mesmo hash bucket. Isso vai exercer o mecanismo de encadeamento e
tamb�m fornece uma indica��o do desempenho de pior caso.
Escreva uma vers�o do seu alocador de armazenamento que falhe
intencionalmente no in�cio, para testar seu c�digo de recupera��o de erros de falta
de mem�ria. Esta vers�o retorna NULL ap�s dez chamadas:

/* testmalloc: retorna NULL ap�s 10 chamadas */


void *testmalloc(size_t n)
{
static int count = 0;

if (++count > 10)


return NULL;
else
return malloc(n);
}

Antes de enviar o seu c�digo, desative as limita��es de teste que afetar�o o


desempenho. Certa vez detectamos um problema de desempenho em um compilador de
produ��o para uma fun��o hash que sempre retornava zero porque o c�digo fora
deixado instalado.
Inicialize os arrays e as vari�veis com algum valor distinto, em vez do
padr�o normal de zero. Depois, se voc� fizer um acesso fora do limite ou pegar uma
vari�vel n�o-inicializada, tem mais chance de notar isso. A constante 0xDEADBEEF �
f�cil de reconhecer em um depurador. �s vezes os alocadores usam tais valores para
ajudar a pegar dados n�o- inicializados.
Varie seus casos de teste, particularmente ao fazer testes pequenos � m�o -
� f�cil entrar na rotina quando se testa sempre a mesma coisa, e assim voc� pode
n�o perceber que algo quebrou.
N�o fique implementando recursos novos ou testando os recursos existentes
quando h� bugs conhecidos. Isso poderia afetar os resultados do teste.
A sa�da do teste deve incluir todas as defini��es de par�metro de entrada,
portanto os testes podem ser reproduzidos de forma exata. Caso seu programa use
n�meros aleat�rios, tenha uma maneira de definir e imprimir a semente inicial,
independente dos testes em si serem aleat�rios. Verifique se as entradas de teste e
as sa�das correspondentes est�o identificadas adequadamente, para que sejam
entendidas e reproduzidas.
Tamb�m � bom fornecer maneiras de tornar a quantidade e o tipo da sa�da
control�vel quando um programa � executado. A sa�da extra pode ajudar durante os
testes.
Fa�a o teste em v�rias m�quinas, compiladores e sistemas operacionais. Cada
combina��o revela potencialmente os erros que n�o ser�o vistos em outros, tais como
as depend�ncias de ordem de byte, tamanhos de inteiros, tratamento de ponteiros
nulos, tratamento do retorno de carro e newline e propriedades espec�ficas de
bibliotecas e arquivos header. O teste em v�rias m�quinas tamb�m descobre problemas
na coleta de componentes de um programa para envio e, como discutiremos no Cap�tulo
8, pode revelar depend�ncias involunt�rias do ambiente de desenvolvimento.
Vamos discutir o teste de desempenho no Cap�tulo 7.

6.7 Quem faz o teste?

O teste � feito pelo implementador ou por outra pessoa que tenha acesso ao
c�digo-fonte, e, �s vezes, � chamado de teste da caixa branca. (O termo � uma
analogia ruim com o teste da caixa preta, onde o testador n�o sabe como o
componente foi implementado; "caixa limpa" poderia ser um termo mais inspirador.) �
importante testar seu pr�prio c�digo: n�o suponha que alguma organiza��o de testes
ou usu�rio encontrar�o as coisas para voc�. Mas � f�cil se iludir sobre o cuidado
que est� tomando com os seus testes, portanto tente ignorar o c�digo e pensar nos
casos dif�ceis e n�o nos casos f�ceis. Para citar Don Knuth ao descrever como ele
cria os testes para o formatador TEX, "eu entro no estado de esp�rito mais maldoso
e desagrad�vel que consigo, e escrevo o c�digo [de teste] mais desagrad�vel que
consigo. Depois volto e incorporo isso em constru��es mais desagrad�veis ainda, as
quais s�o quase obscenas". O motivo de testar � encontrar bugs, n�o declarar que o
programa est� funcionando. Assim sendo, os testes devem ser r�gidos, e quando eles
encontram problemas, isso significa que os seus m�todos est�o reclamando, n�o um
motivo de alarme.
O teste da caixa preta significa que o testador n�o tem conhecimento ou
acesso ao funcionamento interno do c�digo. Ele encontra tipos diferentes de erros,
porque o testador tem suposi��es diferentes sobre o lugar onde procurar. As
condi��es de limite s�o um bom local para come�ar o teste da caixa preta. Entradas
de volume alto, perversas e ilegais s�o bons candidatos. Obviamente voc� tamb�m
deve testar os usos comuns ou convencionais do programa para verificar a
funcionalidade b�sica.
Os verdadeiros usu�rios s�o a pr�xima etapa. Os novos usu�rios encontram
bugs novos, porque eles testam o programa de formas inesperadas. � importante
realizar esse tipo de teste antes que o programa seja lan�ado no mundo, embora,
infelizmente, muitos programas sejam vendidos sem nenhum tipo de teste. As vers�es
beta de software s�o uma tentativa de fazer com que in�meros usu�rios reais testem
um programa antes de ele ser finalizado, mas elas n�o devem ser usadas como
substituto para um teste completo. � medida que os sistemas de software ficam cada
vez maiores e complexos, e que os cronogramas de desenvolvimento ficam mais curtos,
por�m, a press�o para vender sem testes adequados aumenta.
� dif�cil testar programas interativos, particularmente quando eles envolvem
entrada do mouse. Parte do teste pode ser feita pelos scripts (cujas propriedades
dependem da linguagem, do ambiente e outros). Os programas interativos devem poder
ser controlados pelos scripts que simulam comportamentos do usu�rio, para que eles
possam ser testados pelos programas. Uma t�cnica � capturar as a��es dos usu�rios
reais e reproduzi-las. Outra t�cnica � a cria��o de uma linguagem de cria��o de
scripts que descreve sequ�ncias e o tempo dos eventos.
Finalmente, pense um pouco em como testar os pr�prios testes. N�s
mencionamos no Cap�tulo 5 a confus�o causada por um programa de teste de um pacote
de lista com falhas. Uma su�te de regress�o infectada por um erro causar� problemas
at� o fim. Os resultados de um conjunto de testes n�o significar�o muita coisa se
os pr�prios testes forem falhos.

6.8 Testando o programa Markov

O programa Markov, do Cap�tulo 3, � suficientemente complicado para


justificar os testes cuidadosos. Ele produz absurdos, cuja validade � dif�cil de
analisar, e n�s escrevemos v�rias vers�es em diversas linguagens. Como um �ltimo
complicador, sua sa�da � aleat�ria e diferente a cada vez. Como podemos aplicar
algumas das li��es deste cap�tulo para testar esse programa?
O primeiro conjunto de testes consiste em meia d�zia de arquivos pequenos
que verificam as condi��es de limite, para ter certeza de que o programa produzir�
a sa�da certa a partir das entradas que cont�m apenas algumas palavras. No caso dos
prefixos de comprimento dois, usamos cinco arquivos que cont�m respectivamente (com
uma palavra por linha):

(arquivo vazio)
a
a b
a b c
a b c d

Para cada arquivo, a sa�da deveria ser id�ntica � entrada. Essas


verifica��es descobriram v�rios erros off-by-one na inicializa��o da tabela e no
in�cio e parada do gerador.
Um segundo teste verificou as propriedades de conserva��o. Para prefixos de
duas palavras, cada palavra, par e trio que aparecem na sa�da de uma execu��o devem
ocorrer tamb�m na entrada. Escrevemos um programa Awk que l� a entrada original de
um array gigante, constr�i os arrays de todos os pares e trios, e depois l� a sa�da
Markov de outro array e compara os dois:

# markov test: verifica se todas as palavras, pares, trios de


# output ARGV[2] est�o na entrada original ARGV[1]
BEGIN {
while (getline <ARGV[1] > 0)
for (i = 1; i <= NF; i++) {
wd[++nw] = $i # palavras da entrada
single[$i]++
}
for (i = 1; i < nw; i++)
pair[wd[i],wd[i+1]]++
for (i = 1; i < nw-1; i++)
triple[wd[i],wd[i+1],wd[i+2]]++

while (getline <ARGV[2] > 0) {


outwd[++ow] = $0 # palavras da sa�da
if (!($0 in single))
print "unexpected word", $0
}
for (i = 1; i < ow; i++)
if (!((outwd[i],outwd[i+1]) in pair))
print "unexpected pair", outwd[i], outwd[i+1]
for (i = 1; i < ow-1; i++)
if (!((outwd[i],outwd[i+1],outwd[i+2]) in triple))
print "unexpected triple",
outwd[i], outwd[i+1], outwd[i+2]
}

Tentamos construir um teste eficiente, a fim de criar o programa de teste


mais simples poss�vel. S�o precisos seis ou mesmo sete segundos para verificar um
arquivo de sa�da de 10.000 palavras com rela��o a um arquivo de entrada com 42.685
palavras, o que n�o � muito mais tempo do que aquilo que algumas vers�es do Markov
levam para essa gera��o. A verifica��o da conserva��o pegou um erro importante em
nossa implementa��o da Java: o programa �s vezes substitu�a as entradas da tabela
hash porque ele usava refer�ncias, em vez de fazer c�pias dos prefixos.
Este teste ilustra o princ�pio de que pode ser muito mais f�cil verificar
uma propriedade da sa�da do que criar a pr�pria sa�da. Por exemplo, � mais f�cil
verificar se um arquivo est� classificado do que classific�-lo.
Um terceiro teste tem natureza estat�stica. A entrada consiste na seq��ncia

a b c a b c ... a b d ...

com dez ocorr�ncias de abc para cada abd. A sa�da deveria ter cerca de dez vezes
mais c' s que d' s se a sele��o aleat�ria estivesse funcionando adequadamente.
Confirmamos isso com freq, � claro.
O teste estat�stico mostrou que uma das primeiras vers�es do programa Java,
que associava os contadores a cada sufixo, produziu 20 c' s para cada d, o dobro
que ela deveria ter. Ap�s co�ar um pouco a cabe�a, n�s percebemos que o gerador de
n�mero aleat�rio da Java retorna inteiros negativos bem como positivos. O fator de
dois ocorreu porque o intervalo de valores era duas vezes maior do que o esperado
e, portanto, duas vezes mais valores seriam zero m�dulo o contador. Isso favorecia
o primeiro elemento da lista, o qual por acaso era c. O fix foi tomar o valor
absoluto antes dos m�dulos. Sem esse teste, nunca ter�amos descoberto o erro. A
olho nu a sa�da parecia boa.
Finalmente, n�s demos ao programa Markov texto em ingl�s para ver que ele
produzia um lindo absurdo. � claro que n�s tamb�m executamos esse teste no in�cio
do desenvolvimento do programa. Mas n�o paramos de testar quando o programa tratou
de entrada normal, porque os casos piores vir�o com a pr�tica. � sedutor acertar os
casos f�ceis, mas os casos dif�ceis tamb�m devem ser testados. O teste automatizado
e sistem�tico � a melhor maneira de evitar essa armadilha.
Todo o teste foi mecanizado. Um script shell gerou os dados de entrada
necess�rios, executou e fez o timing dos testes e imprimiu toda. a sa�da an�mala. O
script era configur�vel para que alguns testes pudessem ser aplicados a qualquer
vers�o do Markov e, sempre que faz�amos um conjunto de altera��es em um dos
programas, execut�vamos os testes novamente para ter certeza de que nada estava
quebrado.

6.9 Resumo

Quanto melhor voc� escrever o seu teste originalmente, menos bugs ele ter� e
mais confian�a voc� pode ter de que o seu teste foi completo. O teste das
condi��es-limite enquanto voc� escreve � uma maneira efetiva de eliminar muitos dos
bugs pequenos e simples. O teste sistem�tico tenta testar todos os pontos
potenciais de problemas de forma organizada. Normalmente, as falhas s�o encontradas
nos limites, os quais podem ser explorados � m�o ou por um programa. Na medida do
poss�vel, � desej�vel automatizar o teste, uma vez que as m�quinas n�o cometem
erros, nem se cansam ou enganam a si mesmas pensando que alguma coisa est�
funcionando, quando n�o est�. Os testes de regress�o verificam se o programa ainda
produz as mesmas respostas de antes. O teste ap�s cada pequena mudan�a � uma boa
t�cnica para localizar a fonte de todos os problemas, porque os bugs novos t�m mais
chance de ocorrer em c�digo novo. A �nica regra importante do teste �: testar.

Leitura suplementar

Uma maneira de aprender sobre os testes � estudar exemplos do melhor


software que est� dispon�vel de gra�a. O livro de Don Knuth, "The Errors of TEX",
em Software - Practice and Experience, 19, 7, p�gs. 607-685,1989, descreve cada
erro encontrado at� aquele ponto no formatador TEX, e inclui uma discuss�o dos
m�todos de teste de Knuth. O teste TRIP para o TEX � um exemplo excelente de uma
su�te de testes completa. A Perl tamb�m vem com uma extensa su�te de testes
destinada a verificar sua corre��o ap�s a compila��o e instala��o em um sistema
novo, e inclui m�dulos tais como MakeMaker e TestHarness, que auxiliam na
constru��o de testes para as extens�es Perl.
Jon Bentley escreveu uma s�rie de artigos em Communications of the ACM, que
depois foram reunidos em Programming Pearls e More Programming Pearls e publicados
pela Addison-Wesley em 1986 e 1988, respectivamente. Eles tocam no assunto dos
testes com freq��ncia, particularmente as estruturas de organiza��o e mecaniza��o
dos testes extensos.

Desempenho

Suas promessas eram, assim como ele o era na �poca, poderosas; Mas seu desempenho
era, assim como ele � agora, inexistente.
Shakespeare, Henrique VIII

H� muito tempo, os programadores se esfor�avam para tornar seus programas


eficientes porque os computadores eram lentos e caros. Hoje em dia, as m�quinas s�o
bem mais baratas e r�pidas; portanto, a necessidade de efici�ncia absoluta diminuiu
muito. Ainda vale a pena se preocupar com o desempenho?
Sim, mas somente se o problema for importante, o programa for
verdadeiramente lento e se houver alguma expectativa de que ele pode ficar mais
r�pido conservando a corre��o, robustez e clareza. Um programa r�pido que d� a
resposta errada n�o economiza tempo nenhum.
Assim sendo, o primeiro princ�pio da otimiza��o � n�o otimize. O programa j�
� suficientemente bom? Sabendo como um programa ser� usado e o ambiente no qual ele
ser� executado, existe algum benef�cio em torn�-lo mais r�pido? Os programas
escritos para uso em uma classe de faculdade nunca s�o usados novamente e a
velocidade raramente importa. Ela tamb�m n�o ser� importante na maioria dos
programas pessoais, ferramentas eventuais, estruturas de teste, experi�ncias e
prot�tipos. O tempo de execu��o de um produto comercial ou de um componente
central, tal como uma biblioteca gr�fica, pode ter import�ncia cr�tica, por�m, e
precisamos entender como encarar as quest�es de desempenho.
Quando devemos tentar agilizar um programa? Como podemos fazer isso? Quais
ganhos podemos esperar? Este cap�tulo discute como fazer para que os programas
sejam executados mais rapidamente ou usem menos mem�ria. A velocidade geralmente �
a preocupa��o mais importante, e assim falaremos principalmente sobre isso. O
espa�o (mem�ria principal, disco) � uma quest�o menos freq�ente mas que pode ser
crucial; portanto, vamos ver esse assunto tamb�m.
Como observamos no Cap�tulo 2, a melhor estrat�gia � usar os algoritmos
mais simples e limpos e as estruturas de dados apropriadas para a tarefa. Depois
medimos o desempenho para ver se s�o necess�rias altera��es, para ativar op��es do
compilador a fim de gerar o c�digo mais r�pido poss�vel, avaliar quais altera��es
no pr�prio programa ter�o o maior efeito, fazer uma altera��o de cada vez e
reavaliar, e manter as vers�es simples para testar as revis�es.
A medi��o � um componente cr�tico para a melhoria do desempenho, uma vez que
o racioc�nio e a intui��o s�o guias falhos e devem ser suplementados com
ferramentas como os comandos de sincroniza��o e profilers. A melhoria do desempenho
tem muito em comum com o teste, incluindo t�cnicas tais como automa��o, manuten��o
cuidadosa de registros, e o uso dos testes de regress�o para ter certeza de que as
altera��es se mant�m corretas e n�o desfazem melhorias anteriores.
Se voc� escolher os algoritmos de forma sensata e escrever bem originalmente
talvez n�o haja a necessidade de melhorar a velocidade. Quase sempre altera��es
pequenas consertar�o quaisquer problemas de desempenho no c�digo bem projetado,
enquanto que o c�digo ruim exigir� grandes modifica��es.

7.1 Um gargalo

Vamos come�ar descrevendo como um gargalo foi removido de um programa


cr�tico de nosso ambiente local.
Nossa correspond�ncia recebida passa por uma m�quina, chamada gateway, a
qual conecta nossa rede interna com a Internet externa. As mensagens de correio
eletr�nico de fora - dezenas de milhares por dia para uma comunidade de alguns
milhares de pessoas - chegam no gateway e s�o transferidas para a rede interna.
Essa separa��o isola nossa rede privada da Internet p�blica e nos permite publicar
um �nico nome de m�quina (aquele do gateway) para todos da comunidade.
Um dos servi�os do gateway � a filtragem do "spam", aquela correspond�ncia
n�o-solicitada que anuncia servi�os de m�rito d�bio. Ap�s as primeiras tentativas
bem-sucedidas do filtro de spam, o servi�o foi instalado como um recurso permanente
para todos os usu�rios do gateway de correio eletr�nico, e imediatamente observamos
um problema. A m�quina de gateway, antiquada e j� bastante ocupada, estava
sobrecarregada porque o programa de filtragem estava ocupando tanto tempo - muito
mais tempo do que o requerido para todo o outro processamento de cada mensagem -
que as filas de correio eram preenchidas e a entrega de mensagens estava atrasada
em horas, enquanto o sistema lutava para acompanhar o ritmo.
Este � um exemplo de um verdadeiro problema de desempenho: o programa n�o
era suficientemente r�pido para fazer seu trabalho, e as pessoas tinham de arcar
com a inconveni�ncia do atraso. O programa simplesmente precisava ser executado
mais rapidamente.
Simplificando um pouco, o filtro de spam funciona da seguinte maneira. Toda
mensagem recebida � tratada como uma �nica string, e um dispositivo de coincid�ncia
de padr�o textual examina aquela string para saber se ela cont�m alguma frase do
spam conhecido, tal como "Ganhe milh�es no seu tempo livre" ou "XXX-rated". As
mensagens tendem a acontecer novamente; portanto, essa t�cnica � notavelmente
efetiva, e quando uma mensagem de spam n�o � detectada, uma frase � adicionada �
lista para peg�-la da pr�xima vez.
Nenhuma das ferramentas existentes de coincid�ncia de string, tais como
grep, tinha a combina��o certa de desempenho e empacotamento; portanto, um filtro
especial de spam foi escrito. O c�digo original era bastante simples. Ele
verificava se cada mensagem continha alguma das frases (padr�es):

/* isspam: testa a ocorr�ncia de qualquer pat em mesg */


int isspam(char *mesg)
{
int i;

for (i = 0; i < npat; i++)


if (strstr(mesg, pat[i]) != NULL) {
printf("spam: match for '%s'\n", pat[i]);
return 1;
}
return 0;
}

Como isso poderia ficar mais r�pido? A string deve ser pesquisada, e a fun��o
strstr da biblioteca C � a melhor maneira de pesquisar: ela � padronizada e
eficiente.
Usando a perfilagem, uma t�cnica da qual falaremos na se��o seguinte, ficou
claro que a implementa��o de strstr tinha propriedades que eram ruins quando usadas
em um filtro de spam. Alterando o modo como strstr funcionava, foi poss�vel torn�-
la mais eficiente para esse problema em particular.
A implementa��o existente de strstr procurava algo assim:

/* simple strstr: usa strchr para procurar o primeiro caractere */


char *strstr(const char *s1, const char *s2)
{
int n;

n = strlen(s2);
for (;;) {
s1 = strchr(s1, s2[0]);
if (s1 == NULL)
return NULL;
if (strncmp(s1, s2, n) == 0)
return (char *) s1;
s1++;
}
}

Ela foi escrita tendo em mente a efici�ncia, e na verdade para o uso t�pico ela era
r�pida porque usava rotinas de biblioteca altamente otimizadas para fazer o
trabalho. Ela chamava strchr para encontrar a pr�xima ocorr�ncia do primeiro
caractere do padr�o, e depois chamava strncmp para ver se o restante da string
coincidia com o restante do padr�o. Assim sendo, ela saltava rapidamente a maioria
da mensagem procurando o primeiro caractere do padr�o, e depois fazia um exame
r�pido para verificar o restante. Por que ela teria esse desempenho ruim?
Existem v�rias raz�es. Em primeiro lugar, strncmp toma como argumento o
comprimento do padr�o, o qual deve ser calculado com strlen. Mas os padr�es s�o
fixos; portanto, n�o seria necess�rio recalcular seus comprimentos em cada
mensagem.
Em segundo lugar, strncmp tem um loop interno complexo. Ele n�o s� deve
comparar os bytes de duas strings, mas tamb�m procurar o byte \0 terminal em ambas
as strings, enquanto faz a contagem do par�metro de comprimento. Como os
comprimentos de todas as strings s�o conhecidos com anteced�ncia (embora n�o para
strncmp), essa � uma complexidade desnecess�ria. Sabemos que as contagens est�o
certas; portanto, a verifica��o de \0 s� faz perder tempo.
Em terceiro lugar, strchr tamb�m � complexa, uma vez que deve procurar o
caractere e tamb�m observar o byte \0 que encerra a mensagem. Para determinada
chamada a isspam, a mensagem � fixa; portanto, o tempo passado procurando \0 �
perdido, uma vez que n�s sabemos onde a mensagem termina.
Finalmente, embora strncmp, strchr e strlen sejam todas eficientes quando
isoladas, a overhead da chamada dessas fun��es � compar�vel ao custo do c�lculo que
elas executar�o. � mais eficiente fazer todo o trabalho em uma vers�o especial e
cuidadosamente escrita de strstr, e evitar totalmente a chamada de outras fun��es
Esses tipos de problemas s�o uma fonte comum de perturba��es no desempenhe -
uma rotina ou interface funciona bem para o caso t�pico, mas tem desempenhe ruim em
um caso incomum que � cr�tico para o programa em quest�o. A strstr existente era
boa quando tanto padr�o quanto strings eram curtos e alteravam cada chamada, mas
quando a string � longa e fixa, a overhead � proibitiva.
Tendo isso em mente, strstr foi reescrita para passar ao mesmo tempo pela
strings de padr�o e mensagem procurando as coincid�ncias, sem chamar sub-rotina: A
implementa��o resultante tem um comportamento previs�vel: ela � ligeiramente mais
lenta em alguns casos, mas muito mais r�pida no filtro de spam e, o mais
importante, nunca � terr�vel em termos de velocidade. Para verificar a corre��o e o
desempenho da nova implementa��o, foi constru�da uma su�te de testes. Essa su�te
inclu�a n�o apenas os exemplos simples, tais como a pesquisa de uma palavra dentro
de uma senten�a, como tamb�m os casos patol�gicos, tais como procurar um padr�o de
um �nico x em uma string de mil e's e um padr�o de mil x' s em uma string de um
�nico e, sendo que ambos esses casos podem ser tratados de forma ruim pelas
implementa��es inocentes. Tais casos extremos s�o parte importante da avalia��o de
desempenho.
A biblioteca foi atualizada com a nova strstr e o filtro de spam foi executa
cerca de 30% mais r�pido, uma boa recompensa para o trabalho de reescrever uma
�nica rotina.

Infelizmente, ela continuava muito lenta.


Ao solucionar problemas, � importante fazer a pergunta certa. At� agora,
v�nhamos pedindo a maneira mais r�pida de pesquisar um padr�o textual dentro de
uma string. Mas o verdadeiro problema � pesquisar um conjunto grande e fixo de
padr�es textuais dentro de uma string longa e vari�vel. Colocado dessa forma, n�o
fica t�o �bvio que strstr � a solu��o certa.
A maneira mais efetiva de fazer um programa ser executado mais rapidamente �
usar um algoritmo melhor. Com uma ideia mais clara do problema, est� na hora de
pensar sobre qual algoritmo funcionaria melhor.
O loop b�sico,

for (i = 0; i < npat; i++)


if (strstr(mesg, pat[i]) != NULL)
return 1;

examina o npat da mensagem em momentos independentes; assumindo que ele n�o


encontre nenhuma coincid�ncia, ele examina cada byte da mensagem npat vezes,
resultando em um total de strlen(mesg)*npat compara��es.
Uma abordagem melhor � inverter os loops, examinando a mensagem uma vez no
loop externo e pesquisando todos os padr�es paralelamente no loop interno:

for (j = 0; mesg[j] != '\0'; j++)


if (some pattern matches starting at mesg[j])
return 1;

A melhoria do desempenho vem de uma observa��o simples. Para ver se algum padr�o
coincide com a mensagem da posi��o j, n�o precisamos olhar todos os padr�es, apenas
aqueles que come�am com o mesmo caractere de mesg[j]. Aproximadamente, com 52
letras mai�sculas e min�sculas podemos esperar fazer apenas strlen(mesg)*npat/52
compara��es. Como as letras n�o est�o distribu�das iniformemente - as palavras
come�am com s com muito mais frequ�ncia do que com x - n�o veremos um
aperfei�oamento com um fator de 52, mas devemos ter algum aperfei�oamento. Na
verdade, constru�mos uma tabela hash usando o primeiro caractere do padr�o como a
chave.
Dado algum pr�-c�lculo para construir uma tabela cujos padr�es come�am com
cada caractere, isspam ainda est� curta:

int patlen[NPAT]; /* comprimento do padr�o */


int starting[UCHAR_MAX+1][NSTART]; /* pats come�ando com char */
int nstarting[UCHAR_MAX+1]; /* n�mero desses padr�es */
...
/* isspam: testa a ocorr�ncia de qualquer pat em mesg */
int isspam(char *mesg)
{
int i, j, k;
unsigned char c;

for (j = 0; (c = mesg[j]) != '\0'; j++ ) {


for (i = 0; i < nstarting[c]; i++) {
k = starting[c][i];
if (memcmp(mesg+j, pat[k], patlen[k]) == 0) {
printf("spam: match for '%s'\n", pat[k]);
return 1;
}
}
}
return 0;
}

O array bidimensional starting[c][] armazena, para cada caractere c, os


�ndices dos padr�es que come�am com aquele caractere. Seu companheiro nstarting[c]
registra o n�mero de padr�es que come�am com c. Sem essas tabelas, o loop interno
seria executado de 0 a npat, cerca de mil. Em vez disso, ele � executado de 0 at�
algo como 20. Finalmente, o elemento de array patlen[k] armazena o resultado pr�-
calculado de strlen(pat[k]).
A figura seguinte faz um esbo�o dessas estruturas de dados para um conjunto
de tr�s padr�es que come�am com a letra b:

O c�digo para construir essas tabelas � f�cil:

int i;
unsigned char c;

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


c = pat[i][0];
if (nstarting[c] >= NSTART)
eprintf("too many patterns (>=%d) begin '%c'",
NSTART, c);
starting[c][nstarting[c]++] = i;
patlen[i] = strlen(pat[i]);
}

Dependendo da entrada, o filtro de spam agora est� de cinco a dez vezes mais
r�pido do que quando se usou a strstr aperfei�oada, e de sete a 15 vezes mais
r�pido do que a implementa��o original. N�o tivemos um fator de 52, em parte por
causa da distribui��o n�o-uniforme das letras, em parte porque o loop � mais
complicado no programa novo, em parte porque ainda h� muitas compara��es de strings
falhas a serem executadas, mas o filtro de spam n�o � mais o gargalo da entrega de
correspond�ncia. O problema de desempenho est� solucionado.
O restante deste cap�tulo vai explorar as t�cnicas usadas para descobrir
problemas de desempenho, isolar o c�digo lento e agiliz�-lo. Antes de continuar,
por�m, vale a pena voltar ao filtro de spam para ver quais li��es ele tem a nos
ensinar. A principal � ter certeza de que o desempenho � importante. N�o valeria a
pena todo o esfor�o se o filtro de spam n�o fosse um gargalo. Depois que sabemos
que isso era um problema, usamos a perfilagem e outras t�cnicas para estudar o
comportamento e saber onde realmente est� o problema. Depois, garantimos que
est�vamos solucionando o problema certo, examinando o programa geral em vez de
focalizarmos apenas strstr, a suspeita �bvia por�m incorreta. Finalmente,
solucionamos o problema correto usando um algoritmo melhor, e verificamos se ele
realmente era mais r�pido. Depois que ele estava suficientemente r�pido, n�s
paramos. Para que o excesso de engenharia?

� Exerc�cio 7-1. Uma tabela que mapeia um �nico caractere para o conjunto de
padr�es que come�a com aquele caractere d� um aperfei�oamento com uma ordem de
magnitude. Implemente uma vers�o de isspam que usa dois caracteres como �ndice. A
quanto aperfei�oamento isso leva? Esses s�o casos especiais simples de uma
estrutura de dados chamada trie. A maioria dessas estruturas de dados se baseia na
troca de espa�o por tempo. ?

7.2 Sincroniza��o e perfilagem

Automatize as medi��es de sincroniza��o. A maioria dos sistemas tem um comando para


medir o tempo que um programa leva. No Unix, o comando se chama time:

% time slowprogram

real 7.0
user 6.2
sys 0.1
%

Isso executa o comando e reporta tr�s n�meros, tudo em segundos: tempo "real", o
tempo decorrido para o programa se completar; tempo de CPU de "usu�rio", o tempo
gasto executando o programa do usu�rio, e tempo de CPU de "sistema", o tempo gasto
dentro do sistema operacional por parte do programa. Caso seu sistema tenha um
comando semelhante, use-o. Os n�meros ser�o mais informativos, confi�veis e f�ceis
de controlar do que o tempo medido com um cron�metro. E fa�a anota��es boas. Ao
trabalhar no programa, fazendo modifica��es e medi��es, voc� vai acumular muitos
dados que podem se tornar confusos um ou dois dias depois. (Qual foi a vers�o que
era executada 20% mais r�pido?) Muitas das t�cnicas que discutimos no cap�tulo
sobre os testes podem ser adaptadas para medir e aperfei�oar o desempenho. Use a
m�quina para executar e medir suas su�tes de testes e, mais importante, use o teste
de regress�o para ter certeza de que suas modifica��es n�o v�o quebrar o programa.
Caso o seu sistema n�o tenha um comando time, ou se voc� estiver medindo o
tempo de uma fun��o isolada, � f�cil construir um andaime de tempo semelhante a um
andaime de teste. C e C++ fornecem uma rotina padr�o, clock, que reporta o tempo de
CPU consumido pelo programa at� agora. Ela pode ser chamada antes e depois de uma
fun��o para medir o uso de CPU:

#include <time.h>
#inctude <stdio.h>
...
clock_t before;
double elapsed;

before = clock();
long_running_function();
elapsed = clock() - before;
printf("function used %.3f seconds\n", elapsed/CLOCKS_PER_SEC);

O termo de ajuste de escala, CLOCKS_PER_SEC, registra a resolu��o do timer


reportada por clock. Se a fun��o leva apenas uma fra��o de um segundo, execute-a
com um loop, mas tenha certeza de compensar a overhead do loop se ela for
significativa:

before = clock();
for (i = 0; i < 1000; i++)
short_running_function();
elapsed = (clock()-before)/(double)i;

Em Java, as fun��es da classe Date d�o a hora do rel�gio de parede, que �


uma aproxima��o do tempo de CPU:

Date before = new Date();


long_running_function();
Date after = new Date();
long elapsed = after.getTime() - before.getTime();

O valor de retorno de getTime est� em milissegundos.

Use um profiler. Al�m de um m�todo de tempo confi�vel, a ferramenta mais importante


para a an�lise do desempenho � um sistema para gerar perfis. Um perfil � uma medida
de onde o programa passa seu tempo. Alguns perfis listam cada fun��o, o n�mero de
vezes que ela � chamada, e a fra��o do tempo de execu��o que ela consome. Os outros
mostram as contagens de quantas vezes cada declara��o foi executada. As declara��es
que s�o executadas com freq��ncia contribuem mais para o tempo de execu��o,
enquanto que as declara��es que nunca s�o executada podem indicar c�digo in�til ou
c�digo que n�o est� sendo adequadamente testado A perfilagem � uma ferramenta
efetiva para localizar os pontos ativos de un programa, as fun��es ou se��es de
c�digo que consomem a maior parte do tempo de computa��o. Entretanto, os perfis
devem ser interpretados com cuidado. Dada a sofistica��o dos compiladores e a
complexidade dos efeitos do armazenamento em cache e da mem�ria, bem como o fato de
que a perfilagem de um programa afeta seu desempenho, as estat�sticas de um perfil
podem ser apenas aproximadas.
No documento de 1971 que introduziu o termo perfilagem, Don Knuth escreveu
que "menos de 4% de um programa geralmente representam mais da metade de seu tempo
de execu��o". Isso indica que a perfilagem deve ser usada para identificar as
partes demoradas cr�ticas do programa, melhor�-las o m�ximo poss�vel e depois medir
novamente para ver se surgiu um ponto ativo novo. Eventualmente, ap�s apenas uma ou
duas itera��es, n�o sobra nenhum ponto ativo �bvio.
A perfilagem geralmente � ativada com um flag ou uma op��o especial de
compilador. O programa � executado e depois uma ferramenta de an�lise mostra os
resultados. No Unix, o flag geralmente � -p e a ferramenta se chama prof.:

% cc -p spamtest.c -o spamtest
% spamtest
% prof spamtest

A tabela seguinte mostra o perfil gerado por uma vers�o especial do filtro de spam
que constru�mos para entender seu comportamento. Ela mostra uma mensagem fixa e um
conjunto fixo de 217 frases, as quais ele coincide com a mensagem 10.000 vezes.
Essa execu��o a 250 MHz MIPS R10000 usou a implementa��o original de strstr que
chama outras fun��es padr�o. A sa�da foi editada e reformatada para caber na
p�gina. Observe como os tamanhos de entrada (217 frases) e o n�mero de execu��es
(10.000) aparecem como verifica��es de consist�ncia na coluna de "chamadas", a qual
conta o n�mero de chamadas de cada fun��o.

12234768552: n�mero total de instru��es executadas


13961810001: total de ciclos computados
55.847: tempo de execu��o total calculado (seg)
1.141: m�dia de ciclos/instru��o

Seg%cum
%ciclosinstru��eschamadasfun��o45.62081,0%81,0%11314990000944011000048350000strchr6
.081 10,9% 91,9% 1520280000 1566460000 46180000 strncmp2.592 4,6% 96,6% 648080000
854500000 2170000 strstr1.825 3,3% 99,8% 456225559 344882213 2170435 strlen0.088
0,2% 100,0% 21950000 28510000 10000 isspam0.000 0,0% 100,0% 100025 100028 1
main0.000 0,0% 100,0% 53677 70268 219 _memccpy0.000 0,0% 100,0% 48888 46403 217
strcpy0.000 0,0% 100,0% 17989 19894 219 fgets0.000 0,0% 100,0% 16798 17547 230
_malloc0.000 0,0% 100,0% 10305 10900 204 realfree0.000 0,0% 100,0% 6293 7161 217
estrdup0.000 0,0% 100,0% 6032 8575 231 cleanfree0.000 0,0% 100,0% 5932 5729 1
readpat0.000 0,0% 100,0% 5899 6339 219 getline0.000 0,0% 100,0% 5500 5720 220
_malloc
� �bvio que strchr e strncmp, ambas chamadas por strstr, dominam
completamente o desempenho. A orienta��o de Knuth est� certa: uma pequena parte do
programa consome a maioria do tempo de execu��o. Quando um programa passa pela
perfilagem pela primeira vez, � comum ver a fun��o principal sendo executada a 50%
ou mais, e isso facilita a decis�o de onde focalizar a aten��o.

Concentre-se nos pontos ativos. Ap�s reescrever strstr, fizemos o perfil de


spamtest novamente e descobrimos que 99,8% do tempo agora eram gastos apenas em
strstr, embora todo o programa estivesse consideravelmente mais r�pido. Quando uma
�nica fun��o � o gargalo, h� apenas duas coisas a fazer: melhorar a fun��o para
usar um algoritmo melhor, ou eliminar a fun��o totalmente reescrevendo o programa
ao redor.
Nesse caso, reescrevemos o programa. Aqui est�o as primeiras linhas do
perfil de spamtest usando a implementa��o final e r�pida de isspam. Observe que o
tempo geral � muito menor, que memcmp agora � o ponto ativo e que isspam agora
consome uma fra��o significativa da computa��o. Isso � mais complexo do que a
vers�o que chamava strstr, mas seu custo � mais do que compensado pela elimina��o
de strlen e strchr de isspam e a substitui��o de strncmp por memcmp, que faz menos
trabalho por byte.

Seg%cum
%ciclosinstru��eschamadasfun��o3.52456,9%56,9%880890000102759000046180000memcmp2.66
2 43,0% 100,0% 665550000 902920000 10000 isspam 0.001 0,0% 100,0% 140304 106043 652
strlen 0.000 0,0% 100,0% 100025 100028 1 main
� instrutivo passar algum tempo comparando as contagens de ciclo e o n�mero
de chamadas dos dois perfis. Observe que strlen passou de alguns milh�es de
chamadas para 652, e que strncmp e memcmp s�o chamadas o mesmo n�mero de vezes.
Observe tamb�m que isspam, que agora incorpora a fun��o de strchr, ainda consegue
usar bem menos ciclos do que strchr usava antes, porque ela examina apenas os
padr�es relevantes de cada etapa. Muito mais detalhes da execu��o podem ser
descobertos examinando os n�meros.
Um ponto ativo quase sempre pode ser eliminado, ou pelo menos congelado, por
uma engenharia bem mais simples do que aquela que usamos no filtro de spam. H�
muito tempo, um perfil do Awk indicou que uma fun��o estava sendo chamada cerca de
um milh�o de vezes ao longo de um teste de regress�o no seguinte loop:

? for (j = i; j < MAXFLD; j++)


? clear(j);

O loop, que limpa os campos antes que cada nova linha de entrada seja lida,
estava ocupando at� 50% do tempo de execu��o. A constante MAXFLD, o n�mero m�ximo
de campos permitido em uma linha de entrada, era de 200. Mas, na maioria dos usos
do Awk, o n�mero real de campos era de apenas dois ou tr�s. Assim sendo, uma
quantidade enorme de tempo estava sendo desperdi�ada na limpeza dos campos que
nunca haviam sido definidos. A substitui��o da constante pelo valor anterior do
n�mero m�ximo de campos resultou em uma agiliza��o geral de 25%. O fix foi alterar
o limite m�ximo do loop:

for (j = i; j < maxfld; j++)


clear(j);
maxfld = i;

Desenhe uma figura. As figuras s�o particularmente boas para apresentar medi��es de
desempenho. Elas podem veicular informa��es sobre os efeitos das altera��es de
par�metro, comparar algoritmos e estruturas de dados e, �s vezes, indicam
comportamento inesperado. Os gr�ficos de contagem de comprimento de cadeia de
diversos multiplicadores hash do Cap�tulo 5 mostraram claramente que alguns
multiplicadores eram melhores do que outros.
O seguinte gr�fico mostra o efeito do tamanho do array da tabela hash no
tempo de execu��o para a vers�o C de markov como Salmos como entrada (42.685
palavras, 22.482 prefixos). Fizemos duas experi�ncias. Um conjunto de execu��es
usou os tamanhos de array que eram pot�ncias de dois a partir de 2 at� 16.384. O
outro conjunto usou tamanhos que s�o o maior primo menor do que cada pot�ncia de
dois. Quer�amos ver se um tamanho de array primo fazia alguma diferen�a que pudesse
ser medida no desempenho.
O gr�fico mostra que o tempo de execu��o dessa entrada n�o � importante para
o tamanho da tabela, uma vez que o tamanho est� acima de 1.000 elementos, nem h�
uma diferen�a not�vel entre os tamanhos de tabela de primo e pot�ncia de dois.

� Exerc�cio 7-2. Se o seu sistema tiver ou n�o um comando time, use clock ou
getTime para escrever um recurso de sincroniza��o para seu pr�prio uso. Compar seus
tempos em um rel�gio de parede. Como outra atividade na m�quina afeta o tempos? ?

� Exerc�cio 7-3. No primeiro perfil, strchr foi chamado 48.350.000 vezes e strncmp
apenas 46.180.000. Explique a diferen�a. ?

7.3 Estrat�gias para ter velocidade

Antes de alterar um programa para torn�-lo mais r�pido, tenha certeza de que
ele est� realmente lento, e use as ferramentas de sincroniza��o e os profilers para
descobrir onde est� indo o tempo. Depois que voc� souber o que est� acontecendo,
existem v�rias estrat�gias a serem adotadas. Listamos aqui algumas por ordem
decrescente de lucratividade.

Use um algoritmo ou uma estrutura de dados melhores. O fator mais importante para
tornar um programa mais r�pido � a escolha do algoritmo e da estrutura de dados.
Pode haver uma diferen�a enorme entre um algoritmo que � eficiente e outro que n�o
�. Nosso filtro de spam sofreu uma altera��o na estrutura de dados que valia um
fator de dez. Um aperfei�oamento maior ainda � poss�vel quando o algoritmo novo
reduz a ordem de c�lculo, digamos de O(n2) para O(nlog n). Abordamos esse t�pico no
Cap�tulo 2; portanto, n�o vamos falar dele aqui.
Verifique se a complexidade � realmente aquilo que voc� espera. Caso
contrarie pode haver um bug oculto de desempenho. Este algoritmo aparentemente
linea para examinar uma string

? for (i = 0; i < strlen(s); i++)


? if (s[i] == c)
? ...

na verdade � quadr�tico: se s tem n caracteres, cada chamada a strlen caminha ao


longo de n caracteres da string e o loop � executado n vezes.

Ative os otimiza��es do compilador. Uma altera��o com custo zero que geralmente
produz um aperfei�oamento razo�vel pode ativar qualquer otimiza��o fornecida pelo
compilador. Os compiladores modernos s�o t�o bons que eliminam grande parte das
altera��es em pequena escala que os programadores precisam fazer.
Como padr�o, a maioria dos compiladores C e C++ n�o tenta realizar muita
otimiza��o. Uma op��o de compilador ativa o otimizador ("aperfei�oador" seria um
termo mais preciso). Provavelmente ela seria o padr�o, exceto que as otimiza��es
tendem a confundir os depuradores no n�vel de fonte, de modo que o programadores
devem ativar o otimizador explicitamente quando acharem que o programa j� foi
depurado.
A otimiza��o de compilador geralmente melhora o tempo de execu��o de alguns
porcentos at� um fator de dois. Eventualmente, por�m, ela deixa o programa mais
lento; portanto, me�a a melhoria antes de enviar seu produto. Comparamos compila��o
n�o-otimizada e otimizada em algumas das vers�es do filtro de spam. Para a su�te de
testes que usava a vers�o final do algoritmo de coincid�ncia, o tempo de execu��o
original foi de 8,1 segundos, o qual caiu para 5,9 segundos quando a otimiza��o foi
ativada, uma melhoria de mais de 25%. Por outro lado, a vers�o que usava a strstr
consertada n�o mostrou nenhum aperfei�oamento com a otimiza��o, porque strstr j�
havia sido otimizada quando foi instalada na biblioteca. O otimizador se aplica
apenas ao c�digo-fonte que est� sendo compilado agora e n�o �s bibliotecas do
sistema. Entretanto, alguns compiladores t�m otimizadores globais, os quais
analisam todo o programa procurando aperfei�oamentos em potencial. Se tal
compilador estiver dispon�vel no seu sistema, experimente-o. Ele pode espremer mais
alguns ciclos.
Uma coisa a ser observada � que quanto mais agressivamente o compilador
otimizar, maior a probabilidade da introdu��o de bugs no programa compilado. Ap�s
ativar o otimizador, execute novamente sua su�te de testes de regress�o, como faria
como qualquer outra modifica��o.

Ajuste o c�digo. A op��o certa pelo algoritmo � importante quando os tamanhos de


dados s�o suficientemente grandes. Al�m disso, os aperfei�oamentos algor�tmicos
funcionam em m�quinas, compiladores e linguagens diferentes. Mas depois que o
algoritmo certo est� configurado, se a velocidade ainda for importante a pr�xima
coisa a tentar � o ajuste do c�digo: o ajuste dos detalhes de loops e express�es
para fazer as coisas andarem mais r�pido.
A vers�o de isspam que mostramos no final da Se��o 7.1 n�o havia sido
ajustada. Vamos mostrar aqui que podemos conseguir mais aperfei�oamentos mexendo no
loop. Como um lembrete, foi assim que o deixamos:

for (j = 0; (c = mesg[j]) != '\0'; j++) {


for (i = 0; i < nstarting[c]; i++) {
k = starting[c][i];
if (memcmp(mesg+j, pat[k], patlen[k]) == 0) {
printf("spam: match for '%s'\n", pat[k]);
return 1;
}
}
}

Essa vers�o inicial levou 6,6 segundos em nossa su�te de testes quando
compilada usando o otimizador. O loop interno tem um �ndice de array (nstarting[c])
em sua condi��o de loop, cujo valor � fixo para cada itera��o do loop externo.
Podemos evitar novo c�lculo salvando o valor em uma vari�vel local:

for (j = 0; (c = mesg[j]) != '\0'; j++) {


n = nstarting[c];
for (i = 0; i < n; i++) {
k = starting[c][i];
...

Isso faz cair o tempo para 5,9 segundos, cerca de 10% mais r�pido, uma
agiliza��o t�pica daquilo que pode ser conseguido com o ajuste. H� outra vari�vel
que podemos arrancar: starting[c] tamb�m � fixa. Parece que arrancar esse c�lculo
do loop tamb�m pode ajudar, mas em nossos testes isso n�o fez nenhuma diferen�a
mensur�vel. Isso tamb�m � t�pico do ajuste: algumas coisas ajudam, outras n�o, e �
preciso medir para descobrir. E os resultados variam dependendo das m�quinas ou dos
compiladores.
H� outra altera��o que poder�amos fazer no filtro de spam. O loop interno
compara todo o padr�o com a string, mas o algoritmo garante que o primeiro
caractere j� coincida. Assim sendo, podemos ajustar o c�digo para iniciar memcmp um
byte adiante. Tentamos isso e descobrimos que o aperfei�oamento foi de 3%, o que �
pouco mas requer a modifica��o de apenas tr�s linhas do programa, uma delas na pr�-
computa��o.

N�o otimize o que n�o tem import�ncia. �s vezes o ajuste n�o consegue nada porque
ele � aplicado onde n�o faz diferen�a. Verifique se o c�digo que voc� est�
otimizando � o lugar onde o tempo � realmente gasto. A seguinte hist�ria pode n�o
ser aut�ntica, mas vamos cont�-la mesmo assim. Uma m�quina antiga de uma empresa
agora extinta foi analisada com um monitor de desempenho de hardware e se descobriu
que ela estava gastando 50% do seu tempo executando a mesma seq��ncia de v�rias
instru��es. Os engenheiros constru�ram uma instru��o especial para encapsular a
fun��o da seq��ncia, reconstru�ram o sistema e descobriram que isso n�o fez a menor
diferen�a. Eles haviam otimizado o loop ocioso do sistema operacional.
Quanto esfor�o voc� deve empenhar para fazer um programa ser executado mais
r�pido? O principal crit�rio � se as altera��es ter�o resultado suficiente para
compensarem. Como orienta��o, o tempo pessoal gasto tornando um programa mais
r�pido n�o deve ser maior do que o tempo que a agiliza��o recuperar� durante a vida
�til do programa. Seguindo essa regra, o aperfei�oamento algor�tmico feito em
isspam valeu a pena: custou um dia de trabalho mas economizou (e continua
economizando) horas todos os dias. A remo��o do �ndice de array do loop interno foi
menos dram�tica, mas ainda valeu a pena, uma vez que o programa fornece um servi�o
para uma comunidade grande. A otimiza��o dos servi�os p�blicos como o filtro de
spam ou uma biblioteca quase sempre vale a pena. A agiliza��o de programas de teste
dificilmente compensa. E para um programa que � executado durante um ano, esprema
tudo o que puder. Talvez valha a pena iniciar de novo se voc� descobrir uma maneira
de ter um aperfei�oamento de dez por cento, mesmo depois de um m�s de execu��o do
programa.
Os programas competitivos - jogos, compiladores, processadores de texto,
planilhas, sistemas de bancos de dados - tamb�m se classificam nessa categoria, uma
vez que o sucesso comercial quase sempre � destinado ao mais r�pido, pelo menos nos
resultados publicados de benchmark.
� importante cronometrar os programas depois das mudan�as, para ter certeza
de que as coisas est�o melhorando. As vezes duas altera��es que melhoram um
programa interagir�o, anulando seus efeitos individuais. Tamb�m acontece de os
mecanismos de tempo serem t�o inconstantes que fica dif�cil tirar conclus�es firmes
sobre o efeito das altera��es. Mesmo nos sistemas de �nico usu�rio, os tempos podem
flutuar de forma imprevis�vel. Se a varia��o do timer interno (ou pelo menos aquilo
que � reportado para voc�) for de dez por cento, as altera��es que resultam em
aperfei�oamentos de apenas dez por cento s�o dif�ceis de distinguir do barulho.

7.4 Ajustando o c�digo

Existem muitas t�cnicas para reduzir o tempo de execu��o quando um ponto


ativo � encontrado. Aqui temos algumas sugest�es, as quais devem ser aplicadas com
cautela, e com o teste de regress�o ap�s cada uma para garantir que o c�digo ainda
est� funcionando. Tenha em mente que os bons compiladores far�o parte disso para
voc�, e na verdade voc� pode atrapalhar seus esfor�os e complicar o programa.
Independente do que voc� tentar, me�a seus efeitos para ter certeza de que est�
ajudando.

Colete as subexpress�es comuns. Se um c�lculo caro aparecer v�rias vezes, fa�a-o


apenas em um lugar e lembre-se do resultado. Por exemplo, no Cap�tulo 1 mostramos
uma macro que calculava uma dist�ncia chamando sqrt duas vezes em seguida com os
mesmos valores. Na verdade, este era o c�lculo:

? sqrt(dx*dx + dy*dy) + ((sqrt(dx*dx + dy*dy) > 0) ? ...)

Calcule a raiz quadrada uma vez e use seu valor em dois lugares.
Se um c�lculo � feito dentro de um loop, mas n�o depende de nada que mude
dentro do loop, mova o c�lculo para fora, assim como neste caso em que
substitu�mos:

for (i = 0; i < nstarting[c]; i++) {

por:

n = nstartingfc];
for (i = 0; i < n; i++) {

Substitua as opera��es caras pelas baratas. O termo redu��o de for�a refere-se �s


otimiza��es que substituem uma opera��o cara por uma barata. Nos tempos antigos,
isso costumava significar substituir multiplica��es por adi��es ou deslocamentos,
mas raramente quer dizer muita coisa agora. A divis�o e o resto s�o muito mais
lentos do que a multiplica��o, de modo que pode haver aperfei�oamento quando uma
divis�o pode ser substitu�da pela multiplica��o pelo inverso, ou um resto por uma
opera��o de m�scara, se o divisor for uma pot�ncia de dois. A substitui��o da
indexa��o de array pelos ponteiros na C ou C++ poderia agilizar as coisas, embora a
maioria dos compiladores fa�a isso automaticamente. A substitui��o de uma chamada
de fun��o por um c�lculo simples ainda vale a pena. A dist�ncia no plano �
determinada pela f�rmula sqrt(dx*dx+dy*dy); portanto, a decis�o do ponto que est�
mais longe normalmente envolveria o c�lculo de duas ra�zes quadradas. Mas a mesma
decis�o pode ser tomada por meio da compara��o dos quadrados das dist�ncias:

if (dx1*dx1+dy1*dy1 < dx2*dx2+dy2*dy2)


...

d� o mesmo resultado da compara��o das ra�zes quadradas das express�es.


Outra inst�ncia ocorre nos matchers de padr�o textual como nosso filtro de
spam ou grep. Se o padr�o come�a com um caractere literal, uma pesquisa r�pida
daquele caractere � feita no texto de entrada. Se nenhuma coincid�ncia for
encontrada, o maquin�rio mais caro de pesquisa n�o � nunca invocado.

Desenrole ou elimine os loops. H� uma determinada overhead na configura��o e


execu��o de um loop. Se o corpo do loop n�o for longo demais e n�o iteragir muitas
vezes, talvez seja mais eficiente escrever cada itera��o na seq��ncia. Assim sendo,
por exemplo,

for (i = 0; i < 3; i++)


a[i] = b[i] + c[i];

se torna:

a[0] = b[0] + c[0];


a[1] = b[1] + c[1];
a[2] = b[2] + c[2];

Isso elimina a overhead do loop, particularmente o desvio, que pode deixar os


processadores modernos lentos por meio da interrup��o do fluxo da execu��o.
Se o loop for mais longo, o mesmo tipo de transforma��o pode ser usado para
amortizar a overhead em menos itera��es:

for (i = 0; i < 3*n; i++)


a[i] = b[i] + c[T];[TN]

se torna:

for (i = 0; i < 3*n; i += 3) {


a[i+0] = b[i+0] + c[1+0];
a[i+l] = b[i+l] + c[i+l];
a[i+2] = b[i+2] + c[i+2];
}

Observe que isso funciona apenas quando o comprimento � um m�ltiplo do tamanho da


etapa. Caso contr�rio, � preciso c�digo adicional para consertar os lados, que s�o
lugares nos quais os erros surgem e onde um pouco da efici�ncia pode ser perdida
novamente.

Fa�a o cache dos valores usados com freq��ncia. Os valores em cache n�o t�m de ser
recalculados. O cache aproveita a localidade, a tend�ncia dos programas (e das
pessoas) de dar prefer�ncia � reutiliza��o dos itens rec�m-acessados ou pr�ximos,
em vez dos itens mais antigos ou distantes. O hardware de c�lculo usa muito os
caches. Sem d�vida, o acr�scimo da mem�ria cache a um computador pode trazer
grandes aperfei�oamentos � velocidade de uma m�quina. O mesmo vale para o software.
Os browsers da Web, por exemplo, fazem o cache das p�ginas e imagens para evitar a
lentid�o na transfer�ncia de dados pela Internet. Em um programa de visualiza��o de
impress�o que escrevemos h� alguns anos, os caracteres especiais n�o-alfab�ticos,
tais como 1/2, tinham de ser procurados em uma tabela. A medi��o mostrou que grande
parte do uso dos caracteres especiais envolvia o desenho de linhas com sequ�ncias
longas do mesmo caractere. O armazenamento em cache apenas do caractere usado mais
recentemente tornou o programa significativamente mais r�pido nas entradas t�picas.
� melhor que a opera��o de armazenamento em cache seja invis�vel do lado de
fora, para que n�o afete o restante do programa, exceto para torn�-lo mais r�pido.
Assim sendo, no caso do visualizador de impress�o, a interface com a fun��o de
desenho de caractere n�o mudou. Ela sempre foi:

drawchar(c);

A vers�o original de drawchar chamava show(lookup(c)). A implementa��o do cache


usou as vari�veis est�ticas para se lembrar do caractere anterior e seu c�digo:

if (c != lastc) { /* atualizar cache */


lastc = c;
lastcode = lookup(c);
}
show(lastcode);

Escreva um alocador especializado. Quase sempre o �nico ponto ativo de um programa


� a aloca��o de mem�ria, a qual se manifesta como muitas chamadas a malloc ou new.
Quando a maioria das solicita��es � feita para blocos de mesmo tamanho, as
agiliza��es substanciais s�o poss�veis por meio da substitui��o das chamadas do
alocador geral pelas chamadas de um alocador especializado. O alocador
especializado faz uma chamada a malloc para preencher um array grande de itens e
depois os passa um de cada vez conforme a necessidade, uma opera��o mais barata. Os
itens liberados s�o colocados de volta em uma lista livre para poderem ser
reutilizados rapidamente.
Se os tamanhos solicitados s�o similares, voc� pode trocar espa�o por tempo
alocando sempre o suficiente para a solicita��o maior. Isso pode ser efetivo para
gerenciar as strings curtas quando voc� usa o mesmo tamanho para todas as strings
at� um comprimento especificado.
Alguns algoritmos podem usar uma aloca��o baseada na pilha, onde toda uma
seq��ncia de aloca��es � feita, e depois todo o conjunto � liberado ao mesmo tempo.
O alocador obt�m um peda�o grande para si mesmo e o trata como uma pilha,
empurrando os itens alocados conforme a necessidade e tirando-os em uma �nica
opera��o no final. Algumas bibliotecas C oferecem uma fun��o alloca para esse tipo
de aloca��o, embora ela n�o seja padr�o. Ela usa a pilha local de chamada como a
fonte de mem�ria, e libera todos os itens quando a fun��o que chama alloca retorna.
Fa�a o buffer da entrada e sa�da. O buffering cria lotes de transa��es para que as
opera��es freq�entes sejam feitas com o m�nimo poss�vel de overhead, e as opera��es
de overhead alta sejam realizadas somente quando necess�rio. Assim sendo, o custo
de uma opera��o fica disperso em diversos valores de dados. Quando um programa em C
chama printf, por exemplo, os caracteres s�o armazenados em um buffer, mas n�o s�o
passados para o sistema operacional at� que o buffer esteja cheio ou seja esvaziado
explicitamente. O sistema operacional em si pode, por sua vez, atrasar a grava��o
dos dados em disco. A desvantagem � a necessidade de esvaziar os buffers de sa�da
para tornar os dados vis�veis. No pior caso, as informa��es que ainda est�o em um
buffer se perder�o quando houver pane do programa.

Trate separadamente dos casos especiais. Tratando objetos de mesmo tamanho em


c�digo separado, os alocadores especializados reduzem o tempo e overhead de espa�o
no alocador geral e, ao mesmo tempo, reduzem a fragmenta��o. Na biblioteca de
gr�ficos do sistema Inferno, a fun��o b�sica draw foi escrita para ser a mais
simples e direta poss�vel. Com isso funcionando, as otimiza��es de uma variedade de
casos (escolhidos por meio da perfilagem) foram adicionadas uma de cada vez. Tamb�m
foi poss�vel testar a vers�o otimizada com rela��o � simples. No final, apenas meia
d�zia de casos foram otimizados porque a distribui��o din�mica das chamadas para a
fun��o se inclinava muito na dire��o da exibi��o dos caracteres. N�o valia a pena
escrever c�digo inteligente para todos os casos.

Pr�-calcule os resultados. �s vezes � poss�vel fazer um programa ser executado mais


rapidamente por meio da pr�-computa��o dos valores, para que eles estejam prontos
quando forem necess�rios. Vimos isso acontecer no filtro de spam, o qual pr�-
calculava strlen(pat[i]) e a armazenava no array em patlen[i]. Quando um sistema de
gr�ficos precisa calcular repetidamente uma fun��o matem�tica como o seno, mas
apenas para um conjunto distinto de valores, tais como graus de inteiro, ser� mais
r�pido calcular uma tabela com 360 entradas (ou fornec�-las como dados) e index�-la
conforme a necessidade. Este � um exemplo da troca de espa�o por tempo. Existem
muitas oportunidades para substituir c�digo por dados ou realizar c�lculo durante a
compila��o, para economizar tempo e eventualmente espa�o tamb�m. Por exemplo, as
fun��es ctype como isdigit quase sempre s�o implementadas indexando uma tabela de
flags de bits, em vez de avaliar uma sequ�ncia de testes.

Use valores aproximados. Se a precis�o n�o for importante, use tipos de dados de
menor precis�o. Em m�quinas mais antigas ou menores, ou em m�quinas que simulam o
ponto flutuante no software, a aritm�tica do ponto flutuante de precis�o simples
quase sempre � mais r�pida do que a precis�o dupla; portanto, use float em vez de
double para economizar tempo. Alguns processadores gr�ficos modernos usam um truque
relacionado. O padr�o de ponto flutuante IEEE requer o "overflow elegante" � medida
que os c�lculos se aproximam do n�vel de entrada dos valores represent�veis, mas
isso � um c�lculo caro. Para as imagens, o recurso � desnecess�rio, e � mais r�pido
e perfeitamente aceit�vel trancar para zero. Isso n�o apenas economiza tempo quando
os n�meros est�o abaixo do fluxo, mas tamb�m pode simplificar o Hardware de toda a
aritm�tica. O uso das rotinas de inteiro sin e cos � outro exemplo do uso de
valores aproximados.

Reescreva em uma linguagem de n�vel inferior. As linguagens de n�vel inferior


tendem a ser mais eficientes, embora com um custo para o tempo do programador.
Assim sendo, reescrever algumas partes cr�ticas de um programa em C++ ou Java para
a C, ou substituir um script interpretado por um programa em uma linguagem
compilada pode fazer com que elas sejam executadas muito mais r�pido.
Eventualmente, podem-se obter agiliza��es significativas com c�digo
dependente de m�quina. Esse � um �ltimo recurso, n�o uma etapa para ser usada sem
crit�rio, porque destr�i a portabilidade e torna muito mais dif�ceis a manuten��o e
as modifica��es futuras. Quase sempre, as opera��es a serem expressas na linguagem
assembly s�o fun��es relativamente pequenas que devem ser incorporadas a uma
biblioteca; memset e memmove, ou as opera��es gr�ficas, s�o exemplos t�picos. A
abordagem � escrever o c�digo da maneira mais limpa poss�vel em uma linguagem de
alto n�vel, e garantir que ele est� correto testando-o conforme descrevemos para
memset no Cap�tulo 6. Essa � a sua vers�o port�vel, a qual funcionar� em toda
parte, embora lentamente. Quando voc� se move para um ambiente novo, pode come�ar
com uma vers�o que reconhecidamente funciona. Agora quando voc� escrever uma vers�o
em linguagem assembly, teste-a exaustivamente com rela��o � vers�o port�vel. Quando
os bugs ocorrerem, o c�digo n�o port�vel ser� sempre suspeito. � reconfortante ter
uma implementa��o de compara��o.

� Exerc�cio 7-4. Uma forma de fazer uma fun��o como memset ser executada mais
r�pido � escrever em peda�os do tamanho de palavras, em vez de usar o tamanho de
byte. Isso provavelmente coincide melhor o hardware e pode reduzir a overhead do
loop por um fator de quatro ou oito. A desvantagem � que agora h� uma variedade de
efeitos finais para lidar se o destino n�o estiver alinhado em um limite de
palavra, e se o comprimento n�o for m�ltiplo do tamanho de palavra. Escreva uma
vers�o de memset que faz essa otimiza��o. Compare seu desempenho com a vers�o
existente da biblioteca e fa�a um loop direto um byte de cada vez. ?

� Exerc�cio 7-5. Escreva um alocador de mem�ria smalloc para as strings C que use
um alocador especializado para strings pequenas, mas que chame malloc diretamente
nas grandes. Voc� ter� de definir uma struct para representar a strings nos dois
casos. Como voc� resolve onde deve deslocar a chamada de smalloc para malloc? ?

7.5 Efici�ncia de espa�o

A mem�ria costumava ser o recurso de computa��o mais precioso, sempre


escasso, e muita programa��o ruim foi criada na tentativa de espremer o m�ximo do
pouco que havia. O famoso "Problema do Ano 2000" � citado freq�entemente como um
exemplo disso. Quando a mem�ria era realmente escassa, mesmo o dois bytes
necess�rios para armazenar 19 eram considerados caros demais. Seja ou n�o o espa�o
a verdadeira raz�o do problema - tal c�digo pode refletir simplesmente o modo como
as pessoas usam as datas no dia-a-dia, onde o s�culo normalmente � omitido - isso
demonstra o perigo inerente � otimiza��o sem vis�o.
Os tempos mudaram e tanto a mem�ria principal quanto o armazenamento
secund�rio est�o surpreendentemente baratos. Assim sendo, a primeira abordagem para
otimizar o espa�o deveria ser a mesma usada para agilizar a velocidade: n�o se
importe.
Ainda existem situa��es, por�m, em que a efici�ncia do espa�o importa.
Quando um programa n�o cabe na mem�ria principal dispon�vel, partes dela ser�o
paginadas e isso tornar� seu desempenho inaceit�vel. Vemos isso quando novas
vers�es de software desperdi�am mem�ria. � uma triste realidade o fato de que as
atualiza��es de software quase sempre s�o seguidas da compra de mais mem�ria.

Economize espa�o usando o menor tipo de dados poss�vel. Uma etapa da efici�ncia do
espa�o � fazer altera��es pequenas para usar melhor a mem�ria existente, por
exemplo, usando o menor tipo de dados que funcionar�. Isso pode significar a
substitui��o de int por short quando os dados se ajustar�o. Essa � uma t�cnica
comum para as coordenadas dos sistemas gr�ficos em 2-D, uma vez que os 16 bits
provavelmente lidam com todo intervalo esperado de coordenadas de tela. Ou ent�o
isso poderia significar a substitui��o de double por float. O problema en potencial
� a perda de precis�o, uma vez que floats geralmente cont�m apenas seis ou sete
d�gitos decimais.
Nesses casos e nos an�logos, outras altera��es talvez se fa�am necess�rias
principalmente as especifica��es de formato em printf e particularmente nas
declara��es scanf.
A extens�o l�gica dessa abordagem � codificar as informa��es em um byte ou
at� mesmo em menos bits, talvez em um �nico bit onde for poss�vel. N�o use os
bitfields da C ou C++. Eles s�o altamente n�o-port�veis e tendem a gerar c�digo
volumoso e ineficiente. Em vez disso, encapsule as opera��es desejadas em fun��es
que tiram e definem os bits individuais das palavras ou em um array de palavras com
as opera��es de deslocamento e m�scara. Esta fun��o retorna um grupo de bits
cont�guos da metade de uma palavra:

/* getbits: obt�m n bits da posi��o p */


/* os bits s�o numerados de 0 (menos significativo) para cima */
unsigned int getbits(unsigned int x, int p, int n)
{
return (x " (p+1-n)) & ~(~0 " n);
}

Se tais fun��es forem muito lentas, elas podem ser aperfei�oadas com as
t�cnicas descritas anteriormente neste cap�tulo. Em C++, a sobrecarga do operador
pode ser usada para fazer com que os acessos de bit se pare�am com o subscripting
regular.

N�o armazene aquilo que voc� pode recalcular facilmente. Essas altera��es s�o
pequenas e se parecem com o ajuste de c�digo. Os grandes aperfei�oamentos
provavelmente v�m de estruturas de dados melhores, talvez ligadas a altera��es de
algoritmo. Aqui temos um exemplo. H� muitos anos, um de n�s foi procurado por um
colega que estava tentando fazer um c�lculo em uma matriz t�o grande que era
preciso fechar a m�quina e recarregar um sistema operacional reduzido para que a
matriz se ajustasse. Ele queria saber se havia alguma alternativa, uma vez que esse
era um pesadelo operacional. Perguntamos como era a matriz e descobrimos que ela
continha valores inteiros, a maioria dos quais era zero. Na verdade, menos de cinco
por cento dos elementos da matriz eram n�o-zero. Isso sugeriu imediatamente uma
representa��o na qual apenas os elementos n�o-zero da matriz eram armazenados, e
cada acesso de matriz como m[i][j] seria substitu�do por uma chamada de fun��o
m(i,j). Existem v�rias maneiras de armazenar os dados; a mais f�cil provavelmente �
um array de ponteiros, um para cada linha, cada uma apontando para um array
compacto de n�meros de colunas e valores correspondentes. Isso representa uma
overhead de espa�o maior para cada item n�o-zero, mas requer muito menos espa�o
geral, e, embora os acessos individuais sejam mais lentos, eles ser�o sensivelmente
mais r�pidos do que o recarregamento do sistema operacional. Para completar a
hist�ria: o colega aplicou a sugest�o e saiu completamente satisfeito.
Usamos uma abordagem semelhante para solucionar uma vers�o moderna do mesmo
problema. Um sistema de projeto de r�dio precisava representar os dados de terreno
e os pontos fontes do sinal de r�dio ao longo de uma �rea geogr�fica bastante
grande (de 100 a 200 quil�metros em um lado) para uma resolu��o de 100 metros. O
armazenamento disso como um grande array retangular excederia a mem�ria dispon�vel
na m�quina de destino e teria causado um comportamento inaceit�vel de pagina��o.
Mas nas regi�es grandes, os valores de terreno e for�a de sinal provavelmente s�o
iguais; portanto, uma representa��o hier�rquica que aglutina as regi�es de mesmo
valor em uma �nica c�lula torna o problema gerenci�vel.
As varia��es desse tema s�o freq�entes, bem como as representa��es
espec�ficas, mas todas compartilham da mesma ideia b�sica: armazenar valor ou
valores comuns implicitamente ou em uma forma compacta, e ocupar mais tempo e
espa�o nos valores restantes. Quando a maioria dos valores comuns � realmente
comum, isso � um ganho.
O programa deve estar organizado para que a representa��o dos dados
espec�ficos de tipos complexos esteja oculta em uma classe ou conjunto de fun��es
que operam em um tipo de dados privado. Essa precau��o garante que o restante do
programa n�o ser� afetado quando a representa��o mudar.
As quest�es de efici�ncia de espa�o �s vezes se manifestam tamb�m na
representa��o externa das informa��es, tanto da convers�o quanto do armazenamento.
Em geral, � melhor armazenar as informa��es como texto sempre que for poss�vel, em
vez de em alguma representa��o bin�ria. O texto � port�vel, f�cil de ler e
process�vel por meio de todo tipo de ferramentas. As representa��es bin�rias n�o
t�m nenhuma dessas vantagens. O argumento a favor da bin�ria geralmente se baseia
na "velocidade", mas isso deve ser tratado com um certo ceticismo, uma vez que a
disparidade entre as formas de texto e bin�ria pode n�o ser t�o boa.
A efici�ncia de espa�o quase sempre tem um custo para o tempo de execu��o.
Uma aplica��o precisava transferir uma imagem grande de um programa para outro. As
imagens, em um formato simples chamado PPM, geralmente tinham um megabyte;
portanto, n�s achamos que seria bem mais r�pido codific�-las para transferir no
formato GIF compactado. Esses arquivos tinham tamanhos de 50K bytes. Mas a
codifica��o e decodifica��o do GIF levava o mesmo tempo economizado ao transferir
um arquivo mais curto; portanto, n�o havia ganho. O c�digo para lidar com o formato
GIF tem cerca de 500 linhas de comprimento. A fonte PPM tem cerca de dez linhas.
Para facilidade de manuten��o, portanto, a codifica��o GIF foi deixada de lado e a
aplica��o continua usando exclusivamente o PPM. � claro que a troca seria diferente
se o arquivo tivesse de ser enviado por meio de uma rede lenta. Nesse caso, a
codifica��o GIF seria muito mais eficiente com rela��o ao custo.

7.6 Estimativa

� dif�cil estimar com anteced�ncia a rapidez com a qual um programa ser�


executado e � duplamente dif�cil estimar o custo das declara��es ou instru��es de
m�quina da linguagem espec�fica. � f�cil, por�m, criar um modelo de custo para uma
linguagem ou um sistema, o qual lhe dar� pelo menos uma ideia aproximada do tempo
das opera��es importantes.
Uma abordagem muito usada nas linguagens convencionais de programa��o � um
programa que cronometra as seq��ncias representativas de c�digo. Existem
dificuldades operacionais, como obter resultados que podem ser reproduzidos e
cancelar as overheads irrelevantes, mas � poss�vel obter id�ias �teis sem muito
esfor�o. Por exemplo, temos um programa de modelo de custo em C e C++ que estima os
custos das declara��es individuais, incluindo-as em um loop que as executa milh�es
de vezes e depois calcula um tempo m�dio. Em um MIPS R10000 de 250 MHz, ele produz
esses dados, com os tempos em nanossegundos por opera��o.

Opera��es int
i1++ 8
i1 = i2 + i3 12
il = i2 - i3 12
i1 = i2 * i3 12
i1 = i2 / i3 114
i1 = i2 % i3 114

Opera��es float
f1 = f2 8
f1 = f2 + f3 12
f1 = f2 - f3 12
f1 = f2 * f3 11
fl = f2 / f3 28

Opera��es double
d1 = d2 8
d1 = d2 + d3 12
d1 = d2 - d3 12
d1 = d2 * d3 11
d1 = d2 / d3 58

Convers�es num�ricas
i1 = f1 8
f1 = i1 8
As opera��es de inteiro s�o r�pidas, exceto pela divis�o e m�dulos. As
opera��es de ponto flutuante s�o r�pidas ou mais r�pidas, uma surpresa para as
pessoas que cresceram num tempo em que as opera��es de ponto flutuante eram muito
mais caras do que as opera��es de inteiro.
Outras opera��es b�sicas tamb�m s�o bastante r�pidas, incluindo as chamadas
de fun��o, as tr�s �ltimas linhas deste grupo:

Opera��es de vetor inteiro


v[i] = i 49
v[v[i]] = i 81
v[v[v[i]]] = i 100

Estruturas de controle
if (i == 5) i1++ 4
if (i != 5) i1++ 12
while (i < 0) i1++ 3
i1 = sum1(i2) 57
i1 = sum2(i2, i3) 58
i1 = sum3(i2, i3, i4) 54

Mas a entrada e sa�da n�o s�o t�o baratas, nem a maioria das outras fun��es de
biblioteca:

Entrada/Sa�da
fputs(s, fp) 270
fgets(s, 9, fp) 222
fprintf(fp, "%d\n", i) 1820
fscanf(fp, "%d", &i1) 2070

Malloc
free(malloc(8)) 342

Fun��es de string
strcpy(s, "0123456789") 157
i1 = strcmp(s, s) 176
i1 = strcmp(s, "3123456789") 64

Convers�es string/n�mero
il = atoi("12345") 402
sscanf("12345", "%d", &i1) 2376
sprintf(s, "%d", i) 1492
f1 = atof("123.45") 4098
sscanf("123.45", "%f", &f1) 6438
sprintf(s, "%6.2f", 123.45) 3902

Os tempos para malloc e free provavelmente n�o indicam o verdadeiro desempenho, uma
vez que liberar imediatamente ap�s a aloca��o n�o � um padr�o t�pico. Finalmente,
as fun��es matem�ticas:

Fun��es matem�ticas
i1 = rand() 135
f1 = log(f2) 418
f1 = exp(f2) 462
f1 = sin(f2) 514
f1 = sqrt(f2) 112

Obviamente, esses valores seriam diferentes em hardware diferente, mas as


tend�ncias podem ser usadas para estimativas de quanto tempo alguma coisa levaria,
ou para comparar os custos relativos de E/S versus opera��es b�sicas, ou para
resolver se uma express�o � reescrita ou uma fun��o em linha � usada.
Existem muitas fontes de varia��o. Uma est� no n�vel de otimiza��o do
compilador. Os compiladores modernos podem encontrar otimiza��es que enganam a
maioria dos programadores. Al�m disso, as CPUs atuais s�o t�o complicadas que
apenas um bom compilador pode aproveitar sua habilidade de emitir v�rias instru��es
ao mesmo tempo, fazer o pipeline de sua execu��o, a busca das instru��es e dados
antes de eles serem necess�rios, e outras coisas semelhantes.
A arquitetura do computador em si � outro motivo para a dificuldade em
prever os n�meros. Os caches de mem�ria representam uma grande diferen�a na
velocidade, e grande parte da intelig�ncia do projeto de hardware est� em ocultar o
fato de que a mem�ria principal � bastante mais lenta do que a mem�ria cache.
As taxas brutas de rel�gio do processador como "400Mz" s�o sugestivas mas n�o
contam toda a hist�ria. Um de nossos Pentiums antigos de 200 MHz �
significativamente mais lento do que um Pentium mais antigo ainda de 100 MHz,
porque este �ltimo tem um cache de segundo n�vel grande que o primeiro n�o tem.
Al�m disso, gera��es diferentes de processador, mesmo para o mesmo conjunto de
instru��es, assumem n�meros diferentes de ciclos de rel�gio para realizar
determinada opera��o.

� Exerc�cio 7-6. Crie um conjunto de testes para estimar os custos das opera��es
b�sicas dos computadores e compiladores que est�o pr�ximos a voc�, e investigue as
similaridades e diferen�as de desempenho. ?

� Exerc�cio 7-7. Crie um modelo de custo para as opera��es de n�vel mais alto em C+
+. Entre os recursos que podem ser inclu�dos est�o a constru��o, c�pia e exclus�o
dos objetos de classe, as chamadas a fun��osmembro, fun��es virtuais, fun��es em
linha, a biblioteca iostream, a STL. Este exerc�cio � livre; portanto, concentre-se
em um conjunto pequeno de opera��es representativas. ?

� Exerc�cio 7-8. Repita o exerc�cio anterior para a Java. ?

7.7 Resumo

Depois da escolha do algoritmo certo, a otimiza��o do desempenho geralmente


� a �ltima preocupa��o ao se escrever um programa. Entretanto, voc� precisa
realiz�-la. O ciclo b�sico � medir, focalizar os poucos lugares nos quais ela far�
mais diferen�a, verificar a corre��o de suas altera��es e depois medir novamente.
Pare assim que puder, e preserve a vers�o mais simples como a base para o tempo e a
corre��o.
Quando voc� est� tentando melhorar a velocidade ou o consumo de espa�o de um
programa � bom criar alguns testes e problemas de benchmark para poder estimar e
controlar o desempenho sozinho. Se j� existem benchmarks padr�o para a sua tarefa,
use-os tamb�m. Se o programa for relativamente autocontido, uma abordagem �
encontrar ou criar uma cole��o de entradas "t�picas". Elas tamb�m podem fazer parte
de uma su�te de testes. Essa � a g�nese das su�tes de benchmark de sistemas
comerciais e acad�micos, tais como compiladores, computadores e outros. Por
exemplo, o Awk vem com cerca de 20 programas pequenos que juntos abordam a maioria
dos recursos das linguagens mais comuns. Esses programas s�o executados em um
arquivo de entrada muito grande para garantir que os mesmos resultados sejam
computados e que nenhum bug de desempenho seja introduzido. Tamb�m temos uma
cole��o de arquivos de dados padr�o grandes que podem ser usados para realizar os
testes de tempo. Em alguns casos � bom quando tais arquivos t�m propriedades
facilmente verificadas, por exemplo, um tamanho que seja uma pot�ncia de dez ou
dois.
O benchmarking pode ser gerenciado com o mesmo tipo de andaimes que
recomendamos para o teste no Cap�tulo 6. Os testes de tempo s�o executados
automaticamente, as sa�das incluem identifica��o suficiente para serem entendidas e
replicadas, os registros s�o mantidos para que as tend�ncias e altera��es
significativas possam ser observadas.
Por falar nisso, � extremamente dif�cil fazer um bom benchmarking e h� casos
de empresas que ajustam seus produtos para se sa�rem bem nos benchmarks; portanto,
� bom encarar os resultados de benchmark com um pouco de desconfian�a.

Leitura suplementar

Nossa discuss�o sobre o filtro de spam se baseia no trabalho de Bob


Flandrena e Ken Thompson. Seu filtro inclui express�es comuns para coincid�ncia
mais sofisticada, e classifica automaticamente as mensagens (certamente spam,
possivelmente spam, n�o-spam) segundo as strings que elas coincidem.
O documento sobre perfilagem de Knuth, "An Empirical Study of FORTRAN
Programs", apareceu em Software - Practice and Experience, 1, 2, p�gs. 105-133,
1971. A base do documento � uma an�lise estat�stica de um conjunto de programas
encontrado nas cestas de lixo e nos diret�rios publicamente vis�veis das m�quinas
dos centros de computa��o.
Os livros de Jon Bentley, Programming Pearls e More Programming Pearls
(Addison-Wesley, 1986 e 1988) t�m v�rios exemplos bons de aperfei�oamentos
algor�tmicos e de ajuste de c�digo. H� tamb�m bons ensaios sobre os andaimes para
melhoria do desempenho e o uso dos perfis.
Rick Booth escreveu Inner Loops (Addison-Wesley, 1997), uma boa refer�ncia
sobre ajuste de programas para PC, embora os processadores evoluam t�o r�pido que
os detalhes espec�ficos ficam desatualizados rapidamente.
A fam�lia de livros sobre arquitetura de computadores de John Hennessy e
David Patterson (por exemplo, Computer Organization and Design: The Hardware /
Software Interface, Morgan Kaufman, 1997) cont�m discuss�es completas sobre as
quest�es de desempenho dos computadores modernos.

Portabilidade

Finalmente, a padroniza��o, assim como a conven��o, pode ser outra manifesta��o da


ordem forte. Mas, ao contr�rio da conven��o, ela tem sido aceita na arquitetura
moderna como um produto enriquecedor de nossa tecnologia, embora temido pelo seu
dom�nio e brutalidade em potencial.
Robert Venturi, Complexity and Contradiction in Architecture

� dif�cil escrever software que seja executado correta e eficientemente.


Assim sendo, depois que um programa est� funcionando em um ambiente, voc� n�o quer
repetir todo o esfor�o quando tiver de mov�-lo para um compilador, processador ou
sistema operacional diferentes. Idealmente, ele n�o deve precisar de nenhuma
altera��o.
Esse ideal se chama portabilidade. Na pr�tica, "portabilidade" quase sempre
representa o conceito mais fraco de que ser� mais f�cil modificar o programa quando
ele mudar do que reescrev�-lo desde o in�cio. Quanto menos revis�es ele pedir, mais
port�vel ele ser�.
Voc� deve estar se perguntando por que se preocupar com a portabilidade. Se
o software ser� executado em apenas um ambiente, sob condi��es especificadas, por
que perder tempo dando-lhe uma aplicabilidade mais ampla? Em primeiro lugar, todo
programa bem-sucedido, quase que por defini��o, � usado de maneiras e em lugares
inesperados. A constru��o do software para ser mais geral do que sua especifica��o
original resultar� em menos manuten��o e mais utilidade no final das contas. Em
segundo lugar, os ambientes mudam. Quando o compilador ou sistema operacional ou
hardware � atualizado, os recursos podem mudar. Quanto menos o programa depender de
recursos especiais, menor a probabilidade de que ele quebre e mais facilmente ele
se adaptar� quando as circunst�ncias mudarem. Finalmente e mais importante � o fato
de que um programa port�vel � um programa melhor. O esfor�o investido para tornar
um programa port�vel tamb�m o torna mais bem projetado, mais bem constru�do e
testado mais completamente. As t�cnicas da programa��o port�vel est�o intimamente
relacionadas �s t�cnicas da boa programa��o em geral.
� claro que o grau da portabilidade deve ser temperado com a realidade. N�o
existe um programa absolutamente port�vel, apenas um programa que ainda n�o foi
experimentado em um n�mero suficiente de ambientes. Mas, podemos manter a
portabilidade como nosso objetivo, visando ao software que � executado sem
altera��es em quase todos os lugares. Mesmo que esse objetivo n�o seja totalmente
alcan�ado, o tempo gasto na portabilidade � medida que o programa � criado
compensar� quando o software precisar ser atualizado.
Nossa mensagem � a seguinte: tente escrever software que funcione dentro da
intersec��o dos diversos padr�es, interfaces e ambientes que ele deve acomodar. N�o
conserte cada problema de portabilidade acrescentando c�digo especial. Em vez
disso, adapte o software para que ele funcione dentro das novas restri��es. Use a
abstra��o e o encapsulamento para restringir e controlar o c�digo n�o-port�vel que
n�o puder ser evitado. Permanecendo dentro da intersec��o das restri��es e
localizando as depend�ncias do sistema, o seu c�digo se tornar� mais inteligente e
mais geral quando for portado.

8.1 Linguagem

Fique com o padr�o. A primeira etapa para o c�digo port�vel obviamente � programar
em uma linguagem de n�vel alto e dentro da linguagem padr�o, se houver uma. Os
bin�rios n�o s�o bem portados, mas o c�digo-fonte sim. Mesmo assim, a maneira como
um compilador traduz um programa para instru��es de m�quina n�o � definida
precisamente, mesmo nas linguagens padr�o. Poucas entre as linguagens muito usadas
t�m apenas uma �nica implementa��o. Geralmente h� v�rios fornecedores, ou vers�es
para sistemas operacionais diferentes, ou vers�es que evolu�ram com o tempo. O modo
como elas interpretam o seu c�digo-fonte varia.
Por que um padr�o n�o � uma defini��o r�gida? Eventualmente um padr�o est�
incompleto e n�o define o comportamento quando h� a intera��o dos recursos.
Eventualmente ele � deliberadamente indefinido. Por exemplo, o tipo char da C e C++
pode ser signed ou unsigned e nem precisa ter exatamente 8 bits. Quando tais
quest�es s�o deixadas por conta do compilador, o resultado s�o implementa��es mais
eficientes e o �xito em evitar a restri��o do hardware � linguagem na qual ele ser�
executado, correndo o risco de tornar a vida dos programadores mais dif�cil.
Quest�es pol�ticas e de compatibilidade t�cnica podem levar a comprometimentos que
deixam os detalhes sem especifica��o. Finalmente, as linguagens s�o complicadas e
os compiladores s�o complexos, e sempre existem erros na interpreta��o e bugs na
implementa��o.
As vezes as linguagens n�o s�o padronizadas. A C tem um padr�o oficial
ANSI/ISO divulgado em 1988, mas o padr�o ISO C++ foi ratificado apenas em 1998. Na
�poca em que est�vamos escrevendo este livro, nem todos os compiladores que eram
usados suportavam a defini��o oficial. A Java � nova e ainda est� longe de ser
padronizada. Um padr�o de linguagem geralmente � desenvolvido depois que a
linguagem tem uma variedade de implementa��es conflitantes a serem unificadas, e
tenha um uso suficientemente amplo para justificar a padroniza��o. Nesse meio
tempo, ainda existem programas a serem escritos e v�rios ambientes a serem
suportados.
Assim sendo, embora os manuais de refer�ncia e os padr�es d�em a impress�o
de ter uma especifica��o rigorosa, eles nunca definem uma linguagem completamente,
e as implementa��es diferentes podem ser interpreta��es v�lidas mas incompat�veis.
�s vezes existem at� mesmo erros. Uma pequena ilustra��o apareceu enquanto
est�vamos escrevendo o primeiro rascunho deste cap�tulo. Esta declara��o externa �
ilegal em C e C++:
? *x[] = {"abc"};

Um teste de uma dezena de compiladores revelou alguns que diagnosticam corretamente


o especificador de tipo char que estava faltando para x, um n�mero razo�vel que
avisava ou confundia os tipos (usando aparentemente uma defini��o antiga da
linguagem para inferir incorretamente que x � um array de ponteiros int), e outros
que compilavam o c�digo ilegal sem nenhuma reclama��o.

Programe no mainstream. A inabilidade de alguns compiladores para sinalizar esse


erro � um fato infeliz, mas tamb�m indica um aspecto importante da portabilidade.
As linguagens t�m cantos obscuros onde a pr�tica varia - os bitfields em C e C++,
por exemplo - e � prudente evit�-los. Use apenas aqueles recursos para os quais a
defini��o da linguagem � inequ�voca e bem compreendida. Muito provavelmente tais
recursos est�o amplamente dispon�veis e se comportam da mesma maneira em qualquer
lugar. Chamamos isso de mainstream da linguagem.
� dif�cil saber onde est� o mainstream, mas � f�cil reconhecer constru��es
que est�o totalmente fora dele. Os recursos novos, como os coment�rios com // e
complex em C, ou os recursos espec�ficos de um arquitetura, tal como as palavras-
chave near e far, certamente causar�o problemas. N�o use um recurso quando ele for
t�o incomum ou obscuro que para entend�-lo � preciso consultar um "advogado
especializado em linguagem" - um especialista em defini��es de linguagem.
Nesta discuss�o vamos focalizar C e C++, as linguagens de uso geral usadas
normalmente para escrever software port�vel. O padr�o C tem mais de uma d�cada de
exist�ncia e a linguagem � muito est�vel, por�m um padr�o novo est� em andamento e
novas transforma��es a caminho. Nesse meio tempo, o padr�o C++ est� saindo do
forno, portanto ainda n�o houve tempo para reunir todas as implementa��es.
Qual � o mainstream de C? O termo geralmente se refere ao estilo
estabelecido de uso da linguagem, mas eventualmente � melhor planejar para o
futuro. Por exemplo, a vers�o original de C n�o requeria prot�tipos de fun��o.
Declarava-se sqrt como sendo uma fun��o dizendo o seguinte

? double sqrt();

a qual define o tipo do valor de retorno mas n�o dos par�metros. O padr�o ANSI C
adicionou prot�tipos de fun��o que especificam tudo:

double sqrt(double);

Os compiladores ANSI C s�o requeridos para aceitar a primeira sintaxe, mas voc� n�o
deve escrever prot�tipos para todas as suas fun��es. Isso vai garantir um c�digo
mais seguro - o tipo das chamadas de fun��o ser� totalmente verificado - e se as
interfaces mudarem, o compilador vai peg�-las. Se o seu c�digo chamar

func(7, PI);

mas func n�o tiver nenhum prot�tipo, o compilador talvez n�o verifique se func est�
sendo chamada corretamente. Se a biblioteca mudar mais tarde para que func tenha
tr�s argumentos, a necessidade de reparar o software pode n�o ser notada porque a
sintaxe no estilo antigo desativa a verifica��o de tipo dos argumentos de fun��o.
A C++ � uma linguagem maior com um padr�o mais recente, portanto seu
mainstream � mais dif�cil de identificar. Por exemplo, embora seja esperado que a
STL se torne o mainstream, isso n�o acontecer� imediatamente, e algumas
implementa��es atuais n�o a suportam completamente.

Cuidado com os locais problem�ticos da linguagem. Como j� mencionamos, os padr�es


deixam algo intencionalmente indefinido ou n�o-especificado, em geral para dar mais
flexibilidade aos criadores de compiladores. A lista desses comportamentos � muito
longa.
Tamanhos de tipos de dados. Os tamanhos dos tipos b�sicos de dados em C e C++ n�o
s�o definidos. Al�m das regras b�sicas que dizem que

sizeof(char) < sizeof(short) < sizeof(int) < sizeof(long)


sizeof(float) < sizeof(double)

e que char deve ter pelo menos 8 bits, short e int pelo menos 16 e long pelo menos
32, n�o h� propriedades garantidas. Nem � necess�rio que um valor de ponteiro se
ajuste a um int.
� f�cil descobrir quais s�o os tamanhos para um compilador espec�fico:

/* sizeof: exibe os tamanhos dos tipos b�sicos */


int main(void)
{
printf("char %d, short %d, int %d, long %d,",
sizeof(char), sizeof(short),
sizeof(int), sizeof(long));
printf(" float %d, double %d, void* %d\n",
sizeof(float), sizeof(double), sizeof(void *));
return 0;
}

A sa�da � igual na maioria das m�quinas que usamos normalmente:

char 1, short 2, int 4, long 4, float 4, double 8, void* 4

mas sem d�vida outros valores s�o poss�veis. Algumas m�quinas de 64 bits produzem o
seguinte:

char 1, short 2, int 4, long 8, float 4, double 8, void* 8

e os primeiros compiladores para PC geralmente produziam isto:

char 1, short 2, int 2, long 4, float 4, double 8, void* 2

Nos primeiros dias dos PCs, o hardware suportava diversos tipos de ponteiros. A
necessidade de lidar com, essa bagun�a causou a inven��o dos modificadores de
ponteiros como far e near, sendo que nenhum deles � padr�o, mas cujos fantasmas das
palavras reservadas ainda assombram os compiladores atuais. Se o seu compilador
pode mudar os tamanhos dos tipos b�sicos, ou se voc� tem m�quinas com tamanhos
diferentes, tente compilar e testar seu programa nestas configura��es diferentes.
O arquivo header padr�o stddef.h define um n�mero de tipos que podem ajudar
na portabilidade. O mais comum deles � size_t que � o tipo inteiro unsigned
retornado pelo operador sizeof. Os valores desse tipo s�o retornados por fun��es
como strlen e s�o usados como argumentos por muitas fun��es, incluindo malloc.
Aprendendo com algumas dessas experi�ncias a Java define os tamanhos de
todos os tipos b�sicos de dados: byte tem 8 bits, char e short t�m 16 bits, int tem
32 e long tem 64 bits.
Vamos ignorar o rico conjunto de quest�es potenciais relacionadas ao c�lculo
do ponto flutuante, uma vez que esse � um assunto para um livro inteiro. Felizmente
as m�quinas mais modernas suportam o padr�o IEEE para o hardware de ponto
flutuante, e assim as propriedades da aritm�tica de ponto flutuante s�o
razoavelmente bem definidas.

Ordem de avalia��o. Em C e C++, a ordem de avalia��o dos operandos de express�es,


efeitos colaterais e argumentos de fun��o n�o � definida. Por exemplo, na
atribui��o

? n = (getchar() " 8) | getchar();


a segunda getchar poderia ser chamada em primeiro lugar: o modo como a express�o �
escrita n�o � necessariamente o modo como ela � executada. Na declara��o

? ptr[count] = name[++count];

count poderia ser incrementada antes ou ap�s ser usada para indexar ptr e em

? printf("%c %c\n", getchar(), getchar());

o primeiro caractere de entrada poderia ser impresso em segundo lugar em vez de ser
impresso em primeiro lugar. Em

? printf("%f %s\n", log(-1.23), strerror(errno));

o valor de errno pode ser avaliado antes de log ser chamado.


Existem regras para quando determinadas express�es s�o avaliadas. Por
defini��o, todos os efeitos colaterais e chamadas de fun��o devem ser conclu�dos em
cada ponto e v�rgula, ou quando uma fun��o � chamada. Os operadores && e || s�o
executados da esquerda para a direita e somente at� onde for necess�rio para
determinar seu valor verdadeiro (incluindo os efeitos colaterais). A condi��o de um
operador ?: � avaliada (incluindo os efeitos colaterais) e depois exatamente uma
das duas express�es seguintes � avaliada.
A Java tem uma defini��o mais r�gida para a ordem de avalia��o. Ela requer
que aquelas express�es, incluindo os efeitos colaterais, sejam avaliadas da
esquerda para a direita, embora um manual aprovado aconselhe que n�o se escreva
c�digo que dependa "crucialmente" desse comportamento. Esse � um conselho sensato
quando h� alguma chance de o c�digo Java ser convertido para C ou C++, as quais n�o
fazem essas promessas. A convers�o entre linguagens � um teste extremo mas
eventualmente razo�vel para a portabilidade.

A qualidade "sign" de char. Em C e C++n�o � especificado se o tipo de dados char �


signed ou unsigned. Isso pode levar a problemas quando se combinam chars e ints,
tal como no c�digo que chama a rotina de valor int getchar. Se voc� disser

? char c; /* deve ser int */


? c = getchar();

o valor de c ser� entre 0 e 255 se char for unsigned e entre -128 e 127 se char for
signed, para a configura��o quase universal de caracteres de 8 bits em uma m�quina
de complemento de dois. Isso traz implica��es quando o caractere deve ser usado
como um subscrito de array ou quando ele deve ser testado com rela��o a EOF, o qual
geralmente tem valor -1 em stdio. Por exemplo, n�s desenvolvemos esse c�digo na
Se��o 6.1 depois de consertar algumas condi��es de limite na vers�o original. A
compara��o s[i] == EOF sempre falhar� se char for unsigned:

? int i ;
? char s[MAX];
?
? for (i = 0; i < MAX-1; i++)
? if ((s[i] = getchar()) == '\n' || s[i] == EOF)
? break;
? s[i] = '\0';

Quando getchar retorna EOF, o valor 255 (0xFF, o resultado da convers�o de -1 para
unsigned char) ser� armazenado em s[i]. Se s[i] for unsigned, ele permanecer� 255
para a compara��o com EOF, a qual falhar�.
Mesmo quando char � signed, por�m, o c�digo n�o est� correto. A compara��o
ter� sucesso em EOF, mas um byte de entrada v�lido de 0xFF se parecer� com EOF e
encerrar� o loop prematuramente. Assim sendo, independente do sinal de char, voc�
sempre deve armazenar o valor de retorno de getchar em um int para a compara��o com
EOF. Aqui temos como escrever o loop de forma port�vel:

int c, i;
char s[MAX];

for (i = 0; i < MAX-1; i++) {


if ((c = getchar()) == '\n' || c == EOF)
break;
s[i] = c;
}
s[i] = '\0';

A Java n�o tem qualificador unsigned. Os tipos integrais s�o signed e o tipo
char (16 bits) n�o �.

Deslocamento aritm�tico ou l�gico. Os deslocamentos � direita das quantidades


signed com o operador >> podem ser aritm�ticos (uma c�pia do bit sign � propagada
durante o deslocamento) ou l�gicos (zeros preenchem os bits vagos durante o
deslocamento). Novamente, aprendendo com os problemas de C e C++, a Java reserva >>
para o deslocamento aritm�tico � direita e fornece um operador separado >>> para o
deslocamento l�gico � direita.

Ordem de byte. A ordem de byte dentro de short, int, e long n�o � definida; o byte
com o endere�o mais baixo pode ser o byte mais significativo ou o menos
significativo. Essa � uma quest�o que depende do hardware e que vamos discutir com
detalhes mais tarde neste cap�tulo.

Alinhamento de estrutura e membros de classe. O alinhamento dos itens dentro das


estruturas, classes e uni�es n�o � definido, exceto pelo fato de que os membros s�o
colocados na ordem da declara��o. Nesta estrutura, por exemplo:

struct X {
char c;
int i ;
}

o endere�o de i poderia ser 2, 4 ou 8 bytes desde o in�cio da estrutura. Algumas


m�quinas permitem que os ints sejam armazenados nos limites �mpares, mas a maioria
exige que um tipo de dados primitivo n-byte seja armazenado em um limite n-byte,
por exemplo que doubles, os quais normalmente t�m 8 bytes de comprimento, sejam
armazenados nos endere�os que s�o m�ltiplos de 8. Acima de tudo isso, aquele que
escreve o compilador pode fazer outros ajustes, tais como for�ar o alinhamento por
quest�es de desempenho.
Voc� nunca deve assumir que os elementos de uma estrutura ocupem mem�ria
cont�gua. As restri��es de alinhamento introduzem "buracos"; struct X ter� pelo
menos um byte de espa�o n�o utilizado. Esses buracos implicam o fato de que uma
estrutura pode ser maior do que a soma dos tamanhos de seus membros, e variam de
uma m�quina para outra. Se voc� est� alocando mem�ria para conter um, deve pedir
sizeof(struct X) bytes, n�o sizeof(char) + sizeof(int).

Bitfields. Os bitfields dependem tanto da m�quina que ningu�m deveria us�-los.

Essa longa lista de perigos pode ser resumida com algumas poucas regras. N�o
use os efeitos colaterais exceto para algumas poucas constru��es idiom�ticas como

a[i++] = 0;
c = *p++;
*s++ = *t++;

N�o compare um char com EOF. Use sempre sizeof para calcular o tamanho dos tipos e
objetos. Nunca fa�a o deslocamento � direita de um valor signed. Verifique se o
tipo de dados � suficientemente grande para o intervalo de valores no qual voc� o
est� armazenando.

Tente v�rios compiladores. � f�cil pensar que voc� entende a portabilidade, mas os
compiladores ver�o problemas que voc� n�o v�, e compiladores diferentes �s vezes
v�em o seu programa de forma diferente; portanto, voc� deve aproveitar sua ajuda.
Ative todos os avisos de compilador. Tente v�rios compiladores na mesma m�quina e
em m�quinas diferentes. Tente um compilador da C++ com um programa em C.
Como a linguagem aceita por diferentes compiladores varia, o fato de que o
seu programa compila com um compilador n�o � garantia nem de que ele est�
sintaticamente correto. Se v�rios compiladores aceitarem seu c�digo, por�m, as
chances aumentam. Compilamos cada programa em C deste livro com tr�s compiladores
da C em tr�s sistemas operacionais n�o relacionados (Unix, Plan 9, Windows) e
tamb�m com alguns dos compiladores da C++. Essa foi uma experi�ncia trabalhosa, mas
ela pegou dezenas de erros de portabilidade que nenhum exame humano teria
descoberto. Eles eram todos f�ceis de consertar.
� claro que os compiladores tamb�m causam problemas de portabilidade,
escolhendo op��es diferentes para comportamentos n�o especificados. Mas nossa
abordagem ainda nos d� esperan�a. Em vez de escrever c�digo de maneira a ampliar as
diferen�as entre os sistemas, ambientes e compiladores, tentamos criar software que
se comporta de forma independente das varia��es. Em resumo, ficamos longe dos
recursos e das propriedades que t�m tend�ncia a variar.

8.2 Headers e bibliotecas

Os headers e as bibliotecas fornecem servi�os que aumentam a linguagem


b�sica. Os exemplos incluem entrada e sa�da por meio de stdio na C, iostream na C++
e java.io na Java. A rigor, elas n�o fazem parte da linguagem, mas s�o definidas
junto com a pr�pria linguagem e devem fazer parte de qualquer ambiente que alegue
suport�-las. Mas como as bibliotecas cobrem um espectro amplo de atividades e quase
sempre devem lidar com quest�es do sistema operacional, elas ainda podem conter
n�o-portabilidades.

Use as bibliotecas padr�o. O mesmo conselho geral se aplica aqui e na linguagem de


n�cleo: fique com o padr�o, e dentro de seus componentes mais antigos e
estabelecidos. A C define uma biblioteca padr�o de fun��es para a entrada e sa�da,
opera��es de string, testes de classe de caracteres, aloca��o de armazenamento e
uma variedade de outras tarefas. Se voc� limitar as opera��es do seu sistema
operacional a essas fun��es, h� boas chances de que o seu c�digo se comporte da
mesma maneira e tenha bom desempenho ao ser movido de um sistema para outro. Mas
voc� ainda deve tomar cuidado, porque h� muitas implementa��es da biblioteca, e
algumas delas podem conter recursos n�o-definidos no padr�o.
O ANSI C n�o define a fun��o de c�pia de string, strdup, embora a maioria
dos ambientes a forne�a, mesmo aqueles que alegam seguir o padr�o. Um programador
experiente pode usar strdup sem saber que ela n�o � padr�o. Mais tarde, a
compila��o do programa falhar� quando ele for portado para um ambiente que n�o
fornece a fun��o. Esse tipo de problema � a principal dor de cabe�a de
portabilidade introduzida pelas bibliotecas. A �nica solu��o � ficar com o padr�o e
testar o seu programa em uma ampla variedade de ambientes.
Os arquivos de header e defini��es de pacote declaram a interface com as
fun��es padr�o. Um problema � que os headers tendem a ficar cheios porque eles
est�o tentando lidar com v�rias linguagens no mesmo arquivo. Por exemplo, � comum
encontrar um �nico arquivo de header como stdio.h servindo aos compiladores pr�-
ANSI C, ANSI C e mesmo C++. Em tais casos, o arquivo fica cheio com orienta��es
condicionais de compila��o como #if e #ifdef. Como a linguagem do pr�-processador
n�o � muito flex�vel, os arquivos s�o complicados e dif�ceis de ler e, �s vezes,
cont�m erros.
Este trecho de um arquivo de header de um de nossos sistemas � melhor do que
a maioria, porque est� formatado de maneira organizada:

? #ifdef _OLD_C
? extern int fread();
? extern int fwrite();
? #else
? # if defined(_STDC_) || defined(__cplusplus)
? extern size_t fread(void*, size_t, size_t, FILE*);
? extern size_t fwrite(const void*, size_t, size_t, FILE*);
? # else /* not _STDC_ || cplusplus */
? extern size_t fread();
? extern size_t fwrite();
? # endif /* else not --STDC-- || --cplusplus */
? #endif

Embora o exemplo seja relativamente limpo, ele demonstra que os arquivos de header
(e os programas) estruturados dessa forma s�o complicados e de dif�cil manuten��o.
Seria mais f�cil usar um header diferente para cada compilador ou ambiente. Isso
exigiria a manuten��o de arquivos separados, mas cada um seria autocontido e
apropriado para determinado sistema, e reduziria a probabilidade de erros como a
inclus�o de strdup em um ambiente estritamente ANSI C.
Os arquivos de header tamb�m podem "poluir" o espa�o de nome declarando uma
fun��o com o mesmo nome de uma fun��o do seu programa. Por exemplo, nossa fun��o de
mensagem de aviso weprintf se chamava originalmente wprintf, mas descobrimos que
alguns ambientes, prevendo o novo padr�o C, definem uma fun��o com aquele nome em
stdio.h. Precisar�amos soletrar o nome de nossa fun��o para compilar naqueles
sistemas e estarmos prontos para o futuro. Se o problema fosse uma implementa��o
err�nea em vez de uma altera��o verdadeira da especifica��o, poder�amos contornar o
problema redefinindo o nome ao incluir o header:

? /* algumas vers�es de stdio usam wprintf e, portanto, defina-a: */


? #define wprintf stdio_wprintf
? #include <stdio.h>
? #undef wprintf
? /* a seguir vem o c�digo usando nossa wprintf() ... */

Isso mapeia todas as ocorr�ncias de printf no arquivo de header para stdio_wprintf,


de modo que elas n�o interfiram em nossa vers�o. Depois, podemos usar nossa pr�pria
wprintf sem alterar seu nome, com o custo de alguma confus�o e o risco de que uma
biblioteca a qual nos vinculemos chame nossa wprintf esperando obter a fun��o
oficial. Para uma �nica fun��o, provavelmente isso n�o vale a pena, mas alguns
sistemas fazem uma bagun�a t�o grande com o ambiente que � preciso recorrer a
extremos para manter o c�digo limpo. Tenha certeza de comentar o que a constru��o
est� fazendo, e n�o piore as coisas adicionando compila��o condicional. Se alguns
ambientes definem wprintf, assuma que todos a definem, depois o conserto �
permanente e voc� tamb�m n�o ter� de manter as declara��es #ifdef. Talvez seja mais
f�cil e, certamente, � mais seguro deslocar do que lutar, de modo que foi isso o
que fizemos ao alterar o nome para weprintf.
Mesmo se voc� tentar seguir as regras e o ambiente estiver limpo, � f�cil
passar dos limites assumindo implicitamente que alguma propriedade preferida �
verdadeira em toda parte. Por exemplo, o ANSI C define seis sinais que podem ser
pegos com signat; o padr�o POSIX define 19; a maioria dos sistemas Unix suporta 32
ou mais. Se voc� quiser usar um sinal n�o-ANSI, h� claramente uma troca entre
funcionalidade e portabilidade, e voc� deve resolver o que � mais importante.
Existem muitos outros padr�es que n�o fazem parte de uma defini��o de
linguagem de programa��o. Os exemplos incluem o sistema operacional e as interfaces
de rede, as interfaces gr�ficas e outros. Alguns devem ser seguidos em mais de um
sistema, como o POSIX. Outros s�o espec�ficos de um sistema, como as diversas APIs
do Microsoft Windows. O conselho tamb�m vale aqui. Seus programas ser�o mais
port�veis se voc� escolher padr�es amplamente usados e bem estabelecidos, e se voc�
ficar com os aspectos mais centrais e usados.

8.3 Organiza��o de programa

Existem duas grandes abordagens para a portabilidade, as quais chamaremos de


uni�o de chamada e intersec��o. A abordagem da uni�o � usar os melhores recursos de
cada sistema em particular, e tornar o processo de compila��o e instala��o
condicional com as propriedades do ambiente local. O c�digo resultante trata da
uni�o de todos os cen�rios, aproveitando os pontos fortes de cada sistema. As
desvantagens incluem o tamanho e a complexidade do processo de instala��o e a
complexidade do c�digo decifrada com os condicionais do tempo de compila��o.

Use apenas os recursos que est�o dispon�veis em toda parte. A abordagem que
recomendamos � a intersec��o: use apenas aqueles recursos que existem em todos os
sistemas de destino. N�o use um recurso se ele n�o estiver dispon�vel em toda
parte. Um perigo � que o requisito da disponibilidade universal dos recursos possa
limitar a variedade dos sistemas de destino ou as capacidades do programa. Outro �
que o desempenho pode diminuir em alguns ambientes.
Para comparar essas abordagens, vamos ver alguns exemplos que usam c�digo de
uni�o e repens�-las usando a intersec��o. Como voc� ver�, o c�digo da uni�o � por
projeto n�o-port�vel, apesar de seu objetivo declarado, enquanto que o c�digo da
intersec��o n�o � apenas port�vel como tamb�m geralmente mais simples.
Este pequeno exemplo tenta lidar com um ambiente que, por algum motivo, n�o
tem o arquivo de header padr�o stdlib.h:

? #if defined (STDC_HEADERS) || defined (_LIBC)


? #inc1ude <stdlib.h>
? #else
? extern void *malloc(unsigned int);
? extern void *realloc(void *, unsigned int);
? #endif

Esse estilo de defesa � aceit�vel se for usado eventualmente, mas n�o se


aparecer com freq��ncia. Ele tamb�m implica a pergunta de como muitas outras
fun��es de stdlib eventualmente encontram seu caminho com esse c�digo condicional
ou semelhante. Por exemplo, se algu�m estiver usando malloc e realloc, certamente
free tamb�m ser� necess�rio. E se unsigned int n�o tiver o mesmo tamanho de size_t,
o tipo adequado de argumento para malloc e realloc? Al�m disso, como sabemos se
STDC_HEADERS ou _LIBC est�o definidos e se est�o corretos? Como podemos verificar
que n�o h� outro nome que poderia disparar a substitui��o em algum ambiente? Todo
c�digo condicional como esse � incompleto - n�o port�vel - porque eventualmente
aparecer� um sistema que n�o combina com a condi��o, e n�s devemos editar o
#ifdefs. Se pud�ssemos solucionar o problema sem a compila��o condicional,
eliminar�amos a constante dor de cabe�a da manuten��o. Mesmo assim, o problema que
este exemplo est� solucionando � real; portanto, como podemos solucion�-lo de uma
vez por todas? Preferir�amos assumir que os headers padr�o existem; caso eles n�o
existam o problema � de outra pessoa. Na falta disso, seria mais simples enviar com
o software um arquivo de header que define malloc, realloc e free, exatamente como
o ANSI C as define. Esse arquivo sempre pode ser inclu�do, em vez de aplicar band-
aids por todo o c�digo. Assim n�s sempre saberemos que a interface necess�ria est�
dispon�vel.

Evite a compila��o condicional. A compila��o condicional com #ifdef e diretivas


semelhantes de pr�-processador s�o dif�ceis de gerenciar porque as informa��es
tendem a se espalhar por toda a fonte.

? #ifdef NATIVE
? char *astring = "convert ASCII to native character set";
? #else
? #ifdef MAC
? char *astring = "convert to Mac textfile format";
? #else
? #ifdef DOS
? char *astring = "convert to DOS textfile format";
? #else
? char *astring = "convert to Unix textfile format";
? #endif /* ?DOS */
? #endif /* ?MAC */
? #endif /* ?NATIVE */

Esse trecho teria ficado melhor com #elif ap�s cada defini��o, em vez de ter os
#endifs se empilhando no final. Mas o problema real � este: apesar de sua inten��o,
esse c�digo � altamente n�o-port�vel porque ele se comporta de forma diferente em
cada sistema e precisa ser atualizado com um novo #ifdef para cada novo ambiente.
Uma �nica string com reda��o mais geral seria mais simples, completamente port�vel
e informativa:

char *astring = "convert to local text format";

Isso n�o precisa de nenhum c�digo condicional, uma vez que ele � o mesmo em todos
os sistemas.
A combina��o do fluxo de controle do tempo de compila��o (determinado pelas
declara��es #ifdef) com o fluxo de controle do tempo de execu��o � muito pior, uma
vez que ela � muito dif�cil de ler.

? #ifndef DISKSYS
? for (i = 1; i <= msg->dbgmsg.msg_total; i++)
? #endif
? #ifdef DISKSYS
? i = dbgmsgno;
? if (i <= msg->dbgmsg.msg_total)
? #endif
? {
? ...
? if (msg->dbgmsg.msg_total == i)
? #ifndef DISKSYS
? break; /* n�o h� mais mensagens para aguardar */
? cerca de 30 outras linhas, com compila��o adicional extra
? #endif

Mesmo quando � aparentemente in�cua, a compila��o condicional quase sempre


pode ser substitu�da por m�todos mais limpos. Por exemplo, #ifdefs# s�o usados para
controlar o c�digo de depura��o:

? #ifdef DEBUG
? printf(...);
? #endif

mas uma declara��o comum if com uma condi��o constante pode funcionar do mesmo
jeito:

enum { DEBUG = 0 };
...
if (DEBUG) {
printf (...);
}

Se DEBUG for zero, a maioria dos compiladores n�o gerar� nenhum c�digo para ele,
mas eles verificam a sintaxe do c�digo exclu�do. Por outro lado, um #ifdef pode
ocultar erros de sintaxe que evitam a compila��o quando #ifdef � ativado mais
tarde.
�s vezes a compila��o condicional exclui blocos grandes de c�digo:

#ifdef notdef /* s�mbolo indefinido */


...
#endif

ou

#if 0
...
#endif

mas o c�digo condicional quase sempre pode ser evitado de vez usando arquivos que
s�o substitu�dos condicionalmente durante a compila��o. Vamos voltar a esse assunto
na se��o seguinte.

Quando voc� precisar modificar um programa para adapt�-lo a um ambiente novo


n�o comece fazendo uma c�pia de todo o programa. Em vez disso adapte a fonte
existente. Voc� provavelmente vai precisar fazer altera��es no corpo principal do
c�digo e, se editar uma c�pia, em breve voc� ter� vers�es divergentes. Sempre que
poss�vel deve haver apenas uma �nica fonte para um programa. Se voc� descobrir que
precisa alterar alguma coisa para portar para determinado ambiente, descubra uma
maneira de fazer a altera��o funcionar em qualquer lugar. Altere as interfaces
internas se for preciso, mas mantenha o c�digo consistente e livre de #ifdef. Isso
tornar� seu c�digo mais port�vel com o tempo, em vez de torn�-lo mais
especializado. Diminua a intersec��o, n�o amplie a uni�o.
J� falamos contra a compila��o condicional e mostramos alguns dos problemas
que ela causa. Mas o problema mais desagrad�vel n�o mencionamos: � quase imposs�vel
testar. Um #ifdef transforma um �nico programa em dois programas compilados
separadamente. � dif�cil saber se todos os programas variantes foram compilados e
testados. Se uma altera��o for feita em um bloco #ifdef, talvez n�o tenhamos de
faz�-la em outros blocos, mas as altera��es s� podem ser verificadas dentro do
ambiente que fez com que aqueles #ifdefs fossem ativados. Se uma altera��o
semelhante precisar ser feita para outras configura��es, ela n�o poder� ser
testada. Da mesma forma, quando adicionamos um novo bloco #ifdef, � dif�cil isolar
a altera��o para determinar quais outras condi��es precisam ser atendidas para
chegar aqui, e onde mais esse problema teria de ser consertado. Finalmente, se
alguma coisa estiver no c�digo que for omitido condicionalmente, o compilador n�o
ver�. Isso seria absurdo e n�o saber�amos at� que algum cliente infeliz tentasse
compil�-lo no ambiente que dispara a condi��o. Este programa compila quando _MAC �
definido e falha quando ele n�o �:

#ifdef _MAC
printf("This is Macintosh\r");
#else
This will give a syntax error on other systems
#endif

Assim sendo, preferimos usar apenas os recursos que s�o comuns a todos os
ambientes de destino. Podemos compilar e testar todo o c�digo. Se alguma coisa � um
problema de portabilidade, n�s reescrevemos para evit�-lo, em vez de adicionar o
c�digo condicional. Dessa forma, a portabilidade aumentar� de maneira uniforme e o
pr�prio programa ser� aperfei�oado em vez de se tornar mais complicado.
Alguns sistemas grandes s�o distribu�dos com um script de configura��o para
adaptar o c�digo para o ambiente local. No tempo de compila��o, o script testa as
propriedades do ambiente - a localiza��o dos arquivos de header e das bibliotecas,
a ordem de byte dentro das palavras, o tamanho dos tipos, as implementa��es que
est�o reconhecidamente quebradas (algo surpreendente-mente comum) e assim por
diante - e gera os par�metros de configura��o ou makefiles que dar�o as defini��es
corretas de configura��o para aquela situa��o. Esses scripts podem ser grandes e
complicados, uma fra��o significativa de uma distribui��o de software, e exigem a
manuten��o cont�nua para que continuem funcionando. Eventualmente algumas t�cnicas
s�o necess�rias mas quanto mais port�vel e sem #ifdef for o c�digo, mais simples e
confi�vel ser� a configura��o e instala��o.

� Exerc�cio 8-1. Investigue como o seu compilador trata do c�digo contido dentro de
um bloco condicional como:

const int DEBUG = 0;


/* or enum { DEBUG = 0 }; */
/* or final boolean DEBUG = false; */

if (DEBUG) {
...
}

Sob quais circunst�ncias ele verifica a sintaxe? Quando ele gera o c�digo? Se voc�
tiver acesso a mais de um compilador, fa�a uma compara��o dos resultados. ?

8.4 Isolamento

Embora gostar�amos de ter uma �nica fonte que compila sem altera��o em todos
os sistemas, isso pode ser algo pouco realista. Mas � um erro ter c�digo n�o-
port�vel espalhado por todo o programa. Esse � um dos problemas criados pela
compila��o condicional.

Localize as depend�ncias de sistema em arquivos separados. Quando c�digo diferente


� necess�rio para sistemas diferentes, as diferen�as devem ser localizadas em
arquivos separados, um arquivo para cada sistema. Por exemplo, o editor de texto
Sam � executado no Unix, no Windows e em v�rios outros sistemas operacionais. As
interfaces de sistema desses ambientes variam muito, mas a maioria do c�digo para o
Sam � id�ntica em qualquer parte. Um �nico arquivo captura as varia��es do sistema
para determinado ambiente; o unix.c fornece o c�digo de interface para os sistemas
Unix, e windows.c para o ambiente Windows. Esses arquivos implementam uma interface
port�vel para o sistema operacional e ocultam as diferen�as. O Sam, na verdade, �
escrito para seu pr�prio sistema operacional virtual, o qual � portado para v�rios
sistemas reais com algumas centenas de linhas de C para implementar meia d�zia de
opera��es pequenas mas n�o-port�veis, usando as chamadas de sistema dispon�veis
localmente.
Os ambientes gr�ficos desses sistemas operacionais s�o quase n�o-
relacionados. O Sam lida com isso tendo uma biblioteca port�vel para seus gr�ficos.
Embora seja muito mais trabalhoso ter de construir tal biblioteca do que roubar o
c�digo e adapt�-lo para determinado sistema - o c�digo para fazer a interface com o
sistema X Window, por exemplo, tem cerca de metade do tamanho do restante do Sam
junto - o esfor�o cumulativo � menor a longo prazo. E como outro benef�cio, a
pr�pria biblioteca gr�fica � valiosa, e foi usada separadamente para tamb�m tornar
v�rios outros programas port�veis.
O Sam � um programa antigo. Hoje em dia, os ambientes gr�ficos port�veis
tais como o OpenGL, Tcl/Tk e Java est�o dispon�veis para uma variedade de
plataformas. Escrevendo seu c�digo com eles em vez de usar uma biblioteca gr�fica
patenteada voc� dar� ao seu programa uma utilidade mais ampla.

Oculte as depend�ncias do sistema por tr�s das interfaces. A abstra��o � uma


t�cnica poderosa para criar limites entre partes port�veis e n�o-port�veis de um
programa. As bibliotecas de E/S que acompanham a maioria das linguagens de
programa��o fornecem um bom exemplo: elas apresentam uma abstra��o do armazenamento
secund�rio em termos dos arquivos a serem abertos e fechados, lidos e gravados, sem
nenhuma refer�ncia � sua localiza��o f�sica ou estrutura. Os programas que usam
essa interface ser�o executados em qualquer sistema que a implemente.
A implementa��o do Sam fornece outro exemplo de abstra��o. Uma interface �
definida para o sistema de arquivos e para as opera��es gr�ficas, e o programa usa
apenas os recursos da interface. A interface em si usa todas as facilidades que
est�o dispon�veis no sistema b�sico. Isso poderia exigir implementa��es muito
diferentes em sistemas diferentes, mas o programa que usa a interface �
independente disso e n�o deve exigir altera��es ao ser movido.
A abordagem da Java para a portabilidade � um bom exemplo de at� onde ela
pode ser levada. Um programa Java � traduzido em opera��es de uma "m�quina
virtual", ou seja, um computador simulado que pode ser implementado para execu��o
em qualquer m�quina real. As bibliotecas da Java fornecem acesso uniforme aos
recursos do sistema b�sico, incluindo gr�ficos, interface de usu�rio, rede e
outros. As bibliotecas s�o mapeadas para qualquer coisa que o sistema local
fornecer. Teoricamente seria poss�vel executar o mesmo programa Java (mesmo ap�s a
tradu��o) em qualquer lugar sem altera��es.

8.5 Troca de dados

Os dados textuais s�o facilmente movidos de um sistema para outro e s�o a


forma port�vel mais simples de trocar informa��es arbitr�rias entre os sistemas.

Use o texto para a troca de dados. O texto � f�cil de manipular com outras
ferramentas e de ser processado de maneiras inesperadas. Por exemplo, quando a
sa�da de um programa n�o serve como sa�da para outro, um script Awk ou Perl pode
ser usado para ajust�-lo; grep pode ser usada para selecionar ou descartar linhas;
seu editor preferido pode ser usado para fazer altera��es mais complicadas. Os
arquivos de texto s�o muito mais f�ceis de documentar e podem nem precisar de muita
documenta��o, uma vez que as pessoas podem l�-los. Um coment�rio em um arquivo de
texto pode indicar qual vers�o do software � necess�ria para processar os dados. A
primeira linha de um arquivo PostScript, por exemplo, identifica a codifica��o:

%!PS-Adobe-2.0

Os arquivos bin�rios, por outro lado, precisam de ferramentas especializadas


e raramente podem ser usados juntos nem que seja na mesma m�quina. Uma variedade de
programas amplamente usados converte os dados bin�rios arbitr�rios em texto, para
que ele possa ser enviado com menos chance de dano. Eles incluem o binhex para os
sistemas Macintosh, uuencode e uudecode para Unix e diversas ferramentas que usam a
codifica��o MIME para transferir dados bin�rios das mensagens de correio
eletr�nico. No Cap�tulo 9, mostramos uma fam�lia de rotinas de compacta��o e
descompacta��o para codificar os dados bin�rios de forma port�vel para a
transmiss�o. A grande variedade de tais ferramentas indica os problemas dos
formatos bin�rios.
H� uma irrita��o cont�nua na troca de texto: os sistemas de PC usam um
retorno de carro '\r' e um newline ou alimenta��o de linha '\n' para encerrar cada
linha, enquanto que os sistemas Unix usam apenas o newline. O retorno de carro � um
artefato de um dispositivo antigo, chamado teletipo, que tinha uma opera��o de
retorno de carro (CR) para retornar o mecanismo de digita��o ao in�cio de uma
linha, e uma opera��o separada de line-feed (LF) para avan��-lo para a linha
seguinte.
Embora os computadores de hoje n�o tenham carros a serem retornados, o
software de PC em sua maior parte continua esperando a combina��o (conhecida como
CRLF) em cada linha. Se n�o houver retornos de carro, um arquivo pode ser
interpretado como uma linha gigante. As contagens de linha e caractere podem estar
erradas ou mudar inesperadamente. Alguns softwares se adaptam bem, mas a maioria
n�o. Os PCs n�o s�o os �nicos culpados. Gra�as a uma seq��ncia de compatibilidades
incrementais, alguns padr�es de rede modernos, tais como o HTTP, tamb�m usam o CRLF
para delimitar as linhas.
Nosso conselho � o uso das interfaces padr�o, as quais tratam o CRLF de
forma consistente em qualquer sistema, seja (nos PCs) por meio da remo��o de \r na
entrada e de seu acr�scimo de volta na sa�da, ou (no Unix) sempre usando \n em vez
de CRLF para delimitar as linhas nos arquivos. Para os arquivos que devem ser
movidos de um lado para outro, um programa para converter os arquivos de cada
formato para o outro � uma necessidade.

� Exerc�cio 8-2. Escreva um programa para remover os retornos de carro de um


arquivo. Escreva um segundo programa para adicion�-los substituindo cada newline
por um retorno de carro e newline. Como voc� testaria esses programas? ?

8.6 Ordem de byte

Apesar das desvantagens discutidas acima, os dados bin�rios �s vezes s�o


necess�rios. Eles podem ser significativamente mais compactos e r�pidos de
decodificar, fatores que os tornam essenciais para muitos problemas da computa��o
de rede. Mas os dados bin�rios apresentam problemas s�rios de portabilidade.
Pelo menos uma quest�o est� decidida: todas as m�quinas modernas t�m bytes
de 8 bits. M�quinas diferentes t�m representa��es diferentes de todo objeto maior
do que um byte, por�m, de modo que depender de propriedades espec�ficas � um erro.
Um inteiro curto (geralmente com 16 bits, ou dois bytes) pode ter seu byte de ordem
baixa armazenado em um endere�o mais baixo do que um byte de ordem alta (little-
endian), ou um endere�o mais alto (big endian). A op��o � arbitr�ria e algumas
m�quinas suportam at� mesmo ambos os modos.
Assim sendo, embora as m�quinas big e little endian vejam a mem�ria como uma
seq��ncia de palavras na mesma ordem, elas interpretam os bytes dentro de uma
palavra na ordem oposta. Neste diagrama, os quatro bytes que iniciam na localiza��o
0 representar�o o inteiro hexadecimal 0x11223344 em uma m�quina big-endian e
0x44332211 em uma little-endian.

Para ver a ordem de byte em a��o experimente o seguinte programa:

/* byteorder: exibe os bytes de um long */


int main(void)
{
unsigned long x;
unsigned char *p;
int i;

/* 11 22 33 44 => big-endian */
/* 44 33 22 11 => little-endian */
/* x = 0x1122334455667788UL; para long de 64 bits */

x = 0x11223344UL;
p = (unsigned char *) &x;
for (i = 0; i < sizeof(long); i++)
printf("%x ", *p++);
printf("\n");
return 0;
}

Em uma m�quina big-endian de 32 bits, a sa�da �:

11 22 33 44

mas em uma m�quina little-endian ela �:

44 33 22 11

e na PDP-11 (uma m�quina antiga de 16 bits ainda encontrada nos sistemas


incorporados) ela �:

22 11 44 33

Nas m�quinas com longs de 64 bits, podemos tornar a constante maior e ver
comportamentos semelhantes.
Isso pode parecer um programa ing�nuo, mas se quisermos enviar um inteiro
para uma interface com largura de byte, tal como uma conex�o de rede, precisamos
escolher qual byte deve ser enviado primeiro, e essa op��o � em ess�ncia a decis�o
entre big-endian e little-endian. Em outras palavras, esse programa est� fazendo
explicitamente aquilo que

fwrite(&x, sizeof(x), 1, stdout);

faz implicitamente. N�o � seguro gravar um int (ou short ou long) de um computador
e l�-lo como um int em outro computador.
Por exemplo, se o computador de origem gravar com

unsigned short x;
fwrite(&x, sizeof(x), 1, stdout);

e o computador de destino ler com

unsigned short x;
fread(&x, sizeof(x), 1, stdin);

o valor de x n�o ser� preservado quando as m�quinas tiverem ordens de byte


diferentes. Se x come�ar como 0x1000 ele pode chegar como 0x0010.
Esse problema freq�entemente � solucionado com a compila��o condicional e a
"troca de bytes", alguma coisa mais ou menos assim:

? short x;
? fread(&x, sizeof(x), 1, stdin);
? #ifdef BIG_ENDIAN
? /* swap bytes */
? x = ((x&0xFF) " 8) | ((x"8)& 0xFF);
? #endif

Essa abordagem se torna incontrol�vel quando muitos inteiros de dois e


quatro bytes est�o sendo trocados. Na pr�tica, os bytes acabam sendo trocados mais
de uma vez ao passarem de um lugar para outro.
Se a situa��o for ruim para short, ela ser� pior para os tipos de dados mais
longos, porque existem muitas maneiras de permutar os bytes. Adicione o enchimento
de vari�vel entre os membros de estrutura, restri��es de alinhamento e as
misteriosas ordens de byte das m�quinas mais antigas e o problema fica intrat�vel.
Use uma ordem de byte fixa para a troca de dados. Existe uma solu��o. Escreva os
bytes em uma ordem usando o c�digo port�vel:

unsigned short x;
putchar(x " 8); /* escrever byte de ordem alta */
putchar(x & 0xFF); /* escrever byte de ordem baixa */

depois leia de volta um byte de cada vez e monte-o novamente:

unsigned short x;
x = getchar() " 8; /* ler o byte de ordem alta */
x |= getchar() & 0xFF; /* ler o byte de ordem baixa */

A abordagem pode ser generalizada para as abordagens se voc� gravar os


valores dos membros da estrutura em uma sequ�ncia definida, um byte de cada vez,
sem o enchimento. N�o importa a ordem de byte escolhida; tudo que for consistente
serve. O �nico requisito � que o remetente e destinat�rio concordem com a ordem de
byte na transmiss�o e com o n�mero de bytes de cada objeto. No cap�tulo seguinte
mostramos um par de rotinas para fazer o wrap up da compacta��o e descompacta��o de
dados gerais.
O processamento de um byte de cada vez pode parecer caro, mas comparado �
E/S que torna a compacta��o e descompacta��o necess�ria, a penalidade n�o � muito
alta. Pense no sistema X Window, no qual o cliente grava os dados em sua ordem de
byte nativa e o servidor deve fazer o unpack daquilo que o cliente enviar. Isso
pode economizar algumas poucas instru��es no lado do cliente, mas o servidor fica
maior e mais complicado pela necessidade de lidar com ordens de byte m�ltiplas ao
mesmo tempo - ele pode ter clientes big-endian e little-endian simult�neos - e o
custo para a complexidade e o c�digo � muito mais significativo. Al�m disso, esse �
um ambiente gr�fico onde a overhead para fazer o pack dos bytes ser� superada pela
execu��o da opera��o gr�fica que ela codifica.
O sistema X Window negocia uma ordem de byte para o cliente e requer que o
servidor seja capaz de realizar ambas. O sistema operacional Plan 9, por outro
lado, define uma ordem de byte para as mensagens com o servidor de arquivos (ou com
o servidor de gr�ficos) e os dados s�o compactados e descompactados com c�digo
port�vel, como acima. Na pr�tica o efeito do tempo de execu��o n�o pode ser
detectado. Comparado � E/S, o custo da compacta��o dos dados � insignificante.

Java � uma linguagem de n�vel mais alto do que C ou C++ e oculta


completamente a ordem de byte. As bibliotecas fornecem uma interface Serializable
que define como � feita a compacta��o dos itens de dados para a troca.
Se voc� trabalha com C ou C++, por�m, deve fazer o trabalho sozinho. O
principal ponto de uma abordagem um byte de cada vez � que ela soluciona o
problema, sem #ifdefs, para todas as m�quinas que t�m byte de 8 bits. Vamos
discutir isso com mais detalhes no pr�ximo cap�tulo.
Mesmo assim, a melhor solu��o quase sempre � converter as informa��es para o
formato de texto, o qual (exceto pelo problema do CRLF) � completamente port�vel.
N�o existe ambiguidade na representa��o. Nem sempre, por�m, ela � a
resposta correta. O tempo ou espa�o podem ser cr�ticos e alguns dados,
particularmente o ponto flutuante, podem perder a precis�o devido ao arredondamento
quando s�o passados por meio de printf e scanf. Se voc� precisar trocar valores de
ponto flutuante com precis�o, verifique se voc� tem uma biblioteca de E/S bem
formatada. Tais bibliotecas existem, mas talvez n�o fa�am parte do seu ambiente. �
particularmente dif�cil representar os valores de ponto flutuante de forma port�vel
em bin�rios, mas com cuidado o texto far� o trabalho.
Existe uma quest�o sutil de portabilidade no uso das fun��es padr�o para
lidar com os arquivos bin�rios - � preciso abrir tais arquivos no modo bin�rio:

FILE *fin;
fin = fopen(binary_file, "rb");
c = getc(fin);

Se o ' b' for omitido, nos sistemas Unix isso n�o faz diferen�a, mas nos sistemas
Windows o primeiro byte control-Z (octal 032, hex 1A ) da entrada terminar� a
leitura (vimos isso acontecer para o programa strings no Cap�tulo 5). Por outro
lado, o uso do modo bin�rio para ler os arquivos de texto far� com que \r seja
preservado na entrada e n�o seja gerado na sa�da.

8.7 Portabilidade e atualiza��o

Uma das fontes mais frustrantes de problemas de portabilidade � o software


de sistema que muda durante seu ciclo de vida. Essas altera��es podem ocorrer com
qualquer interface do sistema, causando incompatibilidades gratuitas entre as
vers�es existentes dos programas.

Altere o nome se voc� mudar a especifica��o. Nosso exemplo preferido (se essa pode
ser a palavra) s�o as propriedades que mudam do comando echo do Unix, cujo projeto
inicial era apenas fazer o eco de seus argumentos:

% echo hello, world


hello, world
%

Entretanto, echo se transformou em uma das principais partes de muitos scripts de


shell, e a necessidade de gerar sa�da formatada se tornou importante. Assim sendo,
echo foi alterado para interpretar seus argumentos, meio como printf:

% echo 'hello\nworld'
hello
world
%

Esse novo recurso � �til, mas causa problemas de portabilidade para todo script
shell que dependa do comando echo para fazer qualquer outra coisa que n�o seja o
eco. O comportamento de

% echo $PATH

agora depende da vers�o de echo que n�s temos. Se a vari�vel por acidente tiver uma
barra invertida, como pode acontecer no DOS ou Windows, ela pode ser interpretada
por echo. A diferen�a � semelhante �quela que existe entre a sa�da de printf(str) e
printf("%s", str) se a string str tiver um sinal de porcento.
Contamos apenas parte da hist�ria toda de echo, mas isso ilustra o problema
b�sico: as altera��es nos sistemas podem gerar vers�es diferentes do software que
se comporta intencionalmente de modo diferente, levando a problemas n�o-
intencionais de portabilidade. E os problemas s�o muito dif�ceis de serem
contornados. Se a vers�o nova de echo tivesse um nome diferente, os problemas
seriam bem menores.
Como um exemplo mais direto, considere o comando sum do Unix, o qual imprime
o tamanho e o checksum de um arquivo. Ele foi escrito para verificar se uma
transfer�ncia de informa��es foi bem-sucedida:

% sum file
52313 2 file
%
% copy file to other machine
%
% telnet othermachine
$
$ sum file
52313 2 file
$

O checksum � o mesmo ap�s a transfer�ncia; portanto, n�s podemos ter confian�a de


que as c�pias antiga e nova s�o id�nticas.
Depois disso os sistemas proliferaram, as vers�es mudaram, e algu�m notou
que o algoritmo de checksum n�o era perfeito, de modo que sum foi modificado para
usar um algoritmo melhor. Outra pessoa fez a mesma observa��o e deu a sum um
algoritmo diferente melhor. E assim por diante at� que hoje h� v�rias vers�es de
sum, cada uma dando uma resposta diferente. Copiamos um arquivo para m�quinas
pr�ximas para ver o que sum calculava:

% sum file
52313 2 file
%
% copy file to machine 2
% copy file to machine 3
% telnet machine2
$
$ sum file
eaa0d468 713 file
$ telnet machine3
>
> sum file
62992 1 file
>

O arquivo est� danificado, ou apenas temos vers�es diferentes de sum? Talvez


ambos.
Assim sendo, sum � o desastre completo em termos de portabilidade: um
programa destinado a auxiliar na c�pia de software de uma m�quina para outra com
vers�es diferentes incompat�veis que o tornam in�til para o seu prop�sito original.
Para sua tarefa simples, o sum original estava bom. Seu algoritmo checksum
de baixa tecnologia era adequado. O seu "fixing" pode t�-lo tornado um programa
melhor, mas n�o muito, e certamente n�o o suficiente para que a incompatibilidade
compensasse. O problema n�o s�o os aperfei�oamentos, mas que os programas
incompat�veis t�m o mesmo nome. A altera��o introduziu um problema de vers�o que
nos atormentou durante anos.

Mantenha a compatibilidade com os programas e dados existentes. Quando uma vers�o


nova de software tal como um processador de texto � criada, � comum que ele leia os
arquivos produzidos com a vers�o antiga. Isso � o que se espera: � medida que
recursos n�o-previstos s�o adicionados, o formato deve evoluir. Mas as vers�es
novas �s vezes n�o fornecem um modo de gravar o arquivo no formato anterior. Os
usu�rios da vers�o nova, mesmo que n�o usem os recursos novos, n�o podem
compartilhar de seus arquivos com as pessoas que usam o software mais antigo e
todos s�o for�ados a fazer a atualiza��o. Seja isso um deslize de engenharia ou uma
estrat�gia de marketing, � algo lament�vel.
A compatibilidade inversa � a habilidade que um programa tem de ser
compat�vel com sua especifica��o mais antiga. Se voc� vai alterar um programa,
verifique se n�o est� quebrando o software e os dados antigos que dependem dele.
Documente bem as altera��es e forne�a modos de recuperar o comportamento antigo. O
mais importante � considerar se a altera��o que voc� est� propondo � um
aperfei�oamento verdadeiro quando comparado ao custo de toda n�o-portabilidade que
voc� vai introduzir.
8.8 Internacionaliza��o

Se algu�m mora nos Estados Unidos � f�cil esquecer que o ingl�s n�o � a
�nica l�ngua, que ASCII n�o � o �nico conjunto de caracteres, $ n�o � o �nico
s�mbolo de moeda, as datas podem ser escritas com o dia primeiro, as horas podem se
basear em um rel�gio de 24 horas e assim por diante. Portanto, outro aspecto da
portabilidade, no sentido amplo, trata de como tornar os programas port�veis em
todos os limites de l�ngua e cultura. Isso � um assunto potencialmente extenso, mas
n�s temos espa�o apenas para destacar algumas das quest�es principais.
A internacionaliza��o � o termo para fazer um programa ser executado sem
suposi��es sobre seu ambiente cultural. Os problemas s�o muitos, variando desde os
conjuntos de caracteres at� a interpreta��o dos �cones das interfaces.

N�o assuma o ASCII. Os conjuntos de caracteres s�o mais ricos do que o ASCII na
maior parte do mundo. As fun��es do teste padr�o de caracteres em ctype.h
geralmente ocultam essas diferen�as:

if (isalpha(c)) ...

� independente do c�digo espec�fico de caracteres e, al�m disso, vai funcionar


corretamente nos locais onde h� mais ou menos letras do que aquelas que est�o entre
a e z, se o programa � compilado naquele local. � claro que at� mesmo o nome
isalpha revela suas origens; alguns idiomas n�o t�m nenhum alfabeto.
A maioria dos pa�ses europeus aumenta a codifica��o do ASCII, o qual define
valores somente at� 0x7F (7 bits), com caracteres extras que representam as letras
de seus idiomas. A codifica��o Latin-1, normalmente usada em toda a Europa
Ocidental, � um superconjunto ASCII que especifica valores de byte de 80 a FF para
os s�mbolos e caracteres acentuados. E7, por exemplo, representa a letra �. A
palavra do ingl�s boy � representada em ASCII (ou Latin-1) por tr�s bytes com os
valores hexadecimais 62 6F 79, enquanto que a palavra gar�on do franc�s �
representada no Latin-1 pelos bytes 67 61 72 E7 6F 6E. Outros idiomas definem
outros s�mbolos, mas eles n�o podem caber todos nos 128 valores n�o-utilizados pelo
ASCII, portanto existe uma variedade de padr�es conflitantes para os caracteres
atribu�dos aos bytes 80 at� FF.
Alguns idiomas n�o cabem nos 8 bits. Existem milhares de caracteres nos
principais idiomas asi�ticos. Os c�digos usados na China, no Jap�o e na Cor�ia t�m
todos 16 bits por caractere. Como resultado, a leitura de um documento gravado em
um idioma em um computador configurado para outro idioma � um grande problema de
portabilidade. Assumindo que os caracteres cheguem intactos, a leitura de um
documento chin�s em um computador americano envolve, no m�nimo, software e fontes
especiais. Se quisermos usar o chin�s, ingl�s e russo juntos, os obst�culos s�o
incr�veis.
O conjunto de caracteres Unicode � uma tentativa de aliviar essa situa��o
fornecendo um �nico c�digo para todos os idiomas do mundo. O Unicode, que �
compat�vel com o subconjunto de 16 bits do padr�o ISO 10646, usa 16 bits por
caractere, com os valores 00FF e abaixo correspondendo ao Latin-1. Assim sendo, a
palavra gar�on � representada pelos valores de 16 bits 0067 0061 0072 00E7 006F
006E, enquanto que o alfabeto cir�lico ocupa valores de 0401 at� 04FF e os idiomas
de ideogramas ocupam um bloco grande que come�a no 3000. Todos os idiomas
conhecidos, e muitos nem t�o conhecidos assim, est�o representados no Unicode;
portanto, esse � o c�digo preferido para transferir documentos entre pa�ses ou para
armazenar texto multil�ngi�e. O Unicode est� se tornando conhecido na Internet e
alguns sistemas o suportam como formato padr�o. A Java, por exemplo, usa o Unicode
como seu conjunto de caracteres nativo para as strings. Os sistemas operacionais
Plan 9 e Inferno usam o Unicode, at� nos nomes de arquivos e usu�rios. O Microsoft
Windows suporta o conjunto de caracteres Unicode, mas n�o o torna obrigat�rio. A
maioria dos aplicativos Windows ainda funciona melhor com o ASCII, mas a pr�tica
est� evoluindo rapidamente na dire��o do Unicode.
Entretanto, o Unicode apresenta um problema: os caracteres n�o cabem mais em
um byte, de modo que o texto do Unicode sofre da confus�o de ordem de byte. Para
evitar isso, os documentos do Unicode geralmente s�o traduzidos para uma
codifica��o de fluxo de byte chamada UTF-8 antes de serem enviados entre programas
ou por uma rede. Cada caractere de 16 bits � codificado como uma sequ�ncia de 1, 2
ou 3 bytes para a transmiss�o. O conjunto de caracteres ASCII usa valores de 00 at�
7F, sendo que todos eles cabem em um �nico byte usando o UTF-8, portanto ele tem
compatibilidade inversa com o ASCII. Os valores entre 80 e 7FF s�o representados em
dois bytes, e os valores 800 e acima s�o representados em tr�s bytes. A palavra
gar�on aparece no UTF-8 como os bytes 67 61 72 C3 A7 6F 6E; o valor Unicode E7, o
caractere �, � representado como os dois bytes C3 A7 no UTF-8.
A compatibilidade inversa do UTF-8 e ASCII � muito boa, pois permite que os
programas que tratam o texto como um fluxo n�o-interpretado de bytes funcionem com
o texto Unicode em qualquer idioma. Experimentamos os programas Markov do Cap�tulo
3 no texto codificado com o UTF-8 em russo, grego, japon�s e chin�s e eles foram
executados sem problemas. Para os idiomas europeus, cujas palavras s�o separadas
por espa�o ASCII, tab ou newline, a sa�da foi um absurdo razo�vel. Para os outros,
seria preciso alterar as regras de quebra de palavra para obter sa�da mais pr�xima
em esp�rito da inten��o do programa.
C e C++ suportam os "caracteres grandes", que s�o inteiros de 16 bits ou
maiores e algumas fun��es que os acompanham e que podem ser usados para processar
os caracteres do Unicode ou outros conjuntos de caracteres grandes. Os literais de
string de caractere s�o escritos como L"...", mas eles apresentam outros problemas
de portabilidade: um programa com constantes de caractere grande s� pode ser
entendido quando examinado em um monitor que use o conjunto de caracteres. Como os
caracteres devem ser convertidos para fluxos de byte tais como o UTF-8 para
transmiss�o port�vel entre m�quinas, a C fornece fun��es para converter caracteres
grandes para e de bytes. Mas qual convers�o devemos usar? A interpreta��o do
conjunto de caracteres e a defini��o da codifica��o de fluxo de bytes est�o ocultas
nas bibliotecas e s�o dif�ceis de extrair. Essa situa��o � no m�ximo
insatisfat�ria. � poss�vel que no futuro todos concordem com o conjunto de
caracteres a ser usado, mas um cen�rio mais prov�vel ser� a confus�o restante dos
problemas de ordem de byte que nos atormentam.

N�o assuma o ingl�s. Os criadores de interfaces devem se lembrar de que os


idiomas diferentes assumem n�meros significativamente diferentes de caracteres para
dizer a mesma coisa; portanto, deve haver espa�o suficiente na tela e nos arrays.
E as mensagens de erro? No m�nimo, elas devem estar livres do jarg�o e g�ria
que s� ter�o significado entre uma popula��o selecionada. Um bom ponto de partida �
escrev�-las em linguagem simples. Uma t�cnica comum � coletar o texto de todas as
mensagens de um lugar, para que elas sejam substitu�das facilmente pelas tradu��es
para outros idiomas.
Existem muitas depend�ncias culturais, como o formato de data mm/dd/aa que �
usado apenas na Am�rica do Norte. Se houver alguma perspectiva de que o software
ser� usado em outro pa�s, esse tipo de depend�ncia deve ser evitado ou minimizado.
Os �cones das interfaces gr�ficas quase sempre dependem da cultura. Muitos �cones
s�o incompreens�veis para os nativos do ambiente de destino, sem contar para as
pessoas de outras culturas.

8.9 Resumo

O c�digo port�vel � um ideal pelo qual vale lutar, uma vez que grande parte
do tempo � gasta fazendo altera��es para passar um programa de um sistema para
outro, ou para mant�-lo funcionando � medida que ele evolui e o sistema no qual ele
� executado muda. Entretanto, a portabilidade n�o � f�cil. Ele exige cuidado na
implementa��o e no conhecimento das quest�es de portabilidade em todos os sistemas
de destino em potencial.
Simulamos as duas abordagens para a portabilidade de uni�o e intersec��o. A
abordagem da uni�o significa escrever vers�es que funcionam em cada destino,
incorporando o c�digo com mecanismos como a compila��o condicional. As desvantagens
s�o muitas: � preciso mais c�digo e quase sempre c�digo mais complicado, � dif�cil
manter-se atualizado e mais dif�cil testar.
A abordagem da intersec��o � escrever o m�ximo poss�vel de c�digo em uma
forma que funcionar� sem altera��es em todos os sistemas. As inevit�veis
depend�ncias de sistema s�o encapsuladas em arquivos-fonte simples que agem como
uma interface entre o programa e o sistema b�sico. A abordagem da intersec��o
tamb�m tem desvantagens, incluindo a perda potencial de efici�ncia e at� mesmo de
recursos, mas a longo prazo, os benef�cios ultrapassam os custos.

Leitura suplementar

Existem muitas descri��es para as linguagens de programa��o, mas algumas s�o


suficientemente precisas para servir como refer�ncias definitivas. Os autores
admitem uma certa tend�ncia pessoal para o livro The C Programming Language, de
Brian Kernighan e Dennis Ritchie (Prentice Hall, 1988), mas ele n�o � um substituto
do padr�o. O livro C: A Reference Manual, de Sam Harbison e Guy Steele (Prentice
Hall, 1994), j� em sua quarta edi��o, traz bons conselhos sobre a portabilidade da
C. Os padr�es oficiais da C e C++ est�o dispon�veis na ISO, a International
Organization for Standardization. A coisa mais pr�xima de um padr�o oficial para a
Java � o livro The Java Language Specification, de James Gosling, Bill Joy e Guy
Steele (Addison-Wesley, 1996).
O livro Advanced Programming in the Unix Environment (Addison-Wesley, 1992),
de Rich Stevens, � um recurso excelente para os programadores em Unix, e fornece
cobertura ampla das quest�es da portabilidade entre as variantes do Unix.
A POSIX, Portable Operating System Interface, � um padr�o internacional que
define os comandos e as bibliotecas com base no Unix. Ela fornece um ambiente
padr�o, portabilidade do c�digo-fonte para os aplicativos e uma interface uniforme
para a E/S, sistemas de arquivos e processos. Ela � descrita em uma s�rie de livros
publicados pelo IEEE.
O termo "big-endian" foi criado por Jonathan Swift em 1726. O artigo de
Danny Cohen, "On holy wars and a plea for peace", IEEE Computer, outubro de 1981, �
uma f�bula maravilhosa sobre a ordem de byte que introduziu os termos "endian" na
computa��o.
O sistema Plan 9 desenvolvido na Bell Labs tornou a portabilidade uma
prioridade central. O sistema compila a partir da mesma fonte sem #ifdef em uma
variedade de processadores e usa o conjunto de caracteres Unicode em todos eles. As
vers�es recentes do Sam (descrito pela primeira vez em "The Text Editor sam",
Software - Practice and Experience, 17, 11, p�gs. 813-845, 1987) usam o Unicode,
mas s�o executadas em uma ampla variedade de sistemas. Os problemas ao se lidar com
os conjuntos de caracteres de 16 bits, tais como o Unicode, s�o discutidos no
documento de Rob Pike e Ken Thompson, "Hello World or KaXrin�pa K�OHE or ????????"
Proceedings of the Winter 1993 USENIX Conference", San Diego, 1993, p�gs. 43-50. A
codifica��o do UTF-8 faz sua primeira apari��o nesse documento. Ele tamb�m est�
dispon�vel no site na Web do Plan 9 da Bell Labs, bem como a vers�o atual do Sam.
O sistema Inferno, que se baseia na experi�ncia do Plan 9, � meio an�logo ao
Java, pois define uma m�quina virtual que pode ser implementada em qualquer m�quina
real, fornece uma linguagem (Limbo) que � traduzida em instru��es para essa m�quina
virtual e usa o Unicode como seu conjunto de caracteres nativos. Ele tamb�m inclui
um sistema operacional virtual que fornece uma interface port�vel para uma
variedade de sistemas comerciais. Ele � descrito em "The Inferno Operating System"
de Sean Dorward, Rob Pike, David Leo Presotto, Dennis M. Ritchie, Howard W Trickey
e Philip Winterbottom, Bell Labs Technical Journal, 2, 1, inverno de 1997.

9
Nota��o

Entre todas as cria��es do homem, a linguagem talvez seja a mais surpreendente.


Giles Lytton Strachey, Words and Poetry
A linguagem certa pode ser a diferen�a na facilidade de escrever um
programa. Por esse motivo o arsenal de um programador cont�m n�o apenas as
linguagens de prop�sito geral, tais como C e suas parentes, como tamb�m shells
program�veis, linguagens de cria��o de scripts e muitas linguagens espec�ficas de
aplicativo.
O poder da boa nota��o vai al�m da programa��o tradicional e invade os
dom�nios dos problemas especializados. As express�es regulares nos permitem
escrever defini��es compactas (e eventualmente cr�ticas) das classes de strings; a
HTML nos permite definir o layout dos documentos interativos, usando quase sempre
os programas incorporados em outras linguagens, tais como JavaScript; a PostScript
expressa todo um documento - este livro, por exemplo - como um programa com estilo.
As planilhas e os processadores de texto quase sempre incluem linguagens de
programa��o como Visual Basic para avaliar express�es, acessar informa��es ou
controlar o layout.
Se voc� escreve muito c�digo para um trabalho comum, ou se voc� tem
problemas em expressar o processo de maneira confort�vel, talvez esteja usando a
linguagem errada. Se a linguagem certa n�o existir ainda, essa seria uma boa
oportunidade para cri�-la. A inven��o de uma linguagem n�o significa
necessariamente construir a sucessora da Java. Quase sempre um problema complicado
pode ser solucionado com uma altera��o de nota��o. Pense nas strings de formato da
fam�lia printf, as quais s�o uma maneira compacta e expressiva de controlar a
exibi��o dos valores impressos.
Neste cap�tulo vamos falar sobre como a nota��o pode solucionar problemas e
demonstrar algumas das t�cnicas que voc� pode usar para implementar suas pr�prias
linguagens de prop�sito especial. Vamos at� mesmo explorar as possibilidades de
fazer com que um programa escreva outro programa, um uso aparentemente extremo da
nota��o que acontece com frequ�ncia e � bem mais f�cil do que muitos programadores
imaginam.

9.1 Formatando os dados

Existe sempre uma lacuna entre aquilo que queremos dizer ao computador
("solucione meu problema") e aquilo que se deve dizer para que o trabalho seja
feito. Quanto menor essa lacuna, melhor. A boa nota��o facilita aquilo que queremos
dizer e torna mais dif�cil dizer a coisa errada por engano. Eventualmente, a boa
nota��o pode fornecer novas ideias, permitindo a solu��o de problemas que pareciam
dif�ceis, ou mesmo nos leva a novas descobertas.
As linguagens pequenas s�o nota��es especializadas para dom�nios estreitos.
Elas n�o apenas fornecem uma boa interface como tamb�m ajudam a organizar o
programa que as implementa. As sequ�ncias de controle printf s�o um bom exemplo:

printf("%d %6.2f %-10.10s\n", i, f, s);

Cada % da string de formato sinaliza um lugar para interpolar o valor do pr�ximo


argumento printf; ap�s alguns flags e larguras de campos opcionais, a �ltima letra
diz o tipo de par�metro que pode ser esperado. Essa nota��o � compacta, intuitiva e
f�cil de escrever, e a implementa��o � direta. As alternativas na C++ (iostream) e
Java (java.io) parecem mais estranhas uma vez que n�o fornecem nota��o especial,
embora se estendam para os tipos definidos pelo usu�rio e ofere�am a verifica��o de
digita��o.
Algumas implementa��es n�o-padr�o de printf permitem que voc� adicione suas
pr�prias convers�es ao conjunto incorporado. Isso � conveniente quando voc� tem
outros tipos de dados que precisam de convers�o de sa�da. Por exemplo, um
compilador poderia usar %L para o n�mero de linha e nome de arquivo; um sistema
gr�fico poderia usar %P para um ponto e %R para um ret�ngulo. A string cr�tica de
letras e n�meros para recuperar cota��es de a��es que vimos no Cap�tulo 4 tinha o
mesmo esp�rito, uma nota��o compacta para organizar combina��es de dados de a��es.
Podemos sintetizar exemplos semelhantes na C e C++. Suponhamos que voc�
queira enviar pacotes contendo diversas combina��es de tipos de dados de um sistema
para outro. Como vimos no Cap�tulo 8, a solu��o mais inteligente seria converter
para uma representa��o textual. Para um protocolo de rede padr�o, por�m, o formato
mais prov�vel � o bin�rio por motivos de efici�ncia ou tamanho. Como podemos
escrever o c�digo de tratamento de pacote para que ele seja port�vel, eficiente e
f�cil de usar?
Para tornar esta discuss�o concreta, imagine que planejamos enviar pacotes
com itens de dados de 8 bits, 16 bits e 32 bits de um sistema para outro. O ANSI C
diz que sempre podemos armazenar pelo menos 8 bits em um char, 16 bits em um short
e 32 bits em um long, de modo que usaremos esses tipos de dados para representar
nossos valores. Haver� muitos tipos de pacotes; o pacote do tipo 1 pode ter um
especificador de tipo de 1 byte, um count de 2 bytes, um valor de 1 byte e um item
de dado de 4 bytes:

O pacote do tipo 2 poderia conter um short e duas palavras de dados long:

Uma abordagem seria escrever as fun��es pack e unpack para cada tipo
poss�vel de pacote:

int pack_type1(unsigned char *buf, unsigned short count,


unsigned char val, unsigned long data)
{
unsigned char *bp;

bp = buf;
*bp++ = 0x01;
*bp++ = count " 8;
*bp++ = count;
*bp++ = val;
*bp++ = data " 24;
*bp++ = data " 16;
*bp++ = data " 8;
*bp++ = data;
return bp - buf;
}

Para um protocolo realista haver� dezenas dessas rotinas, todas varia��es sobre um
mesmo tema. As rotinas poderiam ser simplificadas usando macros ou fun��es para
lidar com os tipos b�sicos de dados (short, long e assim por diante), mas mesmo
assim tal c�digo repetitivo � f�cil de errar, dif�cil de ler e de atualizar.
A repeti��o inerente do c�digo � uma pista de que a nota��o pode ajudar.
Emprestando a ideia de printf, n�s podemos definir uma linguagem min�scula de
especifica��o na qual cada pacote � descrito por uma string curta que captura o
layout do pacote. Os elementos sucessivos do pacote s�o codificados com c para um
caractere de 8 bits, s para um inteiro short de 16 bits e l para um inteiro long de
32 bits. Assim sendo, por exemplo, o tipo 1 de pacote constru�do no nosso exemplo
acima, incluindo o byte de tipo inicial, poderia ser descrito pela string de
formato cscl. Depois podemos usar uma �nica fun��o pack para criar pacotes de
qualquer tipo. Este pacote seria criado com
pack(buf, "cscl", 0x01, count, val, data);

Como nossa string de formato cont�m apenas defini��es de dados, n�o h� necessidade
dos caracteres % usados por printf.
Na pr�tica, as informa��es do in�cio do pacote poderiam dizer ao destinat�rio
como decodificar o restante, mas vamos assumir que o primeiro byte do pacote possa
ser usado para determinar o layout. O remetente codifica os dados nesse formato e o
envia. O destinat�rio l� o pacote, escolhe o primeiro byte e o usa para decodificar
o que vem a seguir.
Aqui temos uma implementa��o de pack, a qual preenche buf com a representa��o
codificada de seus argumentos determinada pelo formato. Tornamos todos os valores
unsigned, incluindo os bytes do buffer de pacote, para evitar problemas de extens�o
de sinal. Tamb�m usamos alguns typedefs convencionais para manter as declara��es
curtas:

typedef unsigned char uchar;


typedef unsigned short ushort;
typedef unsigned long ulong;

Assim como sprintf, strcpy e fun��es semelhantes, pack assume que o buffer �
suficientemente grande para conter o resultado. A responsabilidade disso � de quem
chama. Tamb�m n�o existe nenhuma tentativa de detectar enganos entre o formato e a
lista de argumentos.

#include <stdarg.h>
/* pack: compacta os itens bin�rios para o buffer, retorna o
comprimento */
int pack(uchar *buf, char *fmt, ...)
{
va_list args;
char *p;
uchar *bp;
ushort s;
ulong l;

bp = buf;
va_start(args, fmt);
for (p = fmt; *p != '\0'; p++) {
switch (*p) {
case 'c': /* char */
*bp++ = va_arg(args, int);
break;
case 's': /* short */
s = va_arg(args, int);
*bp++ = s " 8;
*bp++ = s;
break;
case 'T: /* long */
l = va_arg(args, ulong);
*bp++ = l " 24;
*bp++ =1 " 16;
*bp++ = l " 8;
*bp++ = 1;
break;
default: /* caractere de tipo ilegal */
va_end(args);
return -1;
}
}
va_end(args);
return bp - buf;
}

A rotina pack usa mais o header stdarg.h do que eprintf usou no Cap�tulo 4.
Os argumentos sucessivos s�o extra�dos usando a macro va_arg, com o primeiro
operando da vari�vel do tipo va_list definido pela chamada de va_start e o segundo
operando do tipo do argumento (esse � o motivo pelo qual va_arg � uma macro, e n�o
uma fun��o). Quando o processamento termina, va_end deve ser chamada. Embora os
argumentos de 'c' e 's' representem os valores de char e short, eles devem ser
extra�dos como ints porque a C promove os argumentos char e short para int quando
eles s�o representados pelo par�metro de retic�ncias.
Cada rotina pack_type agora ter� uma linha de comprimento, dispondo seus
argumentos em uma chamada de pack:

/* pack_type1: compacta o pacote de formato 1 */


int pack_type1(uchar *buf, ushort count, uchar vai, ulong data)
{
return pack(buf, "cscl", 0x01, count, val, data);
}

Para descompactar, podemos fazer a mesma coisa: em vez de gravar c�digo


separado para dividir cada formato de pacote, n�s chamamos uma �nica unpack com uma
string de formato. Isso centraliza a convers�o em um �nico lugar:

/* unpack: descompacta os itens compactados do buf, retorna o


comprimento */
int unpack(uchar *buf, char *fmt, ...)
{
va_list args;
char *p;
uchar *bp, *pc;
ushort *ps;
ulong *pl;

bp = buf;
va_start(args, fmt);
for (p = fmt; *p != '\0'; p++) {
switch (*p) {
case 'c': /* char */
pc = va_arg(args, uchar*);
*pc = *bp++;
break;
case 's': /* short */
ps = va_arg(args, ushort*);
*ps = *bp++ " 8;
*ps |= *bp++;
break;
case 'l': /* long */
pi = va_arg(args, ulong*);
*pl = *bp++ " 24;
*pl |= *bp++ " 16;
*pl |= *bp++ " 8;
*pl |= *bp++;
break;
default: /* caractere de tipo ilegal */
va_end(args);
return -1;
}
}
va_end(args);
return bp - buf;
}

Assim como scanf, unpack deve retornar v�rios valores para quem chama, de modo que
seus argumentos s�o ponteiros para as vari�veis nas quais os resultados devem ser
armazenados. Seu valor de fun��o � o n�mero de bytes do pacote, o qual pode ser
usado para a verifica��o de erros.
Como os valores s�o unsigned e porque permanecemos dentro dos tamanhos que o
ANSI C define para os tipos de dados, esse c�digo transfere os dados de forma
port�vel mesmo entre m�quinas com tamanhos diferentes para short e long. Desde que
o programa que usa pack n�o tente enviar um valor que n�o pode ser representado em
32 bits como um long (por exemplo), o valor ser� recebido corretamente. Na verdade,
transferimos os 32 bits baixos do valor. Se precisarmos enviar valores maiores
podemos definir outro formato.
As rotinas espec�ficas de descompacta��o de tipo que chama unpack s�o
f�ceis:

/* unpack_type2: descompacta e processa pacotes do tipo 2 */


int unpack_type2(int n, uchar *buf)
{
uchar c;
ushort count;
ulong dwl, dw2;

if (unpack(buf, "csll", &c, &count, &dwl, &dw2) != n)


return -1;
assert(c == 0x02);
return process type2(count, dwl, dw2);
}

Para chamar unpack_type2 n�s devemos reconhecer que temos um pacote do tipo 2, o
que implica um loop do destinat�rio mais ou menos assim:

while ((n = readpacket(network, buf, BUFSIZ)) > 0) {


switch (buf[0]) {
default:
eprintf("bad packet type 0x%x", buf[0]);
break;
case 1:
unpack_type1(n, buf);
break;
case 2:
unpack_type2(n, buf);
break;
...
}
}

Esse estilo de programa��o pode se tornar muito longo. Um m�todo mais compacto �
definir uma tabela de ponteiros de fun��o cujas entradas s�o as rotinas de
descompacta��o indexadas por tipo:

int (*unpackfn[])(int, uchar *) = {


unpack_type0,
unpack_type1,
unpack_type2,
};
Cada fun��o da tabela analisa um pacote, verifica o resultado e inicia o
processamento adicional para aquele pacote. A tabela torna direto o trabalho do
destinat�rio:

/* receive: l� os pacotes da rede e os processa */


void receive(int network)
{
uchar type, buf [BUFSIZ];
int n;

while ((n = readpacket(network, buf, BUFSIZ)) > 0) {


type = buf[0];
if (type >= NELEMS(unpackfn))
eprintf("bad packet type 0x%x", type);
if ((*unpackfn[type])(n, buf) < 0)
eprintf("protocol error, type %x length %d", type, n);
}
}

Cada c�digo de tratamento de pacote � compacto, est� em um �nico lugar e �


f�cil de atualizar. O destinat�rio � altamente independente do protocolo em si. Ele
tamb�m � inteligente e r�pido.
Esse exemplo se baseia em parte de um c�digo real para um protocolo comercial
de rede. Depois que o autor percebeu que essa abordagem poderia funcionar, alguns
milhares de linhas repetidas e propensas a erros encolheram para algumas centenas
de linhas que s�o facilmente atualiz�veis. A nota��o reduz em muito a bagun�a.

� Exerc�cio 9-1. Modifique pack e unpack para transmitir os valores signed


corretamente, mesmo entre m�quinas com tamanhos diferentes para short e long. Como
voc� modificaria as strings de formato para especificar um item de dados signed?
Como voc� testa o c�digo para verificar, por exemplo, se ele transfere corretamente
um -1 de um computador com longs de 32 bits para um com longs de 64 bits? ?

� Exerc�cio 9-2. Estenda pack e unpack para lidar com as strings. Uma das
possibilidades seria incluir o comprimento da string na string de formato. Estenda-
as para lidar com itens repetidos com uma contagem. Como isso interage com a
codifica��o das strings? ?

� Exerc�cio 9-3. A tabela dos ponteiros de fun��o do programa C acima est� no


centro do mecanismo de fun��o virtual da C++. Reescreva pack, unpack e receive na
C++ para aproveitar essa conveni�ncia da nota��o. ?

� Exerc�cio 9-4. Escreva uma vers�o de linha de comando para printf que imprima o
segundo argumento e os argumentos subsequentes no formato dado pelo primeiro
argumento. Alguns shells j� fornecem isso como um recurso incorporado. ?

� Exerc�cio 9-5. Escreva uma fun��o que implementa as especifica��es de formato


encontradas nos programas de planilha ou na classe Decimal Format da Java e que
exiba os n�meros segundo os padr�es que indicam d�gitos obrigat�rios e opcionais,
localiza��o dos pontos e v�rgulas decimais e assim por diante. Para ilustrar, o
formato

##,##0.00

especifica o n�mero com duas casas decimais, pelo menos um d�gito � esquerda do
ponto decimal, uma v�rgula ap�s o d�gito de milhar e o preenchimento do espa�o em
branco at� a casa dos dez milhares. Isso representaria 12345.67 como 12,345.67 e .4
como _ _ _ _ _0.40 (usando sublinhados no lugar dos espa�os em branco). Para uma
especifica��o completa, consulte a defini��o de Decimal Format ou um programa de
planilha. ?

9.2 Express�es regulares

Os especificadores de formato para pack e unpack s�o uma nota��o muito


simples para definir o layout dos pacotes. Nosso pr�ximo t�pico � uma nota��o
ligeiramente mais complicada, mas muito mais expressiva, as express�es regulares,
as quais especificam os padr�es do texto. Usamos as express�es regulares
eventualmente em todo o livro sem defini-las com precis�o; elas s�o suficientemente
conhecidas para serem entendidas sem muitas explica��es. Embora as express�es
regulares estejam presentes em todo o ambiente de programa��o Unix, elas n�o s�o
t�o amplamente usadas em outros sistemas, de modo que nesta se��o demonstraremos
parte do seu poder. No caso de voc� n�o ter uma biblioteca de express�es regulares
� m�o, tamb�m mostraremos uma implementa��o rudimentar.
Existem v�rios tipos de express�es regulares, mas em esp�rito elas s�o todas
iguais, um modo de descrever os padr�es dos caracteres literais, junto com
repeti��es, alternativas e atalhos para classes de caracteres como d�gitos ou
letras. Um exemplo conhecido s�o os chamados "caracteres curinga" (wildcards)
usados nos processadores de linha de comando ou shells para coincidir padr�es de
nomes de arquivos. Geralmente o significado de * � assumido como "qualquer string
de caracteres" e, assim, um comando como este

C:\> del *.exe

usa um padr�o que coincide todos os arquivos cujos nomes consistem em qualquer
string que termine em ".exe". Como sempre, os detalhes diferem de um sistema para
outro, e mesmo de um programa para outro.
Embora os caprichos dos diferentes programas possam sugerir que as
express�es regulares n�o s�o um mecanismo ad hoc, na verdade elas s�o uma linguagem
com uma gram�tica formal e um significado preciso para cada express�o da linguagem.
Al�m disso, a implementa��o correta pode ser executada muito rapidamente. Uma
combina��o de teoria e pr�tica da engenharia faz muita diferen�a, um exemplo do
benef�cio dos algoritmos especializados ao qual nos referimos no Cap�tulo 2.
Uma express�o regular � uma sequ�ncia de caracteres que define um conjunto
de strings coincidentes. A maioria dos caracteres simplesmente faz sua pr�pria
coincid�ncia, de modo que a express�o regular abc coincidir� com aquela string de
letras sempre que ela ocorrer. Al�m disso alguns metacaracteres indicam a
repeti��o, o agrupamento ou posicionamento. Nas express�es regulares do Unix, ^
quer dizer o in�cio de uma string e $ o final, de modo que ^x coincide com um x
apenas no in�cio de uma string, x$ coincide com um x apenas no final, ^x$ coincide
com x apenas se ele for o �nico caractere da string e ~$ coincide com a string
vazia.
O caractere "." coincide com qualquer caractere, portanto x.y coincide com
xay, x2y e assim por diante, mas n�o com xy ou xaby, e *.$ coincide com uma string
com um �nico caractere arbitr�rio.
Um conjunto de caracteres dentro de colchetes [] coincide com qualquer um
dos caracteres que est�o inclu�dos, portanto [0123456789] coincide com um �nico
d�gito; ele pode ser abreviado como [0-9].
Esses elementos b�sicos se combinam a par�nteses para o agrupamento, | para
alternativos, * para zero ou mais ocorr�ncias, + para uma ou mais ocorr�ncias e ?
para zero ou uma ocorr�ncia. Finalmente, \ � usado como prefixo para citar um
metacaractere e desativar seu significado especial; \* � um literal * e \\ � uma
barra invertida literal.
A ferramenta de express�o regular mais conhecida � o programa grep que
mencionamos v�rias vezes. O programa � um exemplo maravilhoso do valor da nota��o.
Ele aplica uma express�o regular a cada linha de seus arquivos de entrada e imprime
essas linhas que cont�m strings coincidentes. Essa especifica��o simples, mais o
poder das express�es regulares, permite solucionar muitas tarefas do dia-a-dia. Nos
exemplos seguintes, observe que a sintaxe da express�o regular usada no argumento
para grep � diferente dos caracteres curinga usados para especificar um conjunto de
nomes de arquivo. Essa diferen�a reflete os v�rios usos.
Qual arquivo-fonte usa a class Regexp?

% grep Regexp *.java

Qual o implementa?

% grep 'class.*Regexp' *.java

Onde eu salvei aquela mensagem de Bob?

% grep '^From:.* bob@' mail/*

Quantas linhas de fonte n�o em branco existem nesse programa?

% grep '.' *.c++ | wc

Com flags para imprimir os n�meros de linha das linhas coincididas,


coincid�ncias de contagem, para fazer a coincid�ncia sem diferencia��o entre
mai�sculas e min�sculas, inverter o sentido (selecionar linhas que n�o coincidem
com o padr�o) e executar outras varia��es da ideia b�sica, grep � t�o amplamente
usada que se torna o exemplo cl�ssico da programa��o baseada na ferramenta.
Infelizmente, nem todo sistema vem com grep ou um equivalente. Alguns
sistemas incluem uma biblioteca de express�es regulares, em geral chamada regex ou
regexp, que voc� pode usar para escrever uma vers�o de grep. Se nenhuma op��o
estiver dispon�vel, � f�cil implementar um subconjunto modesto da linguagem
completa de express�o regular. Aqui, apresentamos uma implementa��o das express�es
regulares, e grep para ir junto com ela. Por quest�es de simplicidade, os �nicos
metacaracteres s�o ^ $ . e *, com * especificando uma repeti��o do ponto anterior
ou caractere literal. Esse subconjunto fornece uma grande parte do poder com uma
pequena parte da complexidade de programa��o das express�es gerais.
Vamos come�ar com a pr�pria fun��o de coincid�ncia. Seu trabalho �
determinar se uma string de texto coincide com uma express�o regular:

/* match: pesquisa regexp em qualquer lugar do texto */


int match(char *regexp, char *text)
{
if (regexp[0] == '^')
return matchhere(regexp+1, text);
do { /* deve procurar mesmo se a string estiver vazia */
if (matchhere(regexp, text))
return 1;
} while (*text++ != '\0');
return 0;
}

Se a express�o regular come�ar com ^, o texto deve come�ar com uma coincid�ncia do
restante da express�o. Caso contr�rio, caminhamos pelo texto, usando matchhere para
ver se o texto coincide em qualquer posi��o. Assim que encontramos uma
coincid�ncia, acabamos. Observe o uso de uma do-while: as express�es podem
coincidir com a string vazia (por exemplo, $ coincide com a string vazia no final
de uma linha e .* coincide com qualquer n�mero de caracteres, incluindo o zero),
portanto n�s devemos chamar matchhere mesmo quando o texto est� vazio. A fun��o
recursiva matchhere faz a maior parte do trabalho:

/* matchhere: pesquisa regexp no in�cio do texto */


int matchhere(char *regexp, char *text)
{
if (regexp[0] == '\0')
return 1;
if (regexp[1] == '*')
return matchstar(regexp[0], regexp+2, text);
if (regexp[0] == '$' && regexp[1] == '\0')
return *text == '\0';
if (*text!='\0' && (regexp[0] == '.' || regexp[0] == *text))
return matchhere(regexp+1, text+1);
return 0;
}

Se a express�o regular estiver vazia, n�s atingimos o final e encontramos uma


coincid�ncia. Se a express�o terminar com $, ela coincide apenas se o texto j� est�
no final. Se a express�o come�ar com um ponto, ela coincide com qualquer caractere.
Caso contr�rio, a express�o come�a com um caractere comum que coincide a si mesmo
no texto. Um ^ ou $ que apare�a na metade de uma express�o regular � tomado como um
caractere literal, n�o como um metacaractere.
Observe que matchhere chama a si mesmo ap�s coincidir com um caractere de
padr�o e string; portanto, a profundidade da recurs�o pode ser igual ao comprimento
do padr�o.
O �nico caso complicado ocorre quando a express�o come�a com um caractere
com estrela, por exemplo x*. Depois n�s chamamos matchstar, com o primeiro
argumento do operando da estrela (x) e os argumentos subsequentes do padr�o ap�s a
estrela e o texto.

/* matchstar: pesquisa c*regexp no in�cio do texto */


int matchstar(int c, char *regexp, char *text)
{
do { /* a * coincide com zero ou mais inst�ncias */
if (matchhere(regexp, text))
return 1;
} while (*text != '\0' && (*text++ == c || c =='.'));
return 0;
}

Aqui temos outro do-while, novamente disparado pelo requisito de que a express�o
regular x* possa coincidir com zero caracteres. O loop verifica se o texto coincide
com a express�o restante, tentando em cada posi��o do texto, desde que o primeiro
caractere coincida com o operando da estrela.
Essa � uma implementa��o pouco sofisticada, mas ela funciona, e com menos de
30 linhas de c�digo ela mostra que as express�es regulares n�o precisam de t�cnicas
avan�adas para serem usadas.
Em breve vamos apresentar algumas ideias para estender o c�digo. Por
enquanto, por�m, vamos escrever uma vers�o de grep que usa match. Aqui temos a
rotina restante:

/* grep main: pesquisa regexp nos arquivos */


int main(int argc, char *argv[])
{
int i, nmatch;
FILE *f;

setprogname("grep");
if (argc < 2)
eprintf("usage: grep regexp [file ...]");
nmatch = 0;
if (argc == 2) {
if (grep(argv[1], stdin, NULL))
nmatch++;
} else {
for (i =2; i < argc; i++) {
f = fopen(argv[i], "r");
if (f == NULL) {
weprintf("can't open %s:", argv[i]);
continue;
}
if (grep(argv[1], f, argc>3 ? argv[i] : NULL) > 0)
nmatch++;
fclose(f);
}
}
return nmatch == 0;
}

� conven��o que os programas C retornem 0 para sucesso e valores diferentes de zero


para diversas falhas. Nossa grep, assim como a vers�o do Unix, define o sucesso
como encontrar uma linha coincidente, de modo que ele retorna 0 quando h� alguma
coincid�ncia, 1 se n�o houver nenhuma e 2 (por meio de epritnf) quando ocorreu um
erro. Esses valores de status podem ser testados por outros programas como um
shell.
A fun��o grep examina um �nico arquivo, chamando match em cada linha:

/* grep: pesquisa regexp no arquivo */


int grep(char *regexp, FILE *f, char *name)
{
int n, nmatch;
char buf[BUFSIZ];

nmatch = 0;
while (fgets(buf, sizeof buf, f) != NULL) {
n = strlen(buf);
if (n > 0 && buf[n-1] == '\n')
buf[n-1] = '\0';
if (match(regexp, buf)) {
nmatch++;
if (name != NULL)
printf("%s:", name);
printf("%s\n", buf);
}
} return nmatch;
}

A rotina principal n�o sai quando falha para abrir um arquivo. Esse projeto
foi escolhido porque � comum dizer alguma coisa assim

% grep herpolhode *.*

e descobrir que um dos arquivos do diret�rio n�o pode ser lido. � melhor que grep
continue depois de reportar o problema, em vez de desistir e for�ar o usu�rio a
digitar a lista de arquivos manualmente para evitar o arquivo problema. Observe
tamb�m que grep imprime o nome do arquivo e a linha coincidente, mas suprime o nome
se estiver lendo entrada padr�o ou um �nico arquivo. Esse projeto pode parecer
estranho, mas reflete um estilo idiom�tico de uso com base na experi�ncia. Quando
dada apenas uma entrada, a tarefa de grep geralmente � a sele��o, e o nome do
arquivo encheria a sa�da. Mas se ela tiver de pesquisar em muitos arquivos, a
tarefa quase sempre � encontrar todas as ocorr�ncias de alguma coisa, e os nomes
s�o informativos. Compare

% strings markov.exe | grep 'DOS mode'

com

% grep grammer chapter*.txt

Esses toques s�o parte daquilo que torna a grep t�o conhecida, e demonstram que a
nota��o deve ter engenharia humana para construir uma ferramenta natural e efetiva.
Nossa implementa��o de match retorna assim que encontra uma coincid�ncia.
Para grep, esse � um bom padr�o. Mas para implementar um operador de substitui��o
(localizar e substituir) em um editor de texto a coincid�ncia mais longa e mais �
esquerda � mais adequada. Por exemplo, dado o texto "aaaaa" o padr�o a* coincide
com a string null no in�cio do texto, mas parece mais natural coincidir com todos
os cinco a's. Para fazer com que match encontre a string mais longa e mais �
esquerda, matchstar deve ser regravada para ser p�o duro: em vez de procurar em
cada caractere de texto da esquerda para a direita, ela deve pular para a string
mais longa que coincide com o operando de estrela, depois voltar se o restante da
string n�o coincidir com o restante do padr�o. Em outras palavras, ela deve ser
executada da direita para a esquerda. Aqui temos uma vers�o de matchstar que faz a
coincid�ncia mais longa e mais � esquerda:

/* matchstar: pesquisa c*regexp na mais longa e mais � esquerda */


int matchstar(int c, char *regexp, char *text)
{
char *t;

for (t = text; *t != '\0' && (*t == c || c == '.'); t++)


;
do { /* * coincide zero ou mais */
if (matchhere(regexp, t))
return 1;
} while (t- > text);
return 0;
}

N�o importa qual coincid�ncia grep encontra, uma vez que ela est� apenas
verificando a presen�a de qualquer coincid�ncia e imprimindo a linha inteira. Assim
sendo, como a coincid�ncia mais longa e mais � esquerda realiza um trabalho extra,
ela n�o � necess�ria para grep, mas para um operador de substitui��o ela �
essencial.
Nossa grep � competitiva com as vers�es fornecidas pelo sistema,
independente da express�o regular. Existem express�es patol�gicas que podem causar
o comportamento exponencial, tal como a*a*a*a*a*b quando recebe a entrada
aaaaaaaaac, mas o comportamento exponencial tamb�m est� presente em algumas
implementa��es comerciais. Uma variante grep dispon�vel no Unix, chamada egrep, usa
um algoritmo de coincid�ncia mais sofisticado que garante o desempenho linear
evitando o "backtrackine" quando determinada coincid�ncia falha.
E que tal fazer match lidar com as express�es regulares completas? Elas
incluiriam classes de caractere tais como [a-zA-Z] para coincidir com um caractere
alfab�tico, a habilidade de citar um metacaractere (por exemplo para pesquisar um
ponto literal), par�nteses para agrupamento e alternativas (abc ou def). A primeira
etapa � ajudar match compilando o padr�o em uma representa��o que � mais f�cil de
examinar. � caro analisar uma classe de caracteres sempre que a comparamos com um
caractere; uma representa��o pr�-calculada com base nos vetores de bit poderia
tornar as classes de caractere muito mais eficientes. Nas express�es regulares
completas, com par�nteses e alternativas, a implementa��o deve ser mais
sofisticada, mas pode usar algumas das t�cnicas das quais falaremos mais tarde
neste cap�tulo.

� Exerc�cio 9-6. Como o desempenho de match pode ser comparado a strstr quando se
pesquisa texto comum? ?

� Exerc�cio 9-7. Escreva uma vers�o n�o recursiva de matchhere e compare seu
desempenho com a vers�o recursiva. ?

�Exerc�cio 9-8. Adicione algumas op��es a grep. As mais conhecidas incluem -v para
inverter o sentido da coincid�ncia, -i para fazer a coincid�ncia que n�o faz
distin��o entre mai�sculas e min�sculas dos alfab�ticos, e -n para incluir os
n�meros das linhas na sa�da. Como os n�meros de linhas deveriam ser impressos? Eles
devem ser impressos na mesma linha do texto coincidente? ?

� Exerc�cio 9-9. Adicione os operadores + (um ou mais) e ? (zero ou um) a match. O


padr�o a+bb? coincide com um ou mais a' s seguido de um ou dois b' s. ?

� Exerc�cio 9-10. A implementa��o atual de match desativa o significado especial de


^ e $ se eles n�o come�arem ou terminarem a express�o, e de * se ele n�o seguir
imediatamente um caractere literal ou um per�odo. Um projeto mais convencional �
citar um metacaractere precedendo-o com uma barra invertida. Conserte match para
lidar com as barras invertidas dessa forma. ?

� Exerc�cio 9-11. Adicione as classes de caractere a match. As classes de caractere


especificam uma coincid�ncia para qualquer um dos caracteres entre colchetes. Elas
podem ficar mais convenientes com o acr�scimo dos intervalos, por exemplo [a-z]
para coincidir toda letra min�scula, e inverter o sentido, por exemplo, [^0-9] para
coincidir com todo caractere exceto um d�gito. ?

� Exerc�cio 9-12. Altere match para usar a vers�o mais longa e mais � esquerda de
matchstar e modifique-a para retornar as posi��es de caractere do in�cio e final do
texto coincidido. Use isso para construir um programa gres que seja como grep mas
que imprima cada linha de entrada ap�s substituir o texto novo pelo texto que
coincide com o padr�o, como em

% gres 'homoiousian' 'homoousian' mission.stmt

� Exerc�cio 9-13. Modifique match e grep para funcionar com as strings UTF-8 dos
caracteres Unicode. Como o UTF-8 e Unicode s�o um superconjunto do ASCII, essa
altera��o tem compatibilidade ascendente. As express�es regulares, bem como o texto
pesquisado, tamb�m precisar�o funcionar adequadamente com o UTF-8. Como as classes
de caracteres devem ser implementadas? ?

� Exerc�cio 9-14. Escreva um testador autom�tico para as express�es regulares que


geram express�es e strings de teste para pesquisa. Se puder, use uma biblioteca
existente como uma implementa��o de refer�ncia. Talvez voc� tamb�m encontre alguns
bugs nela. ?

9.3 Ferramentas program�veis

Muitas ferramentas est�o estruturadas em torno de uma linguagem de prop�sito


especial. O programa grep � apenas um de uma fam�lia de ferramentas que usam
express�es regulares ou outras linguagens para solucionar problemas de programa��o.
Um dos primeiros exemplos foi o int�rprete de comando ou linguagem de
controle de tarefa. Percebeu-se no in�cio que seq��ncias comuns de comandos podiam
ser colocadas em um arquivo, e uma inst�ncia do int�rprete de comandos ou shell
poderia ser executada com aquele arquivo como entrada. Depois disso foi r�pido
adicionar os par�metros, condicionais, loops, vari�veis e todas as outras
armadilhas de uma linguagem convencional. A principal diferen�a foi que havia
apenas um tipo de dados - as strings - e os operadores dos programas shell tendiam
a ser programas inteiros que faziam c�lculos interessantes. Embora a programa��o de
shell tenha ca�do no esquecimento, dando lugar a alternativas como a Perl, nos
ambientes de comando, e aos bot�es que podem ser apertados, nas interfaces gr�ficas
de usu�rio, ela ainda � uma maneira efetiva de construir opera��es complexas a
partir de pe�as simples.
Awk � outra ferramenta program�vel, uma linguagem de a��o padr�o pequena e
especializada que focaliza a sele��o e transforma��o de um fluxo de entrada. Como
vimos no Cap�tulo 3, Awk l� automaticamente os arquivos de entrada e divide cada
linha em campos chamados de $1 a $NF, onde NF � o n�mero de campos da linha.
Fornecendo o comportamento padr�o de muitas tarefas comuns, ele possibilita os
�teis programas on-line. Por exemplo, este programa Awk completo

# split.awk: split input into one word per line


{ for (i = 1; i <= NF; i++) print $i }

imprime as "palavras" de cada linha da entrada uma por linha. Para ir em outra
dire��o, aqui temos uma implementa��o de fmt, a qual preenche cada linha da entrada
com palavras, at� o m�ximo de 60 caracteres; uma linha em branco causa uma quebra
de par�grafo.

# fmt.awk: formata em linhas de 60 caracteres


/./ { for(i = 1; i <= NF; i++) addword($i) } # linha n�o em branco
/^S/ { printline(); print "" } # linha em branco
END { printline() }

function addword(w) {
if (length(line) + 1 + length(w) > 60)
printline()
if (length(line) == 0)
line = w
else
line = line " " w
}
function printline() {
if (length(line) > 0) {
print line line = ""
}
}

Com frequ�ncia usamos fmt para criar novos par�grafos para as mensagens de correio
eletr�nico e outros documentos curtos; tamb�m a usamos para formatar a sa�da dos
programas Markov do Cap�tulo 3.
As ferramentas program�veis se originam nas linguagens pequenas criadas para
a express�o natural das solu��es dos problemas dentro de um dom�nio estreito. Um
bom exemplo � a ferramenta eqn do Unix, que digita f�rmulas matem�ticas. Sua
linguagem de entrada parece com aquilo que um matem�tico diria ao ler equa��es em
voz alta: ?/2 � escrito como pi sobre 2.

A TEX usa a mesma abordagem; sua nota��o para essa f�rmula � \pi\over 2. Se
houver uma nota��o natural ou conhecida para o problema que voc� est� solucionando,
use-a ou adapte-a, mas n�o comece do zero.
Awk foi inspirada por um programa que usava express�es regulares para
identificar dados an�malos nos registros do tr�fego telef�nico, mas Awk inclui
vari�veis, express�es, loops e assim por diante, para torn�-la uma linguagem de
programa��o real. Perl e Tcl foram criadas desde o in�cio para combinar a
conveni�ncia e express�o das linguagens pequenas ao poder das linguagens grandes.
Elas s�o linguagens de prop�sito geral, embora quase sempre sejam usadas para o
processamento de texto.
O termo gen�rico para essas ferramentas � linguagens de cria��o de scr�pts
porque elas evolu�ram dos primeiros int�rpretes de comando, cuja facilidade de
programa��o era limitada, para a execu��o de "scripts" enlatados de programas. As
linguagens de cria��o de scripts permitem o uso criativo das express�es regulares,
n�o apenas para a coincid�ncia de padr�o - reconhecendo que determinado padr�o
ocorre - mas tamb�m para identificar regi�es de texto a serem transformadas. Isso
ocorre nos dois comandos regsub (regular expression substitution) do seguinte
programa Tcl. O programa � uma ligeira generaliza��o do programa que mostramos no
Cap�tulo 4 que recupera as cota��es de a��es; este coloca o URL dado pelo seu
primeiro argumento. A primeira substitui��o remove a string http// se ela estiver
presente; a segunda substitui a primeira / por um espa�o em branco, dividindo assim
o argumento em dois campos. O comando lindex recupera os campos de uma string
(come�ando pelo �ndice 0). O texto entre [] � executado como um comando Tcl e
substitu�do pelo texto resultante; $x � substitu�do pelo valor da vari�vel x.

# geturl.tcl: recupera o documento do URL


# a entrada tem a forma [http://]abc.def.com[/whatever. ..]

regsub "http://" $argv "" argv ;# remove http:// se estiver


presente
regsub "/" $argv " " argv ;# substitui a / inicial por espa�o
em branco

set so [socket [lindex $argv 0] 80] ;# cria a conex�o de rede


set q "/[lindex $argv 1]"

puts $so "GET $q HTTP/1. 0\n\n" ;# envia a solicita��o


flush $so
while {[gets $so line] >= O && $line != ""} {} ;# salta o header
puts [read $so] ;# l� e imprime toda a resposta

Este script geralmente produz sa�da volumosa, sendo que grande parte dela �
formada por tags HTML entre < e >. A Perl � boa para a substitui��o de texto, de
modo que nossa pr�xima ferramenta � um script em Perl que usa as express�es
regulares e substitui��es para descartar os tags:

# unhtml.pl: exclui os tags HTML


while (<>) { # re�ne toda a entrada em uma �nica string
$str .= $_; # concatenando as linhas de entrada
}
$str =~ s/<[^>]*>//g; # exclui <;;;>
$str =~ s/&nbsp;/ /g; # substitui &nbsp; por espa�o em branco
$str =~ s/\s+/\n/g; # compacta o espa�o em branco
print $str;

Este exemplo � cr�tico quando n�o se fala Perl. A constru��o

$str =~ s/regexp/repl/g

substitui a string repl pelo texto de str que coincide (o mais longo e mais �
esquerda) com a express�o regular regexp; o g final, de "global", significa fazer
isso para todas as coincid�ncias da string em vez de apenas para a primeira. A
sequ�ncia de metacaracteres \s � uma abrevia��o para um caractere de espa�o em
branco (espa�o, tab, newline e outros); \n � newline. A string "&nbsp;" � um
caractere HTML, como aqueles do Cap�tulo 2, que define um caractere de espa�o que
n�o pode ser dividido.
Colocando tudo isso junto temos um browser da Web lento mas funcional,
implementado como um script shell de uma linha:

# web: recupera a p�gina da web e formata seu texto, ignorando a HTML

geturl.tcl $1 | unhtml.pl | fmt.awk

Isso recupera a p�gina da Web, descarta todas as informa��es de controle e


formata��o, e formata o texto segundo suas pr�prias regras. Essa � uma maneira
r�pida de conseguir uma p�gina de texto na Web.
Observe a variedade de linguagens que colocamos juntas em cascata, cada uma
adequada a uma determinada tarefa: Tcl, Perl, Awk e, dentro de cada uma delas, as
express�es regulares. O poder da nota��o vem do fato de se ter uma boa nota��o para
cada problema. Tcl � particularmente boa para pegar texto na rede; Perl e Awk s�o
boas para editar e formatar texto e, � claro, as express�es regulares s�o boas para
especificar partes do texto para pesquisa e modifica��o. Essas linguagens juntas
s�o mais poderosas do que qualquer uma delas isolada. Vale a pena dividir a tarefa
em partes, se assim voc� puder lucrar com a nota��o certa.

9.4 Int�rpretes, compiladores e m�quinas virtuais

Como um programa coloca em execu��o sua forma de c�digo-fonte? Se a


linguagem for suficientemente simples, como em printf ou em nossas express�es
regulares mais simples, n�s podemos executar direto da fonte. Isso � um in�cio
f�cil e muito r�pido.
Existe uma troca entre o tempo de configura��o e a velocidade de execu��o.
Se a linguagem � mais complicada, geralmente � desej�vel converter o c�digo-fonte
para uma representa��o conveniente e eficiente para a execu��o. Os programas que
combinam a convers�o e execu��o em um �nico programa que l� o texto-fonte, o
converte e executa se chamam int�rpretes. Awk e Perl interpretam, assim como muitas
outras linguagens de cria��o de scripts e prop�sito geral.
Uma terceira possibilidade seria gerar instru��es para o tipo espec�fico de
computador no qual o programa ser� executado, como fazem os compiladores. Isso
exige mais tempo e esfor�o iniciais mas resulta na execu��o subsequente mais
r�pida.
Existem outras combina��es. Uma combina��o que vamos estudar nesta se��o � a
compila��o de um programa em instru��es para um computador feito (uma m�quina
virtual) que pode ser simulado em qualquer computador real. Uma m�quina virtual
combina muitas das vantagens da interpreta��o e compila��o convencional.
Se uma linguagem � simples, n�o � preciso muito processamento para inferir a
estrutura do programa e convert�-la em uma forma interna. Se, por�m, a linguagem
tiver alguma complexidade - declara��es, estruturas aninhadas, declara��es ou
express�es definidas recursivamente, operadores com preced�ncia e outros - fica
mais complicado analisar a entrada para determinar a estrutura.
Os analisadores quase sempre s�o escritos com o aux�lio de um gerador
autom�tico de analisador, tamb�m chamado compilador-compilador, tal como o yacc ou
bison. Tais programas traduzem uma descri��o da linguagem, chamada de sua
gram�tica, em um programa (tipicamente) C ou C++que, depois de compilado, traduzir�
as declara��es da linguagem para uma representa��o interna. E claro que a gera��o
de um analisador diretamente de uma gram�tica � outra demonstra��o do poder da boa
nota��o.
A representa��o produzida por um analisador geralmente � uma �rvore, com os
n�s internos contendo operadores e folhas contendo operandos. Uma declara��o tal
como

a = max(b, c/2);

produziria esta �rvore de an�lise (ou sintaxe):


Muitos dos tr�s algoritmos descritos no Cap�tulo 2 podem ser usados para construir
e processar as �rvores de an�lise.
Depois que a �rvore est� constru�da, existe uma variedade de procedimentos.
O mais direto, usado em Awk, � caminhar diretamente pela �rvore, avaliando os n�s
enquanto isso. Uma vers�o simplificada de tal rotina de avalia��o para uma
linguagem de express�o baseada no inteiro envolveria uma travessia p�s-ordem como
esta:

typedef struct Symbol Symbol;


typedef struct Tree Tree;

struct Symbol {
int value;
char *name;
};
struct Tree {
int op; /* c�digo da opera��o */
int value; /* valor se for n�mero */
Symbol *symbol; /* entrada de Symbol se for vari�vel */
Tree *left;
Tree *right;
};

/* avalia a express�o de �rvore */


int eval(Tree *t)
{
int left, right;

switch (t->op) {
case NUMBER:
return t->value;
case VARIABLE:
return t->symbol->value;
case ADD:
return eval(t->left) + eval(t->right);
case DIVIDE:
left = eval(t->left);
right = eval(t->right);
if (right == 0)
eprintf("divide %d by zero", left);
return left / right;
case MAX:
left = eval(t->left);
right = eval(t->right);
return left>right ? left : right;
case ASSIGN:
t->left->symbol->value = eval(t->right);
return t->left->symbol->value;
/*... */
}
}

Os primeiros casos avaliam express�es simples como constantes e valores. Os �ltimos


avaliam as express�es aritm�ticas, e outros podem realizar o processamento
especial, condicionais e loops. Para implementar as estruturas de controle, a
�rvore precisar� de informa��es extras, n�o mostradas aqui, que representam o fluxo
de controle.
Como em pack e unpack, podemos substituir o deslocamento expl�cito por uma
tabela de ponteiros de fun��o. Os operadores individuais s�o bastante iguais aos da
declara��o de deslocamento:

/* addop: retorna a soma das tr�s express�es */


int addop(Tree *t)
{
return eval(t->left) + eval(t->right);
}

A tabela dos ponteiros de fun��o relaciona os operadores �s fun��es que executam as


opera��es:

enum { /* c�digos de opera��o, Tree.op */


NUMBER,
VARIABLE,
ADD,
DIVIDE,
/* ... */
};

/* optab: tabela de fun��o de operador */


int (*optab[])(Tree *) = {
pushop, /* NUMBER */
pushsymop, /* VARIABLE */
addop, /* ADD */
divop, /* DIVIDE */
/* ... */
};

A avalia��o usa o operador para �ndice na tabela de ponteiros de fun��o para chamar
as fun��es certas. Esta vers�o invoca outras fun��es recursivamente:

/* eval:vers�o 2: avalia a �rvore da tabela de operador */


int eval(Tree *t) {
return (*optab[t->op])(t);
}

Ambas essas vers�es de eval s�o recursivas. Existem maneiras de eliminar a


recurs�o, incluindo uma t�cnica inteligente chamada c�digo encadeado que achata
completamente a pilha de chamadas. O m�todo melhor � acabar totalmente com a
recurs�o armazenando as fun��es em um array que depois � atravessado de forma
seq�encial para executar o programa. Esse array se torna uma seq��ncia de
instru��es a serem executadas por uma m�quina pequena de prop�sito especial.
Ainda precisamos de uma pilha para representar os valores avaliados
parcialmente no c�lculo, de modo que a forma das fun��es mude, mas a transforma��o
� f�cil de ver. Na verdade, n�s inventamos uma m�quina de pilha na qual as
instru��es s�o pequenas fun��es e os operandos s�o armazenados em uma pilha
separada de operandos. Isso n�o � uma m�quina real, mas podemos programar como se
ela estivesse l�, e podemos implement�-la facilmente como um int�rprete.
Em vez de caminhar pela �rvore para avali�-la, n�s caminhamos por ela para
gerar o array de fun��es para executar o programa. O array tamb�m conter� os
valores de dados usados pelas instru��es, tais como constantes e vari�veis
(s�mbolos), de modo que o tipo dos elementos do array deve ser uma uni�o:

typedef union Code Code;


union Code {
void (*op)(void); /* fun��o se operador */
int value; /* valor se n�mero */
Symbol *symbol; /* entrada de Symbol se vari�vel */
};

Aqui temos a rotina para gerar os ponteiros de fun��o e coloc�-los em um array,


code, desses itens. O valor de retorno de generate n�o � o valor da express�o - o
qual ser� calculado quando o c�digo gerado for executado - mas sim o �ndice de code
da pr�xima opera��o a ser gerada:

/* generate: gerar instru��es caminhando pela �rvore */


int generate(int codep, Tree *t)
{
switch (t->op) {
case NUMBER:
code[codep++].op = pushop;
code[codep++] . value = t->value;
return codep;
case VARIABLE:
code[codep++].op = pushsymop;
code[codep++] .symbol = t->symbol ;
return codep;
case ADD:
codep = generate(codep, t->left);
codep = generate(codep, t->right);
code[codep++] .op = addop;
return codep;
case DIVIDE:
codep = generate(codep, t->left);
codep = generate(codep, t->right);
code[codep++] .op = divop;
return codep;
case MAX:
/* ... */
}
}

Para a declara��o a = max(b, c/2) o c�digo gerado seria assim:

pushsymop
b
pushsymop
c
pushop
2
divop
maxop
storesymop
a

As fun��es de operador manipulam a pilha, tirando operadores e colocando


resultados.
O int�rprete � um loop que caminha por um contador de programa ao longo do
array de ponteiros de fun��o:

Code code[NCODE];
int stack[NSTACK];
int stackp;
int pc; /* contador de programa */
/* eval: vers�o 3: avalia a express�o do c�digo gerado */
int eval(Tree *t)
{
pc = generate(0, t);
code[pc].op = NULL;

stackp = 0;
pc = 0;
while (code[pc].op != NULL)
(*code[pc++].op)();
return stack[0];
}

Esse loop simula no software de nossa m�quina de pilha inventada o que


acontece no hardware de uma m�quina real. Aqui temos alguns operadores
representativos:

/* pushop: empurra n�mero; o valor � a pr�xima palavra do fluxo de


c�digo */
void pushop(void)
{
stack[stackp++] = code[pc++].value;
}

/* divop: calcula a rela��o entre duas express�es */


void divop(void)
{
int left, right;

right = stack[--stackp];
left = stack[--stackp];
if (right == 0)
eprintf("divide %d by zero\n", left);
stack[stackp++] = left / right;
}

Observe que a verifica��o dos divisores por zero aparece em divop, n�o em generate.
A execu��o condicional, desvios e loops operam por meio da modifica��o do
contador do programa dentro de uma fun��o de operador, executando um desvio para um
ponto diferente do array de fun��es. Por exemplo, um operador goto sempre define o
valor da vari�vel pc, enquanto que um desvio condicional define pc somente quando a
condi��o � verdadeira.
O array code � interno ao. int�rprete, � claro, mas imagine que quis�ssemos
salvar o programa gerado em um arquivo. Se escrev�ssemos os endere�os de fun��o, o
resultado seria n�o-port�vel e fr�gil. Mas, poder�amos escrever constantes que
representassem as fun��es, digamos 1000 para addop, 1001 para pushop, e assim por
diante, e as traduzissem de volta para os ponteiros de fun��o quando o programa
fosse lido para interpreta��o.
Se examinarmos um arquivo resultante desse procedimento, ele se parecer� com
um fluxo de instru��es de uma m�quina virtual cujas instru��es implementam os
operadores b�sicos de nossa linguagem pequena, e a fun��o generate � realmente um
compilador que traduz a linguagem para a m�quina virtual. As m�quinas virtuais s�o
uma ideia antiga e ador�vel, e ficaram novamente na moda h� pouco tempo por meio de
Java e de Java Virtual Machine JVM); elas fornecem uma maneira f�cil de produzir
representa��es port�veis e eficientes dos programas escritos em uma linguagem de
n�vel alto.

9.5 Programas que escrevem programas


Talvez a coisa mais not�vel sobre a fun��o generate seja o fato de que ela �
um programa que escreve um programa: sua sa�da � um fluxo de instru��es execut�veis
para outra m�quina (virtual). Os compiladores fazem isso o tempo todo, traduzindo
c�digo-fonte para instru��es de m�quina, de modo que a id�ia certamente �
conhecida. Na verdade, os programas que escrevem programas aparecem de muitas
formas.
Um exemplo comum � a gera��o din�mica de HTML para p�ginas da Web. A HTML �
uma linguagem, embora limitada, que tamb�m pode conter c�digo JavaScript. As
p�ginas da Web quase sempre s�o geradas imediatamente por programas em Perl ou C,
com conte�do espec�fico (por exemplo, resultados de pesquisa e propaganda dirigida)
determinado pelas solicita��es recebidas. N�s usamos as linguagens especializadas
nos gr�ficos, figuras, tabelas, express�es matem�ticas e �ndice deste livro. Como
outro exemplo, o PostScript � uma linguagem de programa��o gerada pelos
processadores de texto, programas de desenho e por uma variedade de outras fontes;
no est�gio final do processamento, todo este livro � representado como um programa
de 57.000 linhas em PostScript.
Um documento � um programa est�tico, mas a id�ia de usar uma linguagem de
programa��o como nota��o para qualquer dom�nio de problema � extremamente poderosa.
H� muitos anos, os programadores sonhavam em fazer com que os computadores
escrevessem todos os seus programas para eles. Isso provavelmente nunca passar� de
um sonho, mas hoje em dia os computadores escrevem rotineiramente os programas para
n�s, com freq��ncia para representar as coisas que anteriormente n�o teriam sido
consideradas programas.
O programa mais comum para escrever programas � um compilador que traduz
linguagem de alto n�vel para c�digo de m�quina. Ele � muito �til, por�m, para
traduzir c�digo para a linguagem de programa��o mainstream. Na se��o anterior, n�s
mencionamos que os geradores de analisadores convertem uma defini��o da gram�tica
de uma linguagem para um programa em C que analisa a linguagem. A C quase sempre �
usada dessa forma, como um tipo de "linguagem assembly de alto n�vel". A Modula-3 e
C++est�o entre as linguagens de prop�sito geral cujos primeiros compiladores
criaram o c�digo C, o qual foi depois compilado por um compilador padr�o C. A
abordagem tem v�rias vantagens, incluindo a efici�ncia - porque os programas podem
a princ�pio ser executados t�o r�pido quanto os programas em C - e a portabilidade
- porque os compiladores podem ser movidos para qualquer sistema que tenha um
compilador C. Isso no in�cio ajudou bastante a divulgar essas linguagens.
Como outro exemplo, a interface gr�fica da Visual Basic gera um conjunto de
declara��es de atribui��o em Visual Basic para inicializar os objetos que o usu�rio
selecionou nos menus e posicionou na tela com um mouse. Uma variedade de outras
linguagens t�m sistemas "visuais" de desenvolvimento e "assistentes" que sintetizam
o c�digo da interface de usu�rio a partir dos cliques do mouse.
Apesar do poder dos geradores de programas, e apesar da exist�ncia de muitos
bons exemplos, a no��o n�o � apreciada tanto quanto deveria e � pouco usada pelos
programadores individuais. Mas existem muitas oportunidades em pequena escala para
a cria��o de c�digo por parte de um programa, de modo que voc� pode conseguir
algumas vantagens para si mesmo. Aqui temos v�rios exemplos que geram c�digo em C
ou C++.
O sistema operacional Plan 9 gera mensagens de erro a partir de um arquivo
header que cont�m nomes e coment�rios; os coment�rios s�o convertidos mecanicamente
para strings com aspas em um array que pode ser indexado pelo valor enumerado. Este
fragmento mostra a estrutura do arquivo header:

/* errors.h: Standard erros messages */


enum {
Eperm /* Permission denied */
Eio /* I/O error */
Efile /* File does not exist */
Emem /* Memory limit reached */
Espace, /* Out of file space */
Egreg /* It's all Greg's fault */
};

Dada essa entrada, um programa simples pode produzir o seguinte conjunto de


declara��es para as mensagens de erro:

/* gerado por m�quina; n�o editar. */

char *errs[] = {
"Permission denied", /* Eperm */
"I/O error", /* Eio */
"File does not exist", /* Efile */
"Memory limit reached", /* Emem */
"Out of file space", /* Espace */
"It's all Greg's fault", /* Egreg */ };

Existem alguns benef�cios nessa abordagem. Em primeiro lugar, os relacio-namentos


entre os valores enum e as strings que eles representam � literalmente
autodocumentado e f�cil de ser tornado independente da linguagem natural. Da mesma
forma, as informa��es aparecem apenas uma vez, um "�nico ponto de verdade" a partir
do qual outro c�digo � gerado, portanto existe apenas um lugar para manter as
informa��es atualizadas. Se em vez disso houver v�rios lugares, � inevit�vel que
eles eventualmente saiam de sincronia. Finalmente, � f�cil providenciar que o
arquivo .c seja recriado e recompilado sempre que o arquivo header mudar. Quando
uma mensagem de erro deve ser alterada, s� � preciso modificar o arquivo header e
compilar o sistema operacional. As mensagens s�o atualizadas automaticamente.
O programa gerador pode ser escrito em qualquer linguagem. Uma linguagem de
processamento de string como a Perl facilita isso:

# enum.pl: gera strings de erro a partir de enum+coment�rios


print "/* gerado por m�quina; n�o editar. */\n\n";
print "char *errs[] = {\n";

while (<>) {
chop; # remover newline
if (/^\s*(E[a-z0-9]+),?/) { # a primeira palavra � E...
$name = $1; # salvar nome
s/.*\/\* *//; # remover at� */
s/ *\*\///: # remover */
print "\t\"$_\", /* $name */\n";
}
}
print "};\n";

As express�es regulares entram em a��o novamente. As linhas cujos primeiros campos


se parecem com identificadores seguidos por v�rgula s�o selecionadas. A primeira
substitui��o exclui tudo at� o primeiro caractere n�o em branco do coment�rio,
enquanto que a segunda remove o terminador do coment�rio e todos os espa�os em
branco que o precedem.
Como parte de um esfor�o de teste de compilador, Andy Koenig desenvolveu um
modo conveniente de escrever c�digo em C++ para verificar se o compilador pegou
erros do programa. Os fragmentos de c�digo que poderiam causar um diagn�stico de
compilador s�o decorados com os coment�rios m�gicos para descrever as mensagens
esperadas. Cada linha tem um coment�rio que come�a com /// (para distingui-lo dos
coment�rios comuns) e uma express�o regular que faz coincidir o diagn�stico daquela
linha. Assim sendo, por exemplo, os dois fragmentos de c�digo seguintes devem gerar
diagn�stico:

int f() {}
/// warning.* non-void function .* should return a value
void g() (return 1;}
/// error.* void function may not return a value

Se executarmos o segundo teste por meio de nosso compilador C++, ele imprime
a mensagem esperada, a qual coincide com a express�o regular:

% CC x.c
"x.c", line 1: error(321): void function may not return a value

Cada um desses fragmentos de c�digo � dado para o compilador, e a sa�da �


comparada com rela��o ao diagn�stico esperado, um processo que � gerenciado por uma
combina��o entre shell e programas Awk. As falhas indicam um teste em que a sa�da
do compilador diferiu daquilo que era esperado. Como os coment�rios s�o express�es
regulares h� alguma latitude na sa�da. Eles podem ser mais ou menos clementes,
dependendo do que se precisa.
A ideia dos coment�rios com sem�ntica n�o � nova. Eles aparecem no
PostScript, onde os coment�rios normais come�am com %. Os coment�rios que come�am
com %% por conven��o podem ter informa��es extras sobre n�meros de p�ginas, caixas
de limite, nomes de fontes e outras:

%%PageBoundingBox: 126 307 492 768


%%Pages: 14
%%DocumentFonts: Helvetica Times-Italic Times-Roman LucidaSans-Typewriter

Em Java, os coment�rios que come�am com /** e terminam com */ s�o usados para criar
documenta��o para a defini��o de classe que vem a seguir. A vers�o em larga escala
do c�digo autodocumentado � programa��o literata, a qual integra um programa e sua
documenta��o de modo que um processo a imprime em uma ordem natural de leitura, e
outro a organiza na ordem certa de compila��o.
Em todos os exemplos acima � importante observar o papel da nota��o, da
mistura de linguagens e do uso das ferramentas. A combina��o amplia o poder dos
componentes individuais.

� Exerc�cio 9-15. Uma piada antiga da computa��o � escrever um programa que quando
executado reproduza exatamente a si mesmo, na forma de fonte. Esse � um �timo caso
especial de um programa que escreve um programa. Experimente-o em algumas de suas
linguagens preferidas. ?

9.6 Usando as macros para gerar c�digo

Descendo alguns n�veis, � poss�vel fazer com que as macros escrevam c�digo
no tempo de compila��o. Em todo este livro, n�s avisamos contra o uso das macros e
da compila��o condicional. Elas encorajam um estilo de programa��o cheio de
problemas. Mas elas t�m o seu lugar; �s vezes a substitui��o textual � exatamente a
resposta certa para um problema. Um exemplo � o uso do pr�-processador de macro da
C++ para montar peda�os de um programa com estilos e repetitivo.
Por exemplo, o programa que estimava a velocidade dos construtores de
linguagem elementar do Cap�tulo 7 usa o pr�-processador C para montar os testes,
fazendo o wrapping deles no c�digo final. A ess�ncia do teste � encapsular um
fragmento de c�digo em um loop que inicia um timer, executa o fragmento muitas
vezes, p�ra o timer e reporta os resultados. Todo o c�digo repetido � capturado em
algumas macros, e o c�digo a ser cronometrado � passado como um argumento. A macro
prim�ria assume esta forma:

#define LOOP(CODE) { \
t0 = clock(); \
for (i = 0; i < n; i++) { CODE; } \
printf("%7d ", clock() - t0) ; \
}

As barras invertidas permitem que o corpo da macro abranja v�rias linhas.


Essa macro � usada nas "declara��es" que geralmente se parecem com o seguinte:

LOOP(f1 = f2)
LOOP(f1 = f2 + f3)
LOOP(f1 = f2 - f3)

�s vezes existem outras declara��es para inicializa��o, mas a parte b�sica


da cronometragem � representada nesses fragmentos de argumento �nico que se
expandem para uma quantidade significativa de c�digo.
O processamento de macros tamb�m pode ser usado para gerar o c�digo de
produ��o. Bart Locanthi escreveu certa vez uma vers�o eficiente de um operador de
gr�ficos bidimensionais. � dif�cil tornar r�pido o operador, chamado bitblt ou
rasterop, porque existem muitos argumentos que se combinam de formas complicadas.
Por meio de an�lise cuidadosa, Locanthi reduziu a combina��o aos loops individuais
que poderiam ser otimizados separadamente. Cada caso foi ent�o constru�do por
substitui��o de macro, an�loga ao exemplo de teste de desempenho, com todas as
variantes dadas por uma �nica e grande declara��o de deslocamento. O c�digo-fonte
original tinha algumas centenas de linhas; o resultado do processamento da macro
tinha v�rios milhares de linhas. O c�digo expandido por macro n�o era ideal mas,
considerando a dificuldade do problema, ele era pr�tico e muito f�cil de produzir.
Da mesma forma, no que diz respeito ao c�digo de alto desempenho, ele era
relativamente port�vel.

� Exerc�cio 9-16. O Exerc�cio 7-7 envolvia a cria��o de um programa para medir o


custo das diversas opera��es em C++. Use as ideias desta se��o para criar outra
vers�o do programa. ?

� Exerc�cio 9-17. O Exerc�cio 7-8 envolvia a cria��o de um modelo de custo para


Java, o qual n�o tinha a capacidade de macro. Solucione o problema escrevendo
outro programa, em qualquer linguagem (ou linguagens) que voc� preferir, o qual
grava a vers�o java e automatiza as cronometragens. ?

9.7 Compilando imediatamente

Na se��o anterior n�s falamos sobre programas que escrevem programas. Em


cada um dos exemplos, o programa gerado estava na forma de fonte; ele ainda
precisava ser compilado ou interpretado para ser executado. Mas � poss�vel gerar
c�digo que est� pronto para ser executado imediatamente produzindo instru��es de
m�quina em vez de fonte. Isso � conhecido como compilar "on the fly" ou "just in
time". O primeiro termo � mais antigo mas o �ltimo, incluindo sua sigla JIT, � mais
conhecido.
Embora o c�digo compilado seja necessariamente n�o-port�vel - ele s� ser�
executado em um �nico tipo de processador - ele pode ser extremamente r�pido. Pense
na express�o

max(b, c/2)

O c�lculo deve avaliar c, dividi-lo por dois, comparar o resultado com b, e


selecionar o maior. Se avaliarmos a express�o usando a m�quina virtual que
rascunhamos antes no cap�tulo poder�amos eliminar a verifica��o da divis�o por zero
em divop. Como 2 nunca � zero, a verifica��o � in�til. Mas dado algum dos projetos
que fizemos para implementar a m�quina virtual, n�o h� como eliminar a verifica��o;
toda implementa��o da opera��o de divis�o compara o divisor com zero.
� aqui que a gera��o din�mica do c�digo pode ajudar. Se construirmos o
c�digo diretamente para a express�o, em vez de apenas usarmos as strings de
opera��es predefinidas, podemos evitar a verifica��o da divis�o por zero para
divisores que s�o reconhecidamente diferentes de zero. Na verdade, podemos ir at�
mais adiante. Se toda a express�o for constante, tal como max(3*3, 4/2), n�s
podemos avali�-la uma vez quando gerarmos o c�digo e substitu�-la pelo valor
constante 9. Se a express�o aparecer em um loop, n�s economizamos tempo a cada
viagem pelo loop e se o loop for executado um n�mero suficiente de vezes, n�s vamos
ganhar de volta a overhead que foi precisa para estudar a express�o e gerar o seu
c�digo.
A principal ideia � que a nota��o nos d� uma maneira geral de expressar um
problema, mas o compilador para a nota��o pode personalizar o c�digo para os
detalhes do c�lculo espec�fico. Por exemplo, em uma m�quina virtual para express�es
regulares, n�s gostar�amos de ter um operador para coincidir com um caractere
literal:

int matchchar(int literal, char *text)


{
return *text == literal;
}

Quando geramos o c�digo de determinado padr�o, por�m, o valor de um literal � fixo


em, digamos, ' x', de modo que poder�amos usar um operador como este:

int matchx(char *text)


{
return *text == 'x';
}

E depois, em vez de predefinir um operador especial para cada valor de caractere


literal, n�s tornamos as coisas mais simples ainda gerando o c�digo dos operadores
que realmente precisamos para a express�o atual. A generaliza��o da ideia para todo
o conjunto de opera��es permite escrever um compilador on the fly que traduz a
express�o regular atual para c�digo otimizado especial daquela express�o.
Ken Thompson fez exatamente isso para uma implementa��o das express�es
regulares no IBM 7094 em 1967. Sua vers�o gerou pequenos blocos de 7.094 instru��es
bin�rias para as diversas opera��es da express�o, fez o thread das instru��es e
depois executou o programa resultante chamando-o, assim como uma fun��o regular.
T�cnicas semelhantes podem ser aplicadas � cria��o de sequ�ncias espec�ficas de
instru��es para atualiza��es de tela nos sistemas gr�ficos, onde h� tantos casos
especiais que � mais eficiente criar c�digo din�mico para cada um que surge do que
escrev�-los todos com anteced�ncia ou incluir testes condicionais no c�digo mais
geral.
Para demonstrar o que est� envolvido na constru��o de um compilador on the
fly real seria preciso detalhes demais de um determinado conjunto de instru��es,
mas vale a pena gastar algum tempo para mostrar como tal sistema funciona. O
restante desta se��o deve ser lido para se obter ideias e percep��o, mas n�o para
os detalhes da implementa��o.
Lembre-se de que deixamos nossa m�quina virtual com uma estrutura como esta:

Code code[NCODE];
int stack[NSTACK];
int stackp;
int pc; /* contador de programa */

...
Tree *t;

t = parse();
pc = generate(0, t);
code[pc].op = NULL;

stackp = 0;
pc = 0;
while (code[pc].op != NULL)
(*code[pc++].op)();
return stack[0];

Para adaptar esse c�digo para compila��o on the fly, devemos fazer algumas
altera��es. Em primeiro lugar, o array code n�o � mais um array dos ponteiros de
fun��o, mas um array de instru��es execut�veis. Se as instru��es ser�o do tipo
char, int ou long � algo que vai depender do processador para o qual estamos
compilando; vamos assumir int. Depois que o c�digo � gerado, chamamos a isso de uma
fun��o. N�o haver� contador de programa virtual porque o ciclo de execu��o do
pr�prio processador vai caminhar pelo c�digo por n�s. Depois que o c�lculo � feito,
ele vai retornar, como uma fun��o regular. Da mesma forma, n�s podemos optar por
manter uma pilha separada de operandos para a m�quina ou usar a pilha do pr�prio
processador. Cada abordagem tem vantagens, mas preferimos ficar com uma pilha
separada e nos concentrarmos nos detalhes do pr�prio c�digo. A implementa��o agora
se parece com o seguinte:

typedef int Code;


Code code[NCODE];
int codep;
int stack[NSTACK];
int stackp;
...
Tree *t;
void (*fn)(void);
int pc;

t = parse();
pc = generate(0, t);
genreturn(pc) /* gerar sequencia de retorno de fun��o */
stackp = 0;
flushcaches(); /* sincroniza a mem�ria com o processador */
fn = (void(*)(void)) code; /* cast do array de ptr para func */
(*fn)(); /* chama fun��o */
return stack[0];

Depois que generate termina, genreturn cria as instru��es que fazem o c�digo
gerado retornar o controle a eval.
A fun��o flushcaches cuida das etapas necess�rias para preparar o
processador para executar c�digo rec�m-gerado. As m�quinas modernas s�o executadas
mais rapidamente em parte porque elas t�m caches para instru��es e dados, e
pipelines internas que sobrep�em a execu��o de muitas instru��es sucessivas. Esses
caches e pipelines esperam que o fluxo de instru��es seja est�tico. Se gerarmos o
c�digo antes da execu��o, o processador pode ficar confuso. A CPU precisa drenar
seu pipeline e esvaziar seus caches antes de executar as instru��es rec�m-geradas.
Essas opera��es s�o altamente dependentes da m�quina. A implementa��o de
flushcaches ser� diferente em cada determinado tipo de computador.
A express�o not�vel (void(*) (void)) code � uma coer��o que converte o
endere�o do array que cont�m as instru��es geradas em um ponteiro de fun��o que
pode ser usado para chamar o c�digo como uma fun��o.
Tecnicamente, n�o � t�o dif�cil gerar o pr�prio c�digo, embora haja uma
quantidade razo�vel de engenharia para fazer isso de forma eficiente. Come�amos com
alguns elementos b�sicos. Assim como antes, um array code e um �ndice para ele s�o
mantidos durante a compila��o. Por quest�es de simplicidade, vamos torn�-los ambos
globais, como fizemos antes. Depois, podemos escrever uma fun��o para criar as
instru��es:

/* emit: anexa instru��o ao fluxo do c�digo */


void emit (Code inst)
{
code[codep++] = inst;
}

As instru��es em si podem ser definidas pelas macros dependentes do processador ou


por fun��es min�sculas que montam as instru��es preenchendo os campos da palavra de
instru��o. Hipoteticamente, n�s poder�amos ter uma fun��o chamada popreg que gera
c�digo para tirar um valor da pilha e armazen�-lo em um registro de processador, e
outra chamada pushreg que gera c�digo para tirar o valor armazenado em um registro
e empurr�-lo para a pilha. Nossa fun��o addop revisada os usaria assim, dadas
algumas constantes definidas que descrevem as instru��es (como ADDINST) e seu
layout (as diversas posi��es de SHIFT que definem o formato):

/* addop: gera instru��o ADD */


void addop(void)
{
Code inst;

popreg (2); /* tira a pilha para o registro 2 */


popreg(l); /* tira a pilha para o registro l */
inst = ADDINST " INSTSHIFT;
inst |= (R1) " OP1SHIFT;
inst |= (R2) " OP2SHIFT;
emit(inst); /* emite ADD R1, RG@ */
pushreg(2); /* empurra val do registro 2 para a pilha */
}

Esse � apenas um ponto de partida. Se estiv�ssemos escrevendo um compilador on the


fly de verdade, empregar�amos as otimiza��es. Se estiv�ssemos adicionando uma
constante, n�o precisar�amos empurrar a constante para a pilha, pux�-la e adicion�-
la. Podemos adicion�-la diretamente. Um racioc�nio semelhante pode eliminar mais da
overhead. Mesmo escrita, por�m, addop ser� executada muito mais r�pido do que as
vers�es que escrevemos anteriormente porque os diversos operadores n�o s�o
encadeados pelas chamadas de fun��o. Em vez disso, o c�digo para execut�-las �
feito na mem�ria como um �nico bloco de instru��es, com o contador de programa do
processador real fazendo todo o encadeamento para n�s.
A fun��o generate se parece muito com aquela que criamos para a
implementa��o da m�quina virtual. Mas desta vez, ela cria instru��es de m�quina
reais em vez de ponteiros para fun��es predefinidas. E para gerar c�digo eficiente
� preciso procurar constantes a serem eliminadas e outras otimiza��es.
Nosso tour pela gera��o de c�digo mostrou apenas relances de algumas das
t�cnicas usadas pelos compiladores reais e deixou de falar de muitas outras. Ele
tamb�m ignorou muitas das quest�es levantadas pelas complexidades das CPUs
modernas. Mas esse tour ilustra como um programa pode analisar a descri��o de um
problema para produzir c�digo de prop�sito especial a fim de solucion�-lo com
efici�ncia. Voc� pode usar essas ideias para escrever uma vers�o incrivelmente
r�pida de grep, para implementar uma linguagem pequena pr�pria, para criar e
construir uma m�quina virtual otimizada para c�lculo de prop�sito especial, ou
mesmo com um pouco de ajuda, para escrever um compilador para uma linguagem
interessante.
Uma express�o regular est� muito longe de um programa em C++, mas ambos s�o
apenas nota��es para solucionar problemas. Com a nota��o certa, muitos problemas se
tornam mais f�ceis. A cria��o e implementa��o da nota��o pode ser bastante
divertida.
� Exerc�cio 9-18. O compilador on the fly gera c�digo mais r�pido quando ele pode
substituir express�es que cont�m apenas constantes, tais como max(3*3, 4/2), por
seus valores. Depois de reconhecer tal express�o, como ele calcularia seu valor? ?

� Exerc�cio 9-19. Como voc� testaria um compilador on the fly? ?

Leitura suplementar

O livro The Unix Programming Environment, de Brian Kernighan e Rob Pike


(Prentice Hall, 1984), cont�m uma discuss�o mais ampla sobre a abordagem baseada em
ferramenta para a computa��o que o Unix suporta t�o bem. O Cap�tulo 8 daquele livro
apresenta uma implementa��o completa, da gram�tica yacc ao c�digo execut�vel, de
uma linguagem simples de programa��o.
O livro TEX: The Program, de Don Knuth (Addison-Wesley, 1986), descreve um
formatador de documento complexo apresentando todo o programa, cerca de 13.000
linhas de Pascal, em um estilo de "programa��o literata" que combina a explica��o
com texto de programa e usa os programas para formatar a documenta��o e extrair
c�digo compil�vel. O livro A Retargetable C Compiler: Design and Implementation, de
Chris Fraser e David Hanson (Addison-Wesley, 1995) faz o mesmo para um compilador
ANSI C.
A m�quina virtual Java � descrita no livro The Java Virtual Machine
Specification, 2nd Edition, de Tim Lindholm e Frank Yellin (Addison-Wesley, 1999).
O algoritmo de Ken Thompson (uma das primeiras patentes de software) foi
descrito em "Regular Expression Search Algorithm", Communications of the ACM, 11,
6, p�gs. 419-422, 1968. O livro Mastering Regular Expressions, de Jeffrey E. F.
Friedl (O'Reilly, 1997) trata o assunto de forma extensa.
Um compilador on the fly para opera��es gr�ficas bidimensionais � descrito
em "Hardware/Software Tradeoffs for Bitmap Graphics on the Blit", de Rob Pike, Bart
Locanthi e John Reiser, Software - Practice and Experience, 15, 2, p�gs. 131-3 152,
fevereiro de 1985.

Ep�logo

Se os homens pudessem aprender com a hist�ria, quais li��es ela nos ensinaria! Mas
a paix�o e o partidarismo cegam nossos olhos, e a luz que a experi�ncia proporciona
� uma lanterna de popa, que brilha apenas nas ondas que est�o atr�s de n�s!
Samuel Taylor Coleridge, Recollections

O mundo da computa��o muda o tempo todo, e o passo parece se acelerar. Os


programadores devem lidar com as linguagens, ferramentas, sistemas novos e, �
claro, com as altera��es incompat�veis com as anteriores. Os programas s�o maiores,
as interfaces s�o mais complicadas, os prazos s�o menores.
Mas existem algumas constantes, alguns pontos de estabilidade, onde as
li��es e ideias do passado podem ajudar no futuro. Os temas b�sicos deste livro se
baseiam nesses conceitos duradouros.
Simplicidade e clareza s�o as coisas principais e as mais importantes, uma
vez que quase tudo o mais vem delas. Fa�a a coisa mais simples que funcione.
Selecione o algoritmo mais simples que deve ser suficientemente r�pido, e a
estrutura de dados mais simples que far� o trabalho; combine-os com c�digo claro e
inteligente. N�o os complique, a menos que as medi��es de desempenho mostrem que �
preciso ter mais engenharia. As interfaces devem ser inteligentes e escassas, pelo
menos at� que haja evid�ncia suficiente de que os benef�cios ser�o maiores que a
complexidade adicional.
A generalidade quase sempre anda lado a lado com a simplicidade, pois ela
torna poss�vel a solu��o de um problema de uma vez por todas, em vez de solucion�-
lo para os casos individuais. Quase sempre essa tamb�m � a abordagem certa para a
portabilidade: encontrar a �nica solu��o geral que funciona em cada sistema, em vez
de ampliar as diferen�as entre os sistemas.
A evolu��o vem a seguir. N�o � poss�vel criar um programa perfeito na
primeira vez. A concentra��o necess�ria para encontrar a solu��o certa vem somente
com uma combina��o de considera��o e experi�ncia; a pura introspec��o n�o produzir�
um sistema bom, nem o puro hacking. As rea��es dos usu�rios contam muito, aqui. Um
ciclo de prot�tipos, experi�ncia, feedback do usu�rio e mais refinamento � mais
efetivo. Os programas que constru�mos para n�s mesmos quase sempre n�o evoluem o
suficiente. Os programas grandes que compramos dos outros mudam muito r�pido, sem
necessariamente ser aperfei�oados.
As interfaces s�o grande parte da batalha da programa��o, e as quest�es de
interface aparecem em muitos lugares. As bibliotecas apresentam os casos mais
�bvios, mas tamb�m existem interfaces entre os programas e entre os usu�rios e os
programas. O desejo de simplicidade e generalidade se mostra particularmente forte
no projeto das interfaces. Fa�a interfaces consistentes, f�ceis de aprender e usar,
e siga-as escrupulosamente. A abstra��o � uma t�cnica efetiva: imagine um
componente perfeito, ou uma biblioteca ou programa; fa�a com que a interface
coincida com aquele ideal o mais fielmente poss�vel. Oculte os detalhes da
implementa��o por detr�s do limite, fora do caminho dos danos.
A automa��o n�o tem o reconhecimento merecido. � muito mais efetivo ter um
computador que fa�a o seu trabalho do que faz�-lo � m�o. Vimos exemplos de teste,
depura��o, an�lise de desempenho e, principalmente, de como escrever c�digo, onde
para o dom�nio do problema certo os programas podem criar programas que seriam
dif�ceis para as pessoas escreverem.
A nota��o tamb�m n�o tem reconhecimento, e n�o apenas como o modo pelo qual
os programadores dizem aos computadores o que fazer. Ela fornece uma estrutura de
organiza��o para implementar uma ampla variedade de ferramentas e tamb�m orienta a
estrutura dos programas que escrevem programas. N�s nos sentimos bem com as
linguagens de prop�sito geral que servem para a maior parte �je nossa programa��o.
Mas as tarefas se tornam t�o espec�ficas e bem entendidas que a sua programa��o
parece quase mec�nica; talvez seja preciso tempo para criar uma nota��o que
expresse naturalmente as tarefas e uma linguagem que a implemente. As express�es
regula�es s�o um de nossos exemplos preferidos, mas existem oportunidades
incont�veis para criar linguagens pequenas para aplica��es especializadas. Elas n�o
t�m que ser sofisticadas para colher benef�cios.

Como programadores individuais, � f�cil nos sentirmos como pequenos


parafusos de uma grande m�quina, usando as linguagens, os sistemas e ferramentas
que nos s�o impostos, fazendo tarefas que deveriam ser feitas por n�s. Mas a longo
prazo, o que conta � o modo como trabalhamos com aquilo que temos. Aplicando
algumas das ideias deste livro, voc� deve descobrir que o seu c�digo � mais f�cil
de trabalhar, suas sess�es de depura��o s�o menos dolorosas e sua programa��o mais
confiante. Esperamos que este livro lhe tenha dado algo que tornar� sua computa��o
mais produtiva e gratificante.

Ap�ndice: regras compiladas

Cada verdade que eu descobri se tornou uma regra que me serviu


depois na descoberta de outras.
Ren� Descartes, Le Discours de la M�thode

V�rios cap�tulos cont�m regras ou orienta��es que resumem uma discuss�o. As


regras foram reunidas aqui para facilitar a refer�ncia. Tenha sempre em mente que
cada uma foi apresentada em um contexto que explica seu prop�sito e aplica��o.
Estilo
Use nomes descritivos para os globais, nomes curtos para os locais.
Seja consistente.
Use nomes ativos para as fun��es.
Seja preciso.
Recue para mostrar a estrutura.
Use a forma natural para as express�es.
Coloque par�nteses para resolver a ambiguidade.
Divida as express�es complexas.
Seja claro.
Tome cuidado com os efeitos colaterais.
Use estilo consistente de recuo e colchetes.
Use os idiomas para ter consist�ncia.
Use else-ifs para decis�es multivias.
Evite as macros de fun��o.
Coloque par�nteses no corpo da macro e nos argumentos.
D� nomes aos n�meros m�gicos.
Defina os n�meros como constantes, n�o como macros.
Use as constantes de caracteres, n�o os inteiros.
Use a linguagem para calcular o tamanho de um objeto.
N�o explique o �bvio.
Comente as fun��es e os dados globais. N�o comente c�digo ruim, reescreva-o. N�o
contradiga o c�digo. Esclare�a, n�o confunda.

Interfaces
Oculte os detalhes da implementa��o.
Selecione um conjunto ortogonal pequeno de primitivos.
N�o v� al�m do usu�rio.
Fa�a a mesma coisa da mesma forma em todos os lugares.
Libere um recurso na mesma camada em que ela foi alocada.
Detecte os erros em um n�vel baixo, trate deles em um n�vel alto.
Use as exce��es apenas para as situa��es excepcionais.

Depura��o
Procure padr�es familiares.
Examine a altera��o mais recente.
N�o cometa o mesmo erro duas vezes.
Depure agora, n�o mais tarde.
Obtenha um rastreamento de pilha.
Leia antes de digitar.
Explique o seu c�digo para outra pessoa.
Torne o bug reproduz�vel.
Divida e conquiste.
Estude a numerologia das falhas.
Exiba a sa�da para localizar sua pesquisa.
Escreva c�digo como autoverifica��o.
Escreva um arquivo de registro.
Desenhe uma figura.
Use as ferramentas.
Mantenha registros.

Testando
Teste o c�digo em seus limites. Teste pr� e p�s-condi��es. Use as afirma��es.
Programe defensivamente. Verifique os retornos de erro. Teste incrementalmente.
Teste primeiro as partes simples.
Saiba qual sa�da pode esperar.
Verifique as propriedades de conserva��o.
Compare as implementa��es independentes.
Me�a a cobertura do teste.
Automatize o teste de regress�o.
Crie testes autocontidos.

Desempenho
Automatize as medi��es de tempo.
Use um profiler.
Concentre-se nos pontos ativos.
Desenhe uma figura.
Use um algoritmo ou estrutura de dados melhores.
Ative as otimiza��es do compilador.
Ajuste o c�digo.
N�o otimize o que n�o importa.
Re�na as subexpress�es comuns.
Substitua as opera��es caras por opera��es baratas.
Desenrole ou elimine os loops.
Fa�a o cache dos valores mais usados.
Escreva um alocador de prop�sito especial.
Fa�a o buffer de entrada e sa�da.
Trate separadamente dos casos especiais.
Pr�-calcule os resultados.
Use valores aproximados.
Reescreva em uma linguagem de n�vel mais baixo.
Economize espa�o usando o menor tipo de dados poss�vel.
N�o armazene aquilo que voc� pode recalcular facilmente.

Portabilidade
Siga o padr�o.
Programe no mainstream.
Cuidado com as �reas problem�ticas da linguagem.
Experimente v�rios compiladores.
Use as bibliotecas padr�o.
Use apenas os recursos dispon�veis em toda parte.
Evite a compila��o condicional.
Localize as depend�ncias do sistema em arquivos separados.
Oculte as depend�ncias do sistema por tr�s das interfaces.
Use o texto para a troca de dados.
26
Use uma ordem de byte fixa para a troca de dados.
Altere o nome se voc� mudar a especifica��o.
Mantenha a compatibilidade com os programas e dados existentes.
N�o assuma o ASCII.
N�o assuma o ingl�s.

�ndice

Mulher: Minha tia Mittnie est� aqui?

Driftwood: Bem, voc� pode entrar e espiar se quiser. Se ela n�o estiver, talvez
voc� encontre algu�m t�o bom quanto ela.

Os Irm�os Marx, A Night at the Opera

Orelha:

A pr�tica da programa��o � mais do que simplesmente escrever c�digo. Os


programadores tamb�m devem avaliar as op��es, escolher entre alternativas de
projeto, depurar, testar, melhorar o desempenho e atualizar o software escrito por
eles e por outras pessoas. Eles tamb�m devem se preocupar com quest�es como
compatibilidade, robustez e confiabilidade, atenendo, ao mesmo tempo, �s
especifica��es.
O livro inclui cap�tulos sobre:
* Depura��o: encontrando os bugs de forma r�pida e met�dica.
* Testando: garantindo que o software vai funcionar de modo correto e confi�vel.
* Desempenho: como tornar os programas mais r�pidos e compactos.
* Portabilidade: garantindo que os programas funcionar�o em qualquer lugar sem
altera��es.
* Projeto: considerando objetivos e restri��es para resolver quais algoritmos e
estruturas de dados s�o melhores.
* Interfaces: usando a abstra��o e ocultando informa��es par controlar as
intera��es entre os componentes.
* Estilo: escrevendo c�digo que funcione e seja agrad�vel de ler.
* Nota��o: escolhendo liguagens e ferramentas que permitem � m�quina fazer uma
parte maior do trabalho.

Para criar este livro Kernighan e Pike usaram sua experi�ncia de anos em escrever
programas, ensinar e trabalhar com outros programadores. Todos que escrevem
software se beneficiar�o com a orienta��o e os princ�pios encontrados neste livro.

Brian W. Kernighan e Rob Pike trabalham no Computing Science Research Center da


Bell Laboratories, Lucent Technologies. Brian Kernighan � editor-consultor da S�rie
de Computa��o Profissional da Addison-Wesley e autor, juntamente com Dennis
Ritchie, do livro C: A Linguagem de Programa��o (Campus) e C: A Linguagem de
Programa��o (Padr�o ANSI). Rob Pike foi um dos primeiros arquitetos e
implementadores dos sistemas operacionais Plan 9 e Inferno.

Você também pode gostar