Aula 7


1  Resumo da matéria

1.1  Instruções de iteração em C++

Na maioria dos programas há conjuntos de operações que é necessário repetir várias vezes: afinal, conhece alguma receita de cozinha que não implique alguma forma de repetição ou pelo menos de espera por uma condição?  Para controlar o fluxo do programa de modo a que um conjunto de instruções sejam repetidos condicionalmente usam-se as instruções iterativas * ou cíclicas.

* De acordo com [1], iterar significa "tornar a fazer (...) repetir (...)" enquanto recorrer significa "tornar a correr (...)".  São, portanto, conceitos semelhantes.  Em termos de linguagens de programação, no entanto, envolvem mecanismos diferentes, como se verá.  No entanto há relações fortes entre os dois conceitos (ver discussão acerca de como transformar soluções recursivas em soluções iterativas em [2, secção 3.4]).

1.1.1  A instrução de iteração while

A instrução while tem uma sintaxe bastante simples:
while(expressão_booleana)
    instrução
A execução da instrução while consiste na execução repetida de instrução enquanto espressão_booleana tiver o valor verdadeiro (true).  A expressão_booleana é sempre calculada antes da instrução, que, se a expressão for falsa (false), não chega a ser executada, passando o fluxo de execução para a instrução subsequente ao while.  Note-se que instrução pode consistir em qualquer instrução do C++, nomeadamente um bloco de instruções.

À expressão booleana de controlo do while chama-se a guarda do ciclo, enquanto à instrução controlada se chama passo.  Assim, o passo é repetido enquanto a guarda for verdadeira.

Por exemplo, se se pretender escrever um procedimento que, dado como parâmetro um inteiro n, escreva no ecrã uma linha com todos os números inteiros de zero a n (exclusivé):

void escreveInteiros(int n)     // 1
{                               // 2
    int i = 0;                  // 3
    while(i < n) {              // 4
        cout << i << ' ';       // 5
        i++;                    // 6
    }                           // 7
    cout << endl;               // 8
}                               // 9
Neste procedimento começa por se inicializar a variável i com 0 (linha 3) e em seguida executa-se o ciclo while que:
  1. Verifica se a condição i < n é verdadeira (linha 4).
  2. Se a condição for verdadeira, executa as instruções nas linhas 5 e 6, voltando-se depois a verificar a condição (volta-se a A.).
  3. Se a condição for falsa, o ciclo termina.
