Fato Histórico 4
Sintaxe 4.4: Comando while while (condition) statement
4.7 Usando variáveis booleanas
Algumas vezes você necessita avaliar uma condição lógica em uma parte do programa e usá-la em al- gum outro ponto. Para armazenar uma condição que pode ser verdadeira ou falsa, você necessita uma variável booleana, de um tipo especial de dado, bool. Variáveis booleanas são assim denominadas em homenagem ao matemático George Boole (1815–1864), um pioneiro no estudo da lógica.
BOOLE PEDE SEU ALMOÇO
NÃO, NÃO, SIM, NÃO, NÃO, SIM, SIM, NÃO,
Variáveis do tipo bool podem armazenar somente dois valores, denotados por false e
true. Estes valores não são strings ou inteiros; eles são valores especiais, apenas para variáveis booleanas.
Aqui está um uso típico de uma variável booleana. Você pode decidir que a combinação de fa- zer entrada de dados com testar se houve sucesso
while (cin >> next)
é bastante complexa. Para desassociar os dois, você pode usar uma variável booleana que controle o laço.
bool more = true; while (more) { cin >> next; if (cin.fail()) more = false; else { processa next } }
A propósito, é considerado esquisito escrever um teste como while (more == true) /* não faça */
ou
while (more != false) /* não faça */
Use o teste mais simples while (more)
Alguns programadores não gostam de introduzir uma variável booleana para controlar um la- ço. O Tópico Avançado 4.2 mostra uma alternativa.
Tópico Avançado
4.2
O Problema do Laço-e-Meio
Alguns programadores não gostam de laços que são controlados por variáveis booleanas, como em:
bool more = true; while (more) { cin >> x; if (cin.fail()) more = false; else { processa x } }
O verdadeiro teste para término do laço está no meio do laço, e não em seu início. Isto é cha- mado de laço-e-meio porque é preciso ir até a metade do caminho do laço antes de saber se é pre- ciso terminar.
while (true) { cin >> x; if (cin.fail()) break; processa os dados }
O comando breakencerra o laço circundante, independentemente da condição do laço. Em geral, um breaké uma maneira muito pobre de sair de um laço. O mau uso de um break
causou a falha do sistema de chaveamento de telefones da AT&T 4ESS em 15 de janeiro de 1990. A falha se propagou por toda a rede americana, tornando-a quase inútil por cerca de 9 horas. Um pro- gramador havia usado um breakpara terminar um comando if. Infelizmente, breaknão pode ser usado com if, e assim a execução do programa saiu fora do comando, pulando algumas inicia- lizações de variáveis e indo em direção ao caos (ver referência [1], p. 38). Usar comandos break
também dificulta o uso de técnicas de prova de correção (veja o Tópico Avançado 4.3).
No caso de laço-e-meio, comandos break podem trazer benefícios. Mas é difícil estabelecer regras claras sobre quando eles são seguros e quando eles devem ser evitados. Nós não usamos o comando break neste livro.
Erro Freqüente
4.5
Detecção de Fim de Arquivo
Ao ler uma quantidade indeterminada de dados de um stream, você pode ler até encontrar um valor sentinela ou ler até o final da entrada. Detectar o final da entrada requer um pouco de engenhosi- dade. Existe uma função eofque reporta a condição de fim de arquivo (“end of file”), mas você pode chamá-la com resultados confiáveis somente após o stream de entrada haver falhado. O la- ço a seguir não funciona:
while (more) {
cin >> x;
if (cin.eof()) /* Não faça! */ { more = false; } else { sum = sum + x; } }
Se o stream de entrada falhar por outra razão (usualmente por ter sido encontrado um não-nú- mero na entrada), então todas as operações de entrada subsequentes falharão e o final de arquivo nunca será encontrado. O laço então se torna um laço infinito. Por exemplo, considere a entrada
↑
cin falha aqui, mas o fim do arquivo ainda não foi encontrado
Em vez disso, primeiro teste se houve falha e então teste eof: bool more = true;
while (more) {
cin >> x; if (cin.fail()) {
more = false; if (cin.eof())
cout << "Final dos dados"; else
cout << "Dado de entrada incorreto "; } else { sum = sum + x; } }
Aqui está um outro erro comum. while (cin)
{
cin >> x;
sum = sum + x; /* Não faça! */ }
Você deve testar se houve falha após cada entrada. Se o último item no arquivo for sucedido por um espaço em branco (é normalmente sucedido por um caractere de nova linha), então aquele espaço em branco pode mascarar o fim de arquivo. Considere o seguinte exemplo de entrada:
← Fim do arquivo
↑
cin não falhou
e o final do arquivo ainda não foi encontrado
Somente quando outra leitura da entrada é tentada após o último valor ter sido lido, o fim de ar- quivo é reconhecido e a entrada falha. Então xnão deve ser adicionado novamente a sum.
Existe uma outra função para testar o estado do stream: good. Infelizmente, não é uma boa idéia usá-la. Se você lê o último item de um stream, então a entrada terá sucesso, mas uma vez que o fim de arquivo tenha sido encontrado, o estado do stream de entrada não mais será bom. Isto é, um teste
while (more) {
cin >> x;
if (cin.good()) /* Não faça! */ {
sum = sum + x; }
}
pode omitir a última entrada. Isto não é bom. Você não pode usar goodpara verificar se a entrada anterior teve sucesso. Nem você pode usar goodpara verificar se a próxima entrada terá sucesso.
if (cin.good()) /* Não faça! */ {
cin >> x; sum = sum + x; }
Se o próximo item da entrada não estiver corretamente formatado, a entrada irá falhar, mesmo que o estado do stream tenha sido bom até agora.
Parece que esta função não tem nenhum bom uso. O mau uso dela é um erro comum, talvez porque programadores preferem o carinhoso cin.good()ao rigoroso cin.fail().
Tópico Avançado
4.3
Invariantes de Laço
Considere a tarefa de calcular an, onde a é um número em ponto flutuante e n é um inteiro positi- vo. Naturalmente, você pode multiplicar a × a . . . × a, n vezes, mas se n é grande, você terminará fazendo muitas multiplicações. O laço a seguir faz rigual a anem poucos passos:
double r = 1; double b = a; int i = n; while (i > 0) { if (i % 2 == 0) /* n é par */ { b = b * b; i = i / 2; } else { r = r * b; i--; } }
Considere o caso n = 100. A função executa os seguintes passos
Bastante surpreendente é que o algoritmo fornece exatamente a100. Você consegue entender porque? Você está convencido que isto irá funcionar para todos os valores de n? Aqui está um ar- gumento esperto para mostrar que a função sempre calcula o resultado correto. Ele demonstra que sempre que o programa atinge o início do laço while, é verdadeiro que
(I) Certamente, é verdadeiro na primeira volta, por que b = a e i = n. Suponha que (I) se aplica no início do laço. O programa rotula os valores de r, be i como “antigos” ao entrar no laço, e os ro- tula como “novos” ao sair do laço. Assuma que na entrada
rantigo ⋅bantigoiantigo = an r⋅bi = an i b r 100 a 1 50 a2 25 a4 24 a4 12 a8 6 a16 3 a32 2 a36 1 a64 0 a100
No laço você deve distinguir dois casos: ipar e iímpar. Se né par, o laço realiza as seguintes transformações:
Portanto,
Por outro lado, se ié ímpar, então
Portanto,
Em ambos os casos, os novos valores para r, b, e i atendem à invariante do laço (I). E então? Quando o laço finalmente termina, (I) se aplica novamente:
Além disso, você sabe que i = 0 desde que o laço esteja terminado. Mas como i = 0, r ⋅ bi = r ⋅ b0 = r. Portanto, r = ane a função realmente calcula a n-ésima potência de a.
Esta técnica é bastante útil por que ela pode explicar um algoritmo que não é de todo óbvio. A condição (I) é chamada de invariante de laço por que ela é verdadeira na entrada do laço, ao iní- cio de cada passo e quando o laço se encerra. Se uma invariante de laço é escolhida com habilidade, po- de ser possível deduzir provas de correção de uma computação. Veja [5] para um outro belo exemplo.
Fato Histórico
4.2
Provas de Correção
No Tópico Avançado 4.3 nós introduzimos a técnica de invariantes de laço. Se você ignorou aque- la nota, dê uma olhada nela agora. Esta técnica pode ser usada para provar rigorosamente que uma função retorna exatamente o valor que supostamente deve calcular. Tal prova é muito mais valiosa que qualquer teste. Não importando quantos casos de teste você tentou, você sempre vai preocu- par-se com outro caso que não tentou e que poderia revelar uma falha. Uma prova determina a cor- reção para todas as entradas possíveis.
Por algum tempo, os programadores estiveram muito esperançosos de que provas de correção como invariantes de laço poderiam reduzir grandemente a necessidade de testes. Você poderia pro- var que cada função e procedimento simples está correta e então colocar os componentes provados juntos e provar que eles funcionam juntos como deveriam. Uma vez provado que mainfunciona
r⋅ bi = an
rnovo ⋅bnovoinovo = rantigo ⋅bantigo ⋅bantigoianttigo antigo antigo antigo n − = ⋅ = 1 r b a i r r b b b i i
novo antigo antigo novo antigo novo an = ⋅ = = ttigo − 1 r b i r b i
novo novo antigo antigo
novo antigo ⋅ = ⋅
( )
2⋅ // 2 = ⋅ = r b a i antigo novo n novo r r b b i i novo antigo novo antigo novo antigo = = = 2 /corretamente, mais nenhum teste é necessário! Alguns pesquisadores estavam tão excitados com estas técnicas que eles tentaram omitir completamente todo o passo de programação. O projetista poderia escrever os requisitos do programa usando a notação da lógica formal. Um provador auto- mático poderia provar que este tal programa poderia ser escrito e gerar o programa como parte de sua prova.
Infelizmente, na prática estes métodos nunca funcionaram muito bem. A notação lógica para descrever o comportamento de um programa é complexa. Mesmo cenários simples exigem muitas fórmulas. É suficientemente fácil expressar a idéia que uma função deve calcular an, mas as fórmu- las lógicas para descrever todos os procedimentos de um programa que controla um avião, por exemplo, encheriam várias páginas. Estas fórmulas são criadas por humanos e humanos cometem erros quando lidam com tarefas difíceis e tediosas. Experimentos mostraram que em vez de pro- gramas com erros, programadores escreveram especificações lógicas com erros e provas de progra- mas com erros.
Van der Linden [1], p. 287, fornece alguns exemplos de provas complicadas que são muito mais difíceis de verificar do que os programas que eles estavam tentando provar.
Técnicas de provas de programas são valiosas para provar a corretude de procedimentos indi- viduais que fazem computações de maneiras não óbvias. Atualmente, no entanto, não existe mais esperança de provar a correção de algo além dos mais triviais programas, de maneira que a especi- ficação e a prova possam ser mais confiáveis do que o programa.
Resumo do capítulo
1. O comando ifpermite que um programa execute diferentes ações dependendo da