Escolar Documentos
Profissional Documentos
Cultura Documentos
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
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.
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.
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:
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
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:
com
? 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
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)) ...
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) <= '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:
? #define TRUE 0
? #define FALSE 1
?
? if ((ch = getchar()) == EOF)
? not_eof = FALSE;
?
? for(n++;n<100;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 (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)
...
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?
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:
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:
? 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:
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:
? array[i++] = i;
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]);
? flag = flag ? 0 : 1;
? if (val & 1)
? bit = 1;
? else
? bit = 0;
?
� 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.
?
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;
? }
? 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;
? for (i = 0; i < n; )
? array[i++] = 1.0;
ou mesmo:
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:
Novamente, todo o controle do loop est� no for. Para um loop infinito, preferimos:
for (;;)
...
mas,
while (1)
...
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
? )
? {
? ;
? )
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:
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:
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);
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;
? }
if (c == '-') {
sign = -1;
c = getchar();
} else if (c == '+') {
c = getchar();
} else if (c != '.' && !isdigit(c)) {
return 0;
}
case '0':
case 'l':
case '2':
...
break;
e nenhum coment�rio � requerido.
? if (istty(stdin));
? else if (istty(stdout));
? else if (istty(stderr));
? else return(0);
? if (retval != SUCCESS)
? {
? return (retval);
? }
? /* Tudo correu bem! */
? return SUCCESS;
? int count = 0;
? while (count < total) {
? count++;
? if (this.getName(count) == nametable.userName()) {
? return (true);
? )
? }
?
Observe que o par�metro c ocorre duas vezes no corpo da macro. Se isupper � chamado
em um contexto como este,
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.)
1 / square(x)
funciona bem se square � uma fun��o, mas se ela for uma macro como esta
? 1 / (x) * (x)
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:
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.
ou final em Java:
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.
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;
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:
double dbuf[100];
for (i = 0; i < NELEMS(dbuf); i++)
...
1.6 Coment�rios
? /*
? * default
? */
? default:
? break;
? /* retorna SUCCESS */
? return SUCCESS;
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:
/*
* 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
*/
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:
#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;
? 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�.
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:
? void dict::insert(string& w)
? // retorna l se w est� no dicion�rio, caso contr�rio, retorna 0
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
2.1 Pesquisando
char *flab[] = {
"actually",
"just",
"quite",
"really",
NULL };
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:
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 */
}
2.2 Classificando
temp = v[i];
v[1] = v[j];
v [j] = temp;
}
2.3 Bibliotecas
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:
char *str[N];
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;
int arr[N];
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:
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:
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);
interface Cmp {
int cmp(Object x, Object y);
}
Quicksort.sort usa cmp para comparar um par de objetos, e chama swap antes de
intercambi�-los.
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:
� 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
� 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. ?
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++.
struct NVtab {
int nval; /* n�mero atual de valores */
int max; /* n�mero alocado de valores */
Nameval *nameval; /* array de pares nome-valor */
} nvtab;
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? ?
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:
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:
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 �:
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:
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:
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:
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,
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:
Para contar os elementos, definimos uma fun��o cujo argumento � um ponteiro para um
inteiro a ser incrementado:
/* 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:
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,
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 */
}
� 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-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
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:
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:
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�.
� 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? ?
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:
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.
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;
}
if (lookup("name") == NULL)
additem(newitem("name", value));
enum { MULTIPLIER = 31 };
h = 0;
for (p = (unsigned char *) str; *p != '\0'; p++)
h = MULTIPLIER * h + *p;
return h % NHASH;
}
� 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-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
Leitura suplementar
Projeto e implementa��o
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)
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.
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 */
};
h = 0;
for (i = 0; i < NPREF; i++)
for (p = (unsigned char *) s[i]; *p != '\0'; p++)
h = MULTIPLIER * h + *p;
return h % NHASH;
}
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:
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;
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:
Rand() % ++nmatch == O
� 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. ?
3.5 Java
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();
...
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".
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.
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:
e equals faz uma compara��o elementwise das palavras dos dois prefixos:
� 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++
A fun��o build usa a biblioteca iostream para ler a entrada uma palavra de
cada vez:
� 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. ?
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
$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
}
push(@{$statetab{$w1}($w2)}, $_);
� 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.
Leitura suplementar
Interfaces
,"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:
"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
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.
% 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'
...
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.
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.
int csvnfield(void);
retorna o n�mero de campos da �ltima linha lida por csvgetline.
comportamento indefinido se chamado antes de csvgetline ser chamada.
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.
� 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-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. ?
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.
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.
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.
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;
}
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;
}
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
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.
strcpy(csvfield(1), csvfield(2));
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.
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>
fflush(stdout);
if (progname() != NULL)
fprintf(stderr, "%s: ", progname());
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
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:
t = (char *)malloc(strlen(s)+1);
if (t == NULL)
eprintf("estrdup(\"%.20s\") failed:", s);
strcpy(t, s);
return t;
}
p = malloc(n);
if (p == NULL)
eprintf("malloc of %u bytes failed:", n);
return p;
}
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:
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>
imprime:
Como mostramos, errno deve ser declarado primeiro. Depois se ocorrer um erro, errno
ser� definido com um valor diferente de zero.
estrdup failed
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:
Leitura suplementar
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
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.
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.
? 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);
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:
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:
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.
int arr[N];
qsort(arr, N, sizeof(arr[0]), icmp);
? 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:
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
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.
"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.
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.
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.
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;
}
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:
? switch (c) {
? case '<':
? mode = LESS;
? break;
? case '>':
? mode = GREATER;
? break;
? defualt:
? mode = EQUAL;
? break;
? }
em vez de
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:
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:
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:
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:
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. ?
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
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
? 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:
Toda linha de texto imprim�vel servir� como um caso de teste, uma vez que a sa�da
conter� apenas metade da entrada:
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
Leitura suplementar
Testando
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.
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:
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:
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.
Se a afirma��o for violada, ela far� o programa abortar com uma mensagem padr�o:
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:
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.
? 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');
? if (i > j)
? printf("%d is greater than %d.\n", i, j);
? else
? printf("%d is smaller than %d.\n", i, j);
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?
?
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.
#include <stdio.h>
#include <ctype.h>
#include <limits.h>
� 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? ?
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.
� Exerc�cio 6-5. Crie uma su�te de testes para printf, usando o m�ximo poss�vel de
aux�lios t�cnicos. ?
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:
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:
� Exerc�cio 6-6. Crie o andaime de teste para memset conforme indicamos acima. ?
� 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. ?
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:
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:
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);
� Exerc�cio 6-10. Tente criar um arquivo que d� pane em seu editor de texto,
compilador ou outro programa preferido. ?
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.
(arquivo vazio)
a
a b
a b c
a b c d
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
Desempenho
Suas promessas eram, assim como ele o era na �poca, poderosas; Mas seu desempenho
era, assim como ele � agora, inexistente.
Shakespeare, Henrique VIII
7.1 Um gargalo
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:
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.
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 i;
unsigned char c;
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. ?
% 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);
before = clock();
for (i = 0; i < 1000; i++)
short_running_function();
elapsed = (clock()-before)/(double)i;
% 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.
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.
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:
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:
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. ?
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
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.
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:
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.
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:
por:
n = nstartingfc];
for (i = 0; i < n; i++) {
se torna:
se torna:
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);
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.
� 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? ?
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:
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
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:
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
� 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. ?
7.7 Resumo
Leitura suplementar
Portabilidade
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"};
? 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.
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:
mas sem d�vida outros valores s�o poss�veis. Algumas m�quinas de 64 bits produzem o
seguinte:
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.
? ptr[count] = name[++count];
count poderia ser incrementada antes ou ap�s ser usada para indexar ptr e em
o primeiro caractere de entrada poderia ser impresso em segundo lugar em vez de ser
impresso em primeiro lugar. Em
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];
A Java n�o tem qualificador unsigned. Os tipos integrais s�o signed e o tipo
char (16 bits) n�o �.
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.
struct X {
char c;
int i ;
}
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.
? #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:
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:
? #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:
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
? #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:
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.
#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:
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.
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
/* 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;
}
11 22 33 44
44 33 22 11
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
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);
unsigned short x;
fread(&x, sizeof(x), 1, stdin);
? short x;
? fread(&x, sizeof(x), 1, stdin);
? #ifdef BIG_ENDIAN
? /* swap bytes */
? x = ((x&0xFF) " 8) | ((x"8)& 0xFF);
? #endif
unsigned short x;
putchar(x " 8); /* escrever byte de ordem alta */
putchar(x & 0xFF); /* escrever byte de ordem baixa */
unsigned short x;
x = getchar() " 8; /* ler o byte de ordem alta */
x |= getchar() & 0xFF; /* ler o byte de ordem baixa */
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.
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\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
$
% 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
>
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)) ...
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
9
Nota��o
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:
Uma abordagem seria escrever as fun��es pack e unpack para cada tipo
poss�vel de pacote:
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:
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:
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:
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:
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:
� 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-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. ?
##,##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. ?
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?
Qual o implementa?
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:
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:
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;
}
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
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
com
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:
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-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
� 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? ?
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.
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.
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:
$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 " " � 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:
a = max(b, c/2);
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;
};
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;
/*... */
}
}
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:
pushsymop
b
pushsymop
c
pushop
2
divop
maxop
storesymop
a
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];
}
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.
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 */ };
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";
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
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. ?
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) ; \
}
LOOP(f1 = f2)
LOOP(f1 = f2 + f3)
LOOP(f1 = f2 - f3)
max(b, c/2)
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:
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:
Leitura suplementar
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
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
Driftwood: Bem, voc� pode entrar e espiar se quiser. Se ela n�o estiver, talvez
voc� encontre algu�m t�o bom quanto ela.
Orelha:
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.