Finalmente, depois de terminado o ciclo, escreve-se um fim-de-linha.  É de notar que, depois da inicialização e até ao final do ciclo (salvo após a execução da linha 5), se pode afirmar que se escreveram já no ecrã todos os inteiros entre 0 e i - 1 com 0 <= i <= n. (   Como esta afirmação é verdadeira sempre (com a exepção indicada), chama-se-lhe uma condição invariante.  Note-se ainda que quando o ciclo termina a guarda é forçosamente falsa, ou seja a condição i >= n é verdadeira.   A conjunção desta condição com a condição invariante leva-nos a concluir que, no final do ciclo, estão de facto escritos no ecrã todos os inteiros entre 0 e n - 1, pois i >= n (falsidade da guarda) e 0 <= i <= n só podem ser simultaneamente verdadeiras se i = n!

Desenvolvimento de ciclos

A construção de programas usando ciclos é simultanemente uma arte [3] e uma ciência [4].  Embora a intuição seja muito importante, é fundamental usar metologias de desenvolvimento de ciclos que permitam provar simultaneamente a sua correcção.  Embora esta seja matéria para a cadeira de Computação e Algoritmia, apresentar-se-ão informalmente algumas ideias sobre o assunto.

Programar deve ser uma actividade orientada pelos objectivos: pretende-se resolver um problema, i.e., dadas determinadas condições iniciais, atingir determinados objectivos.  A construção de ciclos é feita da mesma forma.  Sejam:

Suponha-se uma dada CI.  Se se demonstrar que, dada PC, inic implica que CI é verdadeira, então CI é verdadeira no início do ciclo.  Por outro lado, se se demonstrar que dadas CI e G, passo implica que CI é verdadeira (isto é, a veracidade da guarda e da condição invariante garantem que, depois do passo do ciclo, a condição invariante é ainda verdadeira), então é claro que CI é verdadeira do início ao fim do ciclo (isto é, CI é de facto invariante).  Finalmente, se se demonstrar que CI e ~ G => CO, então, como a condição invariante é verdadeira no final do ciclo e a guarda é forçosamente falsa, conclui-se forçosamente que o ciclo:
inic
while(G) {
    passo
}
conduz (se terminar) a CO verdadeira se PC também o for.  Estas demonstrações levem à conclusão de que o ciclo está parcialmente correcto, i.e., verificadas as pré-condições, conduz a CO, mas apenas se terminar, o que não se demonstrou.  Para assegurar a correcção do ciclo pode-se usar o conceito de funções de limitação (bound functions) [4].  Aqui, far-se-á apenas uso de conceitos informais.
Exemplo
Pretende-se desenvolver uma função que devolva a potência inteira positiva de um valor real arbitrário.  A interface natural para essa função será: double potência(double x, int n), sendo x o valor a potenciar e n a potência desejada.  Assim, a função terá a forma (em comentários colocam-se as asserções que se garante serem verdadeiras em cada linha da função):
// PC: n > 0 e x = X e n = N
// CO: resultado = XN
double potência(double x, int n)
{
    // PC
    double resultado;
    ....
    // CO
    return resultado;
}
Note-se que se impôs que o resultado corresponde à potência N de X, sendo N e X constantes correspondentes aos valores iniciais de n e x.  Isto serve para evitar soluções ingénuas como *:
// PC: n > 0 e x = X e n = N
// CO: resultado = xn
double potência(double x, int n)
{
    // PC
    double resultado;
    resultado = 0;
    x = 0;
    n = 1;
    // CO
    return resultado;
}
Como desenvolver o interior da função?  Uma potência é a multiplicação de um valor por si próprio várias vezes, pelo que a utilização dum ciclo parece apropriada:
// PC: n > 0 e x = X e n = N
// CO: resultado = XN
double potência(double x, int n)
{
    // PC
    double resultado;
    inic
    // CI: ??
    while(G) {
        // CI e G
        passo
        // CI
    }
    // CI e ~ G => CO
    // CO
    return resultado;
}
A metodologia típica de desenvolvimento de um ciclo corresponde a:
  1. Substituir uma das constantes da CO numa variável com limites apropriados, obtendo-se assim a CI.  Neste caso, a escolha apropriada, como se verá, corresponde a substituir N por uma variável inteira i (por exemplo, o nome não é importante, desde que não colida com os de outras variáveis da função).  Assim, CI: resultado = xi e 0 <= i <= n e x = X e n = N.  Ou seja, a condição invariante indica que resultado contém uma potência de X entre 0 e N.

  2.  
  3. Escolher uma guarda G tal que CI e ~ G => CO.  Neste caso é simples, ~ G deve ser tal que resultado = xi e 0 <= i <= n e x = X e n = N e ~ G obriguem a que i seja n!  A escolha acertada é ~ G: i = n, ou seja, G: i <> n.  Ou seja, traduzindo para C++:

  4.  
      // CI: resultado = xi e 0 <= i <= n e x = X e n = N
      while(i != n) { // G: i <> n
          // CI e G, ou seja, resultado = xi e 0 <= i <= n e x = X e n = N e i <> n
          passo
          // CI
      }
      // CI e i = n => CO
  5. Escolher uma inicialização inic de modo a garantir a veracidade de CI logo no início do ciclo.  Deve-se escolher sempre uma inicialização trivial.  Neste caso, essa escolha corresponde a atribuir 1 a i e x a resultado, pois nesse caso CI é verdadeira: x = x1 e 0 <= 1 <= n e x = X e n = N (pois N > 0!).  Ou seja, traduzindo para C++:

  6.  
      // PC: n > 0 e x = X e n = N
      double resultado = x;
      int i = 1;
      // CI: resultado = xi e 0 <= i <= n e x = X e n = N
    Note-se que se poderia igualmente ter inicializado resultado com 1 e i com 0, mas isso punha o problema da indeterminação 00, pois o valor inicial de x pode ser qualquer.  Como resolveria o problema?
     
  7. Escolher o passo do ciclo de modo a que o invariante se mantenha verdadeiro e, muito importante, se garanta a terminação do ciclo.  Sendo que o ciclo começa com i = 0 e termina quando i = n (e n > 0), então é óbvio que, se i aumentar de pelo menos uma unidade em cada passo, atingirá fatalmente n em tempo finito!  Assim, parte do passo corresponderá a aumentar o valor de i, sendo a escolha mais simples a simples incrementação: i++.  Mas esta é apenas uma parte do passo: é chamado o progresso (prog) do ciclo, pois garante que ele progride em direcção à falsidade da guarda.  Mas o passo tem de levar também à veracidade de CI.  Para isso, são necessárias normalmente acções que mantenham a veracidade de CI apesar do progresso.  A essa parte do passo chama-se normalmente a acção (acção) do ciclo.  Assim, normalmente passo corresponde a

  8.  
      acção
      progresso
    sendo que neste caso já se escolheu o progresso:
     
      acção
      i++;
    Colocando as asserções e manipulando-as um pouco (matematicamente):
     
      // CI e G, ou seja, resultado = xi e 0 <= i <= n e x = X e n = N e i <> n
      // ou seja, resultado = xi e 0 <= i < n e x = X e n = N
      // ou seja, resultado  x = xi x = xi+1 e 1 <= i + 1 < n + 1 e x = X e n = N
      // ou seja, resultado  x = xi x = xi+1 e 1 <= i + 1 <= n e x = X e n = N
      // o que implica, resultado  x = xi x = xi+1 e 0 <= i + 1 <= n e x = X e n = N
      // o pelo que se porna evidente que a acção a usar é
      resultado *= x; // resultado = resultado * x
      i++; // i = i + 1
      // pois conduz a resultado = xi e 0 <= i <= n e x = X e n = N
      // ou seja, conduz a CI
Concluindo, a metodologia apresentada permite desenvolver uma função e demonstrar simultaneamente o seu bom funcionamento:
// PC: n > 0 e x = X e n = N
// CO: resultado = XN
double potência(double x, int n)
{
    // PC: n > 0 e x = X e n = N
    double resultado = x;
    int i = 1;
    // CI: resultado = xi e 0 <= i <= n e x = X e n = N
    while(i != n) { // G: i <> n
        // CI e G, ou seja, resultado = xi e 0 <= i <= n e x = X e n = N e i <> n
        // ou seja, resultado = xi e 0 <= i < n e x = X e n = N
        // ou seja, resultado x = xi x = xi+1 e 1 <= i + 1 < n + 1 e x = X e n = N
        // ou seja, resultado x = xi x = xi+1 e 1 <= i + 1 <= n e x = X e n = N
        // que implica resultado x = xi x = xi+1 e 0 <= i + 1 <= n e x = X e n = N
        // o pelo que se porna evidente que a acção a usar é
        resultado *= x; // resultado = resultado * x
        i++; // i = i + 1
        // pois conduz a resultado = xi e 0 <= i <= n e x = X e n = N
        // ou seja, conduz a CI
    }
    // CI e ~ G => CO, ou seja
    // resultado = xi e 0 <= i <= n e x = X e n = N e i = n => resultado = XN
    return resultado;
}
que, sem as asserções, fica (mas note-se que se mantiveram os comentários com PC, CO e CI, pois documentam perfeitamente a função):
// PC: n > 0 e x = X e n = N
// CO: resultado = XN
double potência(double x, int n)
{
    double resultado = x;
    int i = 1;
    // CI: resultado = xi e 0 <= i <= n e x = X e n = N
    while(i != n) {
        resultado *= x;
        i++;
    }
    return resultado;
}
Este foi um exemplo simples, no qual a utilização da metologia apresentada pode parecer excessiva.  Mas a verdade é que a disciplina no desenvolvimento de programas trará vantagens muito evidentes quando a complexidade dos problemas aumentar.

Finalmente, note-se que a metologia apresentada não é de todo exaustiva: a escolha do invariante pode ser feita de outras formas.  Na realidade, por vezes tem de ser feita de outra forma.  Mas essa é matéria para Computação e Algoritmia.

* Note-se que, nas asserções, tenta-se usar a simbologia habitual da matemática, onde = significa "igual a", e não a atribuição.  Só mesmo no código C++ se usa o formato ==.  No caso da desigualdade, à falta, em HTML, do símbolo habitual, usa-se <> para o as asserções, e != apenas no código C++.

1.1.2  Variantes do ciclo while: for e do while

Uma vez que a maior parte dos ciclos usando a instrução while têm a forma
inic
while(G) {
    acção
    prog
}
o C++ disponibiliza um forma abreviada para os escrever: através da instrução for:
for(inic; G; prog)
    acção
em que inic e prog têm de ser expressões em C++ (mas inic pode definir variáveis locais).  A função potência() desenvolvida acima pode portanto ser escrita:
// PC: n > 0 e x = X e n = N
// CO: resultado = XN
double potência(double x, int n)
{
    double resultado;
    int i;
    // CI: resultado = xi e 0 <= i <= n e x = X e n = N

    for(resultado = x, i = 1; i != n; i++)
        resultado *= x;

   return resultado;
}

Um outro tipo de ciclo em C++ é o correspondente à instrução do while.  A sintaxe desta instrução é:
do
    instrução
while(expressão_booleana);
A execução da instrução do while consiste na execução repetida de instrução enquanto espressão_booleana tiver o valor verdadeiro (true).  Mas, ao contrário do que se passa no caso da instrução while, a expressão_booleana é sempre calculada e verificada depois da instrução.  Quando o resultado é falso o fluxo de execução passa para a instrução subsequente ao do while.  Note-se que instrução pode consistir em qualquer instrução do C++, nomeadamente um bloco de instruções.

Se se pretender validar uma entrada de dados por um utilizador (se o utilizador apenas devesse escrever números entre 0 e 10, por exemplo) e se se quiser obrigar à repetição da entrada de dados até o utilizador escrever o número correcto:

cout << "Introduza valor (0 a 10): ";
cin >> n;
while(n < 0 || n > 10) {
    cout << "Incorrecto, introduza de novo (0 a 10): ";
    cin >> n;
}
A instrução cin >> n aparece duas vezes, o que não parece uma solução muito elegante.  Isto deve-se a que, antes de entrar no ciclo para a primeira iteração, tem de fazer sentido verificar se n está ou não dentro dos limites impostos.  A alternativa usando o ciclo do while seria:
cout << "Introduza valor (0 a 10): ";
do {
    cin >> n;
    if(n < 0 || n > 10)
        cout << "Incorrecto, introduza de novo (0 a 10): ";
} while(n < 0 || n > 10);
Este ciclo executa o corpo pelo menos uma vez dado que a guarda só é avaliada depois da primeira execução.  Assim, só é necessária uma instrução cin >> n.  A contrapartida é a necessidade da instrução alternativa if dentro do ciclo...

Em geral os ciclos while e for são suficientes para resolver os problemas, sendo muito raras as utilizações onde a utilização de do while resulta em código realmente mais claro.

1.1.3  Exemplo

#include <iostream>
using namespace std;
// Escreve um triângulo oco de asteriscos com a altura passada
// como argumento:
void escreveTriângulo(int altura)
{
    for(int i = 0; i < altura ; i++) {
        for(int j = 0; j < altura ; j++)
            if (j == 0 || j == i || i == altura - 1)
                cout << '*';
            else
                cout << ' ';
        cout << endl;
    }
}

int main()
{
    cout << "Introduza a altura do triângulo: ";
    int a;
    cin >> a;

    escreveTriângulo(a);
}

2  Exercícios

Escreva programas para testar todas as funções e procedimentos desenvolvidos.

1.a)  Desenvolva uma função que calcule, duma forma iterativa, o factorial de um inteiro passado como argumento.  Compare com a versão recursiva desenvolvida em aulas anteriores.

1.b)  Desenvolva um procedimento que, dados dois inteiros como argumento, um o dividendo e outro o divisor, sendo o dividendo não negativo e o divisor positivo, calcule o quociente e o resto da divisão do dividendo pelo divisor e os guarde em variáveis externas à função (use passagem de argumentos por referência).  Nota: não pode usar os operadores *, / e %.  Qual será o invariante?

2.a)  Escreva uma função devolva true se o valor do argumento inteiro for primo e false no caso contrário.  Recorda-se que um número primo é todo aquele que apenas é divisível por 1 e por si mesmo.

2.b)  Desenvolva um programa que escreva no ecrã todos os números primos entre 2 e n (inclusivé), sendo n dado pelo utilizador.

2.c)  Sabendo que todos os números primos maiores que 3 se podem escrever na forma 6m-1 ou 6m+1 (mas nem todos os números desta forma são primos!), torne o programa da alínea anterior mais eficiente.

3.a)  Faça um procedimento que imprima no ecrã um quadrado com a altura indicada como argumento e com o interior vazio.  Crie um pequeno programa para testar o procedimento.  Exemplo:

Introduza a altura do quadrado: 4
****
*  *
*  *
****
3.b)  Faça um procedimento que imprima no ecrã um quadrado com a altura indicada como argumento e com as diagonais assinaladas. Crie um pequeno programa para testar o procedimento.  Exemplo:
Introduza a altura do quadrado: 8
********
**    **
* *  * *
*  **  *
*  **  *
* *  * *
**    **
********
4.  Faça uma função que permita ao utilizador inserir 20 caracteres e devolva quantos dos caracteres introduzidos pelo utilizador são asteriscos.  Crie um pequeno programa para testar esta função.  Exemplo:
Introduza um conjunto de caracteres: qwe*98*8wer*we112rwq
Contei 3 asteriscos.

3  Exercícios propostos

1.a)  Desenvolva uma função que calcule, duma forma iterative, o valor da sequência de Fibonacci num determinado mês, passado como argumento.  Que condição invariante deverá usar?

1.b)  Escreva uma função que calcule a multiplicação inteira de dois números recorrendo apenas aos operadores aritméticos + e -.

2.  Suponha que as letras A, B, C e D foram codificadas em zeros e uns da seguinte forma: A = 0, B = 11, C = 100 e D = 101.  Crie um programa que, quando é introduzida uma sequência de zeros e uns apartir do teclado, descodifique as várias letras.  Atenção: pode haver sequências que não terminam com a codificação total de um caracter (por exemplo 01 = A?).  O programa deve estar preparado para lidar com esse tipo de erros do utilizador.  Por exemplo:

Introduza um conjunto de zeros e uns: 10010011001101
CCBAABA?
Utilize uma função para descodificar cada caractere.  A leitura deve terminar quando for introduzido um caractere que não seja '0' nem '1'.

3.  Crie uma função que leia uma sequência de números inteiros e devolva um valor booleano que indica se este conjunto de números está por ordem crescente (no sentido lato).  A sequência termina quando o utilizador insere o número 0 (zero).  Como é evidente este número serve apenas como marcador do fim da sequência e não é tido em conta para o resultado da função.  Pode assumir que a sequência tem sempre dois ou mais números.  Crie um pequeno programa para testar esta função.

4.  Crie uma função que leia uma sequência de números inteiros e devolva um valor inteiro que será 2 caso a sequência esteja ordenada por ordem crescente (em sentido lato), 1 caso esteja por ordem decrescente (em sentido lato), 0 (zero) caso seja constante e -1 caso não seja monótona.  A sequência termina quando o utilizador insere o número 0 (zero).  Como é evidente este número serve apenas como marcador do fim da sequência e não é tido em conta para o resultado da função.  Crie um pequeno programa para testar esta função.

4  Referências

[1] Aurélio Buarque de Holanda Ferreira, "Novo Dicionário da Língua Portuguesa", 2ª edição, Editora Nova Fronteira, S.A., Rio de Janeiro, 1986.

[2] Yedidyah Langsam, Moshe J. Augenstein e Aaron M. Tenenbaum, "Data Structures Using C and C++", segunda edição, Prentice Hall, Upper Saddle River, New Jersey, 1996.

[3] Donald E. Knuth, "Fundamental algorithms", volume 1 de "The Art of Computer Programming", segunda edição, segunda edição, Addison-Wesley Publishing Company, Reading, Massachusetts, 1973.

[4]  David Gries, "The Science of Programming", volume da série "Texts and Monographs in Computer Science", editada por David Gries, Springer-Verlag, Nova Iorque, 1981 [1989]. *

* Existe na biblioteca do ISCTE.