4  Controlo do fluxo dos programas

2º andar direito
(...)
Se temos...! Diz ela
      mas o problema não é só de aprender
      é saber a partir daí que fazer
(...)
Sérgio Godinho,  Pano Cru


Quase todas as resoluções de problemas envolvem tomadas de decisões e repetições.  Dificilmente se consegue encontrar um algoritmo interessante que não envolva, quando lido em português, as palavras 'se' e 'enquanto', correspondendo aos conceitos de selecção ou alternativa e de iteração ou repetição.  Neste capítulo estudar-se-ão os mecanismos que a linguagem C++ fornece para suportar esses conceitos e discutir-se-ão metodologias de resolução de problemas usando esses mecanismos.

4.1  Instruções de selecção em C++

A resolução de um problema implica quase sempre a tomada de decisões ou pelo menos a selecção de alternativas.

Suponha-se que se pretendia desenvolver uma função para calcular o valor absoluto de um inteiro.  O "esqueleto" da função pode ser o que se segue

/*
   Esta função devolve o valor absoluto do argumento.
   PC: (sem restrições)
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    ...
}
Este esqueleto inclui, como habitualmente, os comentário iniciais que clarificam o que a função faz, o cabeçalho que indica como a função se utiliza, e um corpo, onde se colocarão as instruções que resolvem o problema, i.e., que explicitam como a função funciona.

A resolução deste problema requer que sejam tomadas acções diferentes consoante o valor de x seja positivo ou negativo (ou nulo).  São portanto fundamentais as chamadas instruções de selecção ou alternativa.

4.1.1  As instruções if e if else

As instruções if e if else são das mais importantes instruções de controlo do fluxo de um programa, i.e., instruções que alteram a sequência normal de execução das instruções de um programa.  Estas instruções permitem executar uma instrução caso uma condição seja verdadeira e, no caso da instrução ifelse, uma outra instrução caso a condição seja falsa.  Por exemplo, no troço de programa
if(x < 0)  // 1
    x = 0; // 2
else       // 3
    x = 1; // 4
...        // 5
as linhas 1 a 4 correspondem a uma única instrução de selecção ou alternativa.. Esta instrução de selecção é composta de uma condição (na linha 1) e duas instruções controladas (linhas 2 e 4).  Se x for menor do que zero quando a instrução if é executada, então a próxima instrução a executar é a instrução na linha 2, passando o valor de x a ser zero.  No caso contrário, se x for maior ou igual a zero, a próxima instrução a executar é a instrução na linha 4, passando a variável x a ter o valor 1.  Em qualquer dos casos, a execução continua na linha 5 *.

Como a instrução na linha 2 só é executada se x < 0, diz-se que x < 0 é a sua guarda, normalmente representada por G.  De igual modo, a guarda da instrução na linha 4 é x >= 0.  A guarda da instrução controlada após o else está implícita, podendo ser obtida por negação da guarda do if: ¬(x < 0) é equivalente a x >= 0.  Assim, numa instrução de selecção pode-se sempre inverter a ordem das instruções controladas desde que se negue a condição.  Por exemplo,

if(x < 0)
    // G1: x < 0
    x = 0;
else
    // G2: x >= 0
    x = 1;
é equivalente a
if(x >= 0)
    // G2: x >= 0
    x = 1;
else
    // G1: x < 0
    x = 0;
Em certos casos pretende-se apenas executar uma dada instrução se determinada condição se verificar, não se desejando fazer nada caso a condição seja falsa.  Nestes casos pode-se omitir o else e a instrução que se lhe segue.  Por exemplo, no troço de programa
if(x < 0)  // 1
    x = 0; // 2
...        // 3
se x for menor do que zero, então é executada a instrução controlada na linha 2, passando o valor de x a ser zero, sendo em seguida executada a instrução na linha 3 *.   No caso contrário a execução passa directamente para a linha 3.

A este tipo restrito de instrução de selecção também se chama instrução condicional.  Uma instrução condicional é sempre equivalente a uma instrução de selecção em que a instrução após o else é uma instrução nula (a instrução nula corresponde em C++ a um simples terminador ;).  Ou seja, o exemplo anterior é equivalente a:

if(x < 0)  // 1
    x = 0; // 2a
else       // 2b
    ;      // 2c
...        // 3
Em muitos casos é necessário executar condicionalmente não uma instrução simples mas sim uma sequência de instruções.  Para o conseguir, agrupam-se essas instruções num bloco de instruções ou instrução composta, colocando-as entre chavetas {}.  Por exemplo, o código que se segue ordena os valores guardados nas variáveis x e y de tal modo que x termine com um valor menor ou igual ao de y:
int x, y;
...
if(x > y) {
    int t = x;
    x = y;
    y = t;
}
* Se existirem instruções return, break, continue ou goto nas instruções controladas pelo if, o fluxo normal de execução pode ser alterado de tal modo que a execução não continua na instrução imediatamente após o if.

Instruções alternativas encadeadas

Caso seja necessário, podem-se encadear instruções de selecção umas nas outras.  Isso acontece quando se pretende seleccionar uma entre mais do que duas instruções controladas.  Por exemplo, para verificar qual a posição do valor de uma variável a relativamente a um intervalo [minimo, maximo]  (todos introduzidos pelo utilizador do programa), pode-se usar
int a, minimo, maximo;
cin >> minimo >> maximo >> a;

if(a < minimo)
    cout << a << " menor que mínimo." << endl;
else {
    if(a <= maximo)
        cout << a << " entre mínimo e máximo." << endl;
    else
        cout << a << " maior que máximo." << endl;
}

Note-se que, sendo as instruções de selecção instruções por si só, o teste acima se pode escrever sem recurso às chavetas, ou seja,
if(a < minimo)
    cout << a << " menor que mínimo." << endl;
else
    if(a <= maximo)
        cout << a << " entre mínimo e máximo." << endl;
    else
        cout << a << " maior que máximo." << endl;
Em casos como este, em que se encadeiam if else sucessivos, é comum usar uma indentação que deixa mais claro que existem mais do que duas alternativas,
if(a < minimo)
    cout << a << " menor que mínimo." << endl;
else if(a <= maximo)
    cout << a << " entre mínimo e máximo." << endl;
else
    cout << a << " maior que máximo." << endl;

Guardas em instruções alternativas encadeadas

Podem-se colocar como comentários do exemplo anterior as guardas de cada instrução controlada, que reflectem as circunstâncias nas quais elas são executadas
if(a < minimo)
    // G1: a < mínimo
    cout << a << " menor que mínimo." << endl;
else if(a <= maximo)
    // G2: mínimo <= a e a <= máximo
    cout << a << " entre mínimo e máximo." << endl;
else
    // G3: mínimo <= a e máximo < a
    cout << a << " maior que máximo." << endl;
Fica claro que as guardas não correspondem directamente à condição indicada no if respectivo, com excepção da primeira guarda.

As n guardas de uma instrução de selecção, construída com n - 1 instruções if encadeadas, podem ser obtidas a partir das condições de cada um dos if como se segue:

if(C1)
    // G1: C1
    ...
else if(C2)
    // G2: ¬G1 e C2 (ou seja, ¬C1eC2)
    ...
else if(C3)
    // G3: ¬G1 e ¬G2 e C3 (ou seja, ¬C1 e ¬C2eC1)
    ...
...
else if(Cn-1)
    // Gn-1: ¬G1 e ... e ¬Gn-2eCn-1 (ou seja, ¬C1e ... e ¬Cn-2eCn-1)
    ...
else
    // Gn: ¬G1 e ... e ¬Gn-2e ¬Gn-1 (ou seja, ¬C1e ... e ¬Cn-1)
    ...
Ou seja, as guardas das instruções controladas são obtidas por conjunção da condição do if respectivo com a negação das guardas das instruções controladas (ou das condições de todos os if) anteriores na sequência, com seria de esperar.

Influência de pré-condições

Note-se que a última guarda do exemplo dado anteriormente parece ser redundante: se a é maior que máximo não é forçosamente maior que mínimo?  Então porque não é a guarda simplesmente máximo < a?  Acontece que nada garante que mínimo <= máximo!  Se se introduzir essa condição como pré-condição das instruções de selecção encadeadas, então essa condição verifica-se forçosamente em cada uma das instruções controladas, pelo que as guardas podem ser simplificadas e tomar a sua forma mais intuitiva:
// PC: mínimo < máximo
if(a < minimo)
    // G1: a < mínimo
    cout << a << " menor que mínimo." << endl;
else if(a <= maximo)
    // G2: mínimo <= a e a <= máximo
    cout << a << " entre mínimo e máximo." << endl;
else
    // G3: máximo < a
    cout << a << " maior que máximo." << endl;

4.1.2  Problemas comuns

A sintaxe das instruções if e if else do C++ presta-se a ambiguidades.  No código
if(m == 0)
    if(n == 0)
        cout << "m e n são zero." << endl;
else
    cout << "m não é zero." << endl;
o else não diz respeito ao primeiro if.  Ao contrário do que a indentação do código sugere, o else diz respeito ao segundo if.  Em caso de dúvida, um else pertence ao if mais próximo (e acima...) dentro mesmo bloco de instruções e que não tenha já o respectivo else.  Para corrigir o exemplo anterior é necessário construir uma instrução composta, que neste caso consiste de uma única instrução de selecção
if(m == 0) {
    if(n == 0)
        cout << "m e n são zero." << endl;
} else
    cout << "m não é zero." << endl;
Para evitar erros deste tipo, é conveniente usar blocos de instruções de uma forma liberal, pois construções como a que se apresentou podem dar origem a erros muito difíceis de detectar e corrigir.  Os compiladores de boa qualidade, no entanto, avisam o programador da presença de semelhantes (aparentes) ambiguidades.

Um outro erro frequente corresponde a colocar um ; logo após a condição do if ou logo após o else.  Por exemplo, a intenção do programador do troço de código

if(x < 0);
    x = 0;
era provavelmente que se mantivesse o valor de x excepto quando este fosse negativo.  Mas a interpretação feita pelo compilador (e a correcta dada a sintaxe da linguagem C++) é
if(x < 0)
    ; // instrução nula: não faz nada.
x = 0;
ou seja, x terminará sempre com o valor zero!  Este tipo de erro, não sendo muito comum, é ainda mais difícil de detectar do que o da (suposta) ambiguidade da pertença de um else: os olhos do programador, habituados que estão à presença de ; no final de cada linha, recusam-se a detectar o erro.

4.1.3  Desenvolvimento de instruções alternativas

Asserções

Chama-se asserção a um predicado (ver Secção A.1) escrito normalmente na forma de comentário antes ou depois de uma instrução de um programa.  As asserções correspondem a afirmações acerca das variáveis do programa que se sabe serem verdadeiras antes da instrução seguinte à asserção (e depois da instrução anterior à asserção).  Uma asserção pode sempre ser vista como pré-condição (PC) da instrução seguinte e condição objectivo (CO) da instrução anterior.  As asserções podem também incluir afirmações acerca de variáveis matemáticas, que não pertencem ao programa.

Note-se que nas asserções cada variável pertence a um determinado conjunto.  Para as variáveis C++, esse conjunto é determinado pelo tipo da variável indicado na sua definição (e.g., int x; significa que x pertence ao conjunto dos inteiros entre -2n-1 e 2n-1 - 1, se os int tiverem n bits).  Para as variáveis matemáticas, esse conjunto deveria, em rigor, ser indicado explicitamente.  Neste texto, no entanto, admite-se que as variáveis matemáticas pertencem ao conjunto dos inteiros, salvo onde for explicitamente indicado outro conjunto ou onde o conjunto seja fácil de inferir pelo contexto da asserção.  Finalmente, nas asserções é normal assumir que as variável C++ não têm limitações (e.g., admite-se que uma variável int pode guardar qualquer inteiro).  Embora isso não seja verdade, permite resolver um grande número de problemas sem que se introduzam demasiados detalhes nas demonstrações.

Em cada ponto de um programa existe um determinado conjunto de variáveis, cada uma das quais pode tomar determinados valores, consoante o seu tipo.  Ao conjunto de todos os possíveis valores de todas as variáveis existentes num dado ponto de um programa chama-se o espaço de estados nesse ponto.  Ao conjunto dos valores das variáveis existentes num determinado ponto do programa num determinado instante de tempo chama-se o estado de um programa.  Assim, o estado de um programa é um elemento do espaço de estados.

Dedução de asserções
Suponha-se o seguinte troço de programa
++n;
onde se admite que a variável n tem inicialmente um valor não-negativo.  Como adicionar asserções a este troço de programa?  Em primeiro lugar escreve-se a asserção que corresponde à assunção de que n guarda um valor não-negativo:
// n >= 0
++n;
A asserção que segue uma instrução é tipicamente obtida pela semântica da instrução e pela respectiva pré-condição.  Ou seja, dada a PC, a instrução implica a veracidade da respectiva CO.  No caso acima é óbvio que
// n >= 0
++n;
// n >= 1
ou seja, se n era maior ou igual a zero antes da incrementação, depois da incrementação será forçosamente maior ou igual a um.

A demonstração informal da correcção de um pedaço de código pode, portanto, ser feita recorrendo a asserções.  Suponha-se que se pretende demonstrar que o código

int t = x;
x = y;
y = t;
troca os valores de duas variáveis x e y do tipo int.  Começa-se por escrever as duas principais asserções: a pré-condição e a condição objectivo da sequência completa de instruções.  Neste caso a escrita destas asserções é complicada pelo facto de a CO ter de ser referir aos valores das variáveis antes das instruções.  Para resolver este problema, considere-se que as variáveis matemáticas x e y representam os valores iniciais das variáveis C++ x e y (note-se bem a diferença de tipo de letra usada para variáveis matemáticas e para variáveis do programa C++).  Então a CO pode ser escrita simplesmente como
// x = y e y = x
e a PC como
// x = x e y = y
donde o código com as asserções iniciais e finais é
// x = x e y = y
int t = x;
x = y;
y = t;
// x = y e y = x
A demonstração de correção pode ser feita deduzindo as asserções intemédias:
// x = x e y = y
int t = x;
// x = x e y = yet = x
x = y;
// x = y e y = yet = x
y = t;
// x = y e y = x (e t = x)
A demonstração de correcção pode ser feita também partindo dos objectivos.  Para cada instrução, partindo da última, determina-se quais as condições mínimas a impor às variáveis antes da instrução, i.e., determina-se qual a PC mais fraca a impor à instrução em causa, de modo a que, depois dessa instrução, a CO da instrução seja verdadeira.
Predicados mais fortes e mais fracos
Diz-se que um predicado P é mais fraco do que outro Q se Q implicar P, ou seja, se o conjunto dos valores que tornam o predicado P verdadeiro contém o conjunto dos valores que tornam o predicado Q verdadeiro.  Por exemplo, se P: x > 0 e Q: x = 10, então P é mais fraco do que Q, pois {10} está contido no conjunto dos positivos.  O mais fraco de todos os possíveis predicados é aquele que é sempre verdadeiro, pois o conjunto dos valores que o verificam é o conjunto universal.  Logo, o mais fraco de todos os possíveis predicados é V.  Por razões óbvias, o mais forte de todos os possíveis predicados é F, sendo vazio o conjunto dos valores que o verificam.
Dedução da pré-condição mais fraca de uma atribuição
Dada uma instrução de atribuição do valor de uma dada expressão a uma variável x, e dada uma CO que se deve verificar depois da atribuição
// PC = ???
x = expressão;
// CO
a PC mais fraca que, depois da atribuição, conduz à CO, pode ser obtida substituindo todas as ocorrências da variável x na CO por expressão.  Esta dedução da PC só pode ser feita se a expressão cujo valor se atribui a x não tiver efeitos laterais, i.e., se não implicar a alteração de nenhuma variável do programa (ver Ordem de cálculo e efeitos laterais).

Por exemplo, suponha-se que se pretendia saber qual a PC mais fraca para a qual a instrução de atribuição x = -x; conduz à CO x >= 0.  Aplicando o método descrito a

// PC = ???
x = -x;
// CO: x >= 0
obtém-se
// PC: -x >= 0, ou seja, x <= 0
x = -x;
// CO: x >= 0
Ou seja, para que a inversão do sinal de x conduza a um valor não-negativo, o menos que tem de se exigir é que o valor de x seja inicialmente não-positivo.

Voltando ao exemplo da troca de valores de duas variáveis

int t = x;
x = y;
y = t;
// x = y e y = x
e aplicando a técnica proposta obtém-se sucessivamente
int t = x;
x = y;
// x = y e t = x
y = t;
// x = y e y = x
int t = x;
// y = y e t = x
x = y;
// x = y e t = x
y = t;
// x = y e y = x
// y = y e x = x
int t = x;
// y = y e t = x
x = y;
// x = y e t = x
y = t;
// x = y e y = x
tendo-se recuperado a PC inicial.

Em geral este método não conduz exactamente à PC escrita inicialmente.  Suponha-se que se pretendia demonstrar que

// PC: x < 0
x = -x;
// x >= 0
Usando o método anterior conclui-se que:
// PC: x < 0
// -x >= 0 ou seja, x <= 0
x = -x;
// x >= 0
Mas como x < 0 implica que x <= 0, conclui-se que o código está correcto.

Asserções em instruções de selecção

Suponha-se de novo o troço de código que pretende ordenar os valores das variáveis x e y (que se presume serem do tipo int) de modo que x <= y,
if(x > y) {
    int t = x;
    x = y;
    y = t;
}
Qual a CO e qual a PC?  Considerem-se x e y os valores de x e y antes da instrução de selecção.  Então a PC e a CO podem ser escritas
// PC: x = x ey = y
if(x > y) {
    int t = x;
    x = y;
    y = t;
}
// CO: x <= y e ((x = x e y = y) ou (x = y e y = x))
Ou seja, o problema fica resolvido quando as variáveis x e y mantêm ou trocam os seus valores iniciais e x termina com um valor não superior ao de y.  Note-se que determinar uma PC não é fácil.  A PC e a CO, em conjunto, constituem a especificação formal do problema.  A sua escrita obriga à compreensão profunda do problema a resolver, e daí a dificuldade.

Será que a instrução condicional conduz forçosamente da PC à CO?  É necessário demonstrá-lo.

É conveniente começar por converter a instrução condicional na instrução de selecção equivalente e, simultaneamente, explicitar as guardas das instruções controladas:

// PC: x = xey = y
if(x > y) {
    // G1: x > y
    int t = x;
    x = y;
    y = t;
} else
    // G2: x <= y
    ; // instrução nula!
// ??
A PC, sendo verdadeira antes da instrução alternativa, também o será imediatamente antes de qualquer das instruções controladas, pelo que se pode escrever
// PC: x = xey = y
if(x > y) {
    // x > y ex = x e y = y
    int t = x;
    x = y;
    y = t;
} else
    // x <= y ex = x e y = y
    ;
// ??
Pode-se agora deduzir as asserções válidas após cada instrução controlada:
// PC: x = xey = y
if(x > y) {
    // x > y ex = x e y = y
    int t = x;
    // t > y ex = x e y = y e t = x
    x = y;
    // t > x ex = y e y = y et = x
    y = t;
    // y > x ex = y e y = x (e t = x)
} else
    // x <= y ex = x e y = y
    ;
    // x <= y ex = x e y = y, a instrução nula não afecta as asserções...
// ??
Conclui-se que, depois da troca de valores entre x e y nas instruções controladas pelo if, y > x, ou seja, x < y.  Mas x < y implica que x <= y.  Logo pode-se deduzir que
// y > x e x = y e y = x
// x <= y e x = y e y = x
onde a escrita de duas asserções uma após a outra significa que a segunda pode ser deduzida da primeira (ou que a segunda é implicada pela primeira).  Usando este facto e eliminando as asserções intermédias, úteis apenas durante a demonstração,
// PC: x = xey = y
if(x > y) {
    int t = x;
    x = y;
    y = t;
    // x <= y e x = y e y = x
} else
    ;
    // x <= y ex = x e y = y
// ??
Falta agora deduzir a asserção válida depois da instrução de selecção.  Esse ponto (assinalado com // ??) pode ser atingido depois de se ter passado por qualquer uma das instruções alternativas, pelo que a asserção válida é a disjunção das asserções deduzidas para cada uma das instruções alternativas:
// PC: x = xe y = y
if(x > y) {
    int t = x;
    x = y;
    y = t;
    // x <= y ex = y e y = x
} else
    ;
    // x <= y ex = x e y = y
// (x <= y e x = y e y = x) ou (x <= yex = x e y = y)
// ou seja,
// x <= y e ((x = y e y = x) ou (x = xey = y))
que é exactamente a CO que se pretendia demonstrar válida.

Mais uma vez seria possível partir dos objectivos durante a demonstração.  Neste caso começa por se observar que a CO tem de ser válida no final de qualquer das instruções controladas pela instrução de selecção:

// PC: x = xey = y
if(x > y) {
    // G1: x > y
    int t = x;
    x = y;
    y = t;
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
} else
    // G2: x <= y
    ;
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
// CO: x <= y e ((x = y e y = x) ou (x = x e y = y))
Depois vão-se determinando sucessivamente as PC mais fracas de cada instrução (do fim para o início) usando as regras descritas acima para as atribuições:
// PC: x = xey = y
if(x > y) {
    // G1: x > y
    // y <= x e ((y = y e x = x) ou (y = x e x = y))
    int t = x;
    // y <= t e ((y = y e t = x) ou (y = x e t = y))
    x = y;
    // x <= t e ((x = y e t = x) ou (x = x e t = y))
    y = t;
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
} else
    // G2: x <= y
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
    ;
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
// CO: x <= y e ((x = y e y = x) ou (x = x e y = y))
Eliminando as asserções intermédias obtém-se:
// PC: x = xey = y
if(x > y) {
    // G1: x > y
    // y <= x e ((y = y e x = x) ou (y = x e x = y))
    int t = x;
    x = y;
    y = t;
} else
    // G2: x <= y
    // x <= y e ((x = y e y = x) ou (x = x e y = y))
    ;
// CO: x <= y e ((x = y e y = x) ou (x = x e y = y))
Basta agora verificar se a PC em conjunção com cada uma das guardas implica a asserção respectiva deduzida.  Neste caso basta verificar que:
  1. PC e G1 implica y <= xe ((y = y e x = x) ou (y = x e x = y)) e que
  2. PC e G2 implica x <= ye ((y = x e x = y) ou (y = y e x = x))),
o que é claramente verdade.

Em geral, portanto, para demonstrar a correcção de uma instrução de selecção (com n alternativas, ou seja, com n instruções controladas)

// PC
if(C1)
    // G1: C1
    instrução1
else if(C2)
    // G2: ¬G1e C2
    instrução2
else if(C3)
    // G3: ¬G1 e ¬G2 e C3
    instrução3
...
else if(Cn-1)
    // Gn-1: ¬G1 e ... e ¬Gn-2eCn-1
    instruçãon-1
else
    // Gn: ¬G1 e ... e ¬Gn-1
    instruçãon
// CO
seguem-se os seguintes passos: Note-se que não é necessário verificar se pelo menos uma guarda é sempre verdadeira, porque, por construção da instrução de selecção, esta termina sempre com um else.  Se isso não acontecer, é necessário fazer a demonstração para a instrução de selecção equivalente em que todos os if têm o respectivo else, o que pode implicar introduzir uma instrução controlada nula.

Desenvolvimento

As secções anteriores apresentaram algum formalismo relativo à construção de instruções de selecção.  Falta agora ver como esse formalismo pode ser usado para desenvolver programas.

Regresse-se ao problema inicial, da escrita de uma função para calcular o valor absoluto de um valor inteiro.  O objectivo é, portanto, preencher o corpo da função com "esqueleto"

/*
   Esta função devolve o valor absoluto do argumento.
   PC: V (sem restrições)
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    ...
}
onde se usou o predicado V (verdadeiro) para indicar que a função não tem pré-condições.

Para simplificar o desenvolvimento, pode-se começar o corpo pela definição de uma variável local para guardar o resultado e terminar com a devolução do seu valor, o que permite escrever as asserções principais da função em termos do valor desta variável *:

/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    int r;
    // PC': V
    ...
    // CO': r >= 0 e (r = -x ou r = x)
    return r;
}
Antes de começar o desenvolvimento, é necessário perceber se para a resolução do problema é necessário recorrer a uma instrução de selecção.  Neste caso é óbvio que sim, pois tem de se discriminar entre valores negativos e positivos (e talvez nulos) de x.

O desenvolvimento usado será baseado nos objectivos.  Este é um princípio importante da programação [4]: a programação deve ser orientada pelos objectivos.  É certo que a pré-condição afecta a solução de qualquer problema, mas os problemas são essencialmente determinados pela condição objectivo.  De resto, com se pode verificar depois de alguma prática, mais inspiração para a resolução de um problema pode ser obtida por análise da condição objectivo do que por análise da pré-condição.  Por exemplo, é comum  não haver qualquer pré-condição na especificação de um problema, donde nesses casos só a condição objectivo poderá ser usada como fonte de inspiração.

* Note-se que a semântica de uma instrução de retorno é muito semelhante à de uma instrução de atribuição, nomeadamente a PC mais fraca pode ser obtida por substituição, na CO da função,do nome da função pela expressão usada na instrução de retorno.  Assim, a PC mais fraca da instrução return r; que leva a

CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -xouabsoluto = x)
é
CO: r = |x|, ou seja, r >= 0 e (r = -x ou r = x).
No entanto, a instrução de retorno difere da instrução de atribuição numa coisa: a instrução de retorno termina a execução da função.
Primeiro passo: escolha das instruções controladas
O primeiro passo do desenvolvimento corresponde a identificar possíveis instruções controladas que pareçam poder levar à veracidade da CO'.  É fácil verificar que há duas possíveis instruções nessas condições: r = -x; e r = x;.  Estas possíveis instruções podem ser obtidas por simples observação da CO'.
Segundo passo: determinação das pré-condições mais fracas
O segundo passo corresponde a verificar em que circunstâncias estas instruções controladas levam à veracidade da CO', ou seja, quais as PCi mais fracas que garantem, depois de cada instrução, que a CO' é verdadeira.  Comece-se pela primeira instrução
// ??
r = -x;
// CO': r >= 0 e (r = -x ou r = x)
donde
// -x >= 0 e (-x = -x ou -x = x) ou seja, x <= 0 e (V ou x = 0), ou seja, x <= 0
r = -x;
// CO': r >= 0 e (r = -x ou r = x)
Logo, a instrução r = -x; só conduz aos resultados pretendidos desde que x tenha inicialmente um valor não-positivo (PC1: x <= 0).

A mesma verificação pode ser feita para a segunda instrução

// ??
r = x;
// CO': r >= 0 e (r = -x ou r = x)
donde
// x >= 0 e (x = -xoux = x) ou seja, x >= 0 e (x = 0 ou V), ou seja, x >= 0
r = x;
// CO': r >= 0 e (r = -x ou r = x)
Logo, a instrução r = x; só conduz aos resultados pretendidos desde que x tenha inicialmente um valor não-negativo  (PC2: x >= 0).

Note-se que, até agora, se determinou o seguinte corpo para a função:

/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    int r;
    // PC': V
    if(C1)
        // G1: ??
        // PC1: x <= 0
        r = -x;
    else
        // G2: ??
        // PC2: x >= 0
        r = x;
    // CO': r >= 0 e (r = -x ou r = x)
    return r;
}
Terceiro passo: determinação das guardas
O terceiro passo corresponde a determinar as guardas Gi de cada uma das instruções controladas.  De acordo com o que se viu anteriormente, para que a instrução de selecção resolva o problema, é necessário que PC' eGi implique PCi.   Só assim se garante que, sendo a guarda Gi verdadeira, a instrução controlada PCi conduz à CO desejada.  Neste caso PC' é sempre V, pelo que dizer que PC'eGiimplicaPCi é o mesmo que dizer que GiimplicaPCi, e portanto a forma mais simples de escolher as guardas é fazer simplesmente Gi = PCi.  Ou seja,
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    int r;
    // PC': V
    if(C1)
        // G1: x <= 0
        r = -x;
    else
        // G2: x >= 0
        r = x;
    // CO': r >= 0 e (r = -x ou r = x)
    return r;
}
Quarto passo: verificação das guardas
O quarto passo corresponde a verificar se a pré-condição PC' da instrução de selecção implica a veracidade de pelo menos uma das guardas Gi das instruções controladas.  Se isso não acontecer, significa que há valores de x para os quais nenhuma guarda é verdadeira, e portanto o problema ainda não está resolvido, sendo necessário determinar instruções controladas adicionais e respectivas guardas até todos os possíveis valores de x estarem cobertos.

Neste caso a PC' não impõe qualquer restrição, ou seja PC': V.  Logo, tem de se verificar se V implicaG1ouG2.  Neste caso G1 ouG2 é x <= 0 ou x <= 0, ou seja, V.  Como V implica V, o problema está quase resolvido.

Quinto passo: escrita das instruções de selecção (escolha das condições)
No quinto e último passo determinam-se as condições das instruções de selecção encadeadas de modo a obter as guardas entretanto determinadas.  De acordo com o que se viu anteriormente G1 = C1, pelo que a função fica
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    int r;
    // PC': V
    if(x <= 0)
        // G1: x <= 0
        r = -x;
    else
        // G2: x > 0
        r = x;
    // CO': r >= 0 e (r = -x ou r = x)
    return r;
}
Note-se que a segunda guarda foi alterada, pois a instrução após o else tem guarda G2 = ¬G1 = x > 0.  Esta guarda é mais forte que a guarda originalmente determinada, pelo que a alteração não traz qualquer problema.  Na realidade o que aconteceu foi que a semântica da instrução if do C++ forçou à escolha de qual das instruções controladas lida com o caso x = 0.
Alterando a solução
Voltando à especificação da função desenvolvida,
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
}
será ela completa?  Observe-se a seguinte "solução":
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(int x)
{
    x = 1;
    return 1;
}
Esta função cumpre a CO, embora claramente não resolva o problema satisfatoriamente.  Que se passou?  O problema está na especificação, que não proíbe mudanças de valor da variável x.  É possível deixar clara essa proibição indicando claramente que x não é uma variável mas sim uma constante, o que invalida a "solução" proposta.  Voltando à solução inicial e explicitando a impossibilidade de alterar o valor de x, a função é portanto:
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(const int x)
{
    int r;
    if(x <= 0)
        r = -x;
    else
        r = x;
    return r;
}
Esta solução pode ser simplificada se se observar que, depois de terminada a instrução de selecção, a função se limita a devolver o valor guardado em r por uma das duas instruções controladas.  Assim, é possível eliminar essa variável e devolver imediatamente o valor apropriado:
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(const int x)
{
    if(x <= 0)
        return -x;
    else
        return x;
}
Por outro lado, se a instrução controlada pelo if termina com uma instrução return, então não é necessária uma instrução de selecção, bastando uma instrução condicional:
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(const int x)
{
    if(x <= 0)
        return -x;
    return x;
}
Este último formato não é forçamente mais claro ou preferível ao anterior, mas é muito comum encontrá-lo em programas escritos em C++.
O método
Existem (pelo menos) dois métodos semi-formais para o desenvolvimento de instruções de selecção.  O primeiro foi usado para o desenvolvimento na secção anterior, e parte dos objectivos:
  1. Determina-se n instruções instruçãoi (com i de 1 a n) que pareçam poder levar à veracidade da CO.  Tipicamente estas instruções são obtidas por análise da CO.
  2. Determina-se as pré-condições mais fracas PCi de cada uma dessas instruções de modo que conduzam à CO.
  3. Determina-se uma guarda Gi para cada alternativa i de modo que PC e Gi implique PCi.  Se o problema não tiver pré-condições (ou seja, PC: V), então pode-se fazer Gi = PCi.  Note-se que uma guarda Gi = F resolve sempre o problema, embora seja tão forte que nunca leve à execução da instrução respectiva...  Por isso deve-se sempre escolher a guarda o mais fraca possível.
  4. Verifica-se se, em todas as circunstâncias, pelo menos uma das guardas Gi encontradas é verdadeira.  Isto é, verifica-se se PCimplicaG1ou ... ou Gn.  Se isso não acontecer, é necessário acrescentar mais instruções controladas às encontradas no ponto 1.  Para o fazer, identifica-se que casos ficaram por cobrir analisando as guardas já encontradas e a PC.
No segundo método tenta-se primeiro determinar as guardas e só depois se desenvolve as respectivas instruções controladas: Em qualquer dos casos, depois de encontradas as guardas e as respectivas instruções controladas, é necessário escrever a instrução de selecção com o número de instruções if apropriado (para n instruções controladas alternativas são necessárias n - 1 instruções if encadeadas) e escolher as condições Ci apropriadas de modo a obter as guardas pretendidas.  Pode-se começar por fazer Ci = Gi (para i entre 1 e n - 1) e depois identificar sobreposições e simplificar as condições Ci.  Note-se que muitas vezes as guardas encontrada contêm sobreposições (e.g., x >= 0 e x <= 0 sobrepõem-se no valor 0), que podem ser eliminadas ao escolher as condições de cada if.
Discussão
As vantagens das metodologias informais apresentadas face a abordagens mais ou menos ad-hoc do desenvolvimento são pelo menos:
  1. Forçam à especificação rigorosa do problema, e portanto à sua compreensão profunda.
  2. O desenvolvimento é acompanhado da demonstração de correcção, o que reduz consideravelmente a probabilidade de erros.
  3. Não são descurados aparentes pormenores que, na realidade, podem dar origem a erros graves e difíceis de corrigir.
É claro que a experiência do programador e a sua maior ou menor inventiva muitas vezes levem a desenvovimentos "ao sabor da pena".  Não é forçosamente errado, desde que feito em consciência.  Recomenda-se, nesses casos, que se tente fazer a posteriori uma demonstração de correcção (e não um teste!) para garantir que a inspiração funcionou...

Para se verificar na prática a importância da especificação cuidada do problema, tente-se resolver o seguinte problema:

Escreva uma função que, dados dois argumentos do tipo int, devolva o booleano verdadeiro se o primeiro for múltiplo do segundo e falso no caso contrário.
Neste ponto o leitor deve parar de ler e tentar resolver o problema.

....................................

A abordagem típica do programador é considerar que um número n é múltiplo de outro m se o resto da divisão de n por m for zero e passar ao código:

bool éMúltiplo(int m, int n)
{
    if(m % n == 0)
        return true;
    else
        return false;
}
Implementada a função, o programador fornece-a à sua equipa para utilização.  Depois de algumas semanas de utilização, o programa em desenvolvimento, na fase de testes, aborta com uma mensagem (ambiente Linux):
Floating exception (core dumped)
Depois de umas horas no depurador, conclui-se que, se o segundo argumento da função for zero, a função tenta uma divisão por zero, com a consequente paragem do programa.

Neste ponto o leitor deve parar de ler e tentar resolver o problema.

....................................

Chamado o programador original da função, este observa que não há múltiplos de zero, e portanto tipicamente corrige a função para

bool éMúltiplo(int m, int n)
{
    if(n != 0)
        if(m % n == 0)
            return true;
        else
            return false;
    else
        return false;
}
Corrigida a função e integrada de novo no programa em desenvolvimento, e repetidos os testes, não se encontra qualquer erro e o desenvolvimento continua.  Finalmente o programa é fornecido ao cliente.  O cliente utiliza o programa durante meses até que detecta um problema estranho, incompreensível.  Como tem um contrato de manutenção com a empresa que desenvolveu o programa, comunica-lhes o problema.  A equipa de manutenção, de que o programador original não faz parte, depois de algumas horas de execução do programa em modo depuração e de tentar reproduzir as condições em que o erro ocorre (que lhe foram fornecidas duma forma parcelar pelo cliente), acaba por detectar o problema: a função devolve false quando lhe são passados dois argumentos zero!  Mas zero é múltiplo de zero!  Rogando pragas ao programador original da função, esta é corrigida para:
bool éMúltiplo(int m, int n)
{
    if(n != 0)
        if(m % n == 0)
            return true;
        else
            return false;
    else
        if(m == 0)
            return true;
        else
            return false;
}
O código é testado e verifica-se que os erros estão corrigidos.

Depois, o programador olha para o código e acha-o demasiado complicado: para quê as instruções de selecção se uma simples instrução de retorno basta?

Neste ponto o leitor deve parar de ler e tentar resolver o problema com uma única instrução de retorno.

....................................

Basta devolver o resultado duma expressão booleana que reflita os casos em que m é múltiplo de n:

bool éMúltiplo(int m, int n)
{
    return (m % n == 0 && n != 0) || (m == 0 && n == 0);
}
Como a alteração é meramente cosmética, o programador não volta a testar e o programa corrigido é fornecido ao cliente.  Poucas horas depois de ser posto ao serviço o programa aborta.  O cliente recorre de novo aos serviços de manutenção, desta vez furioso.  A equipa de manutenção verifica rapidamente que a execução da função leva a uma divisão por zero como originalmente.  Desta vez a correcção do problema é simples: basta inverter os operandos da primeira conjunção:
bool éMúltiplo(int m, int n)
{
    return (n != 0 && m % n == 0) || (m == 0 && n == 0);
}
Note-se que esta simples troca corrige o problema porque nos operadores lógicos o cálculo é atalhado, i.e., o operando esquerdo é calculado em primeiro lugar e, se o resultado ficar imediatamente determinado (ou seja, se o primeiro operando numa conjunção for falso ou se o primeiro operando numa disjunção for verdadeiro) o operando direito não chega a ser calculado (ver Secção 2.5.3).

Outro exemplo

Apresenta-se brevemente o desenvolvimento de uma outra instrução alternativa, um pouco mais complexa.  Neste caso pretende-se escrever um procedimento de interface
void limita(int& x, int mínimo, int máximo)
que limite o valor de x ao intervalo [mínimo, máximo], onde se assume que mínimo <= máximo.  I.e., se o valor de x for inferior a mínimo, então deve ser alterado para mínimo, se for superior a máximo, deve ser alterado para máximo, se pertencer ao intervalo deve ser deixado com o valor original.

Como habitualmente, começa por se escrever a especificação do procedimento, onde x é uma variável matemática usada para representar o valor inicial da variável C++ x:

/*
   Este procedimento limita x ao intervalo [mínimo, máximo].
   PC: mínimo <= máximo e x = x
   CO: (x = xex >= mínimo e x <= máximo) ou (x = mínimo e x <= mínimo) ou
      (x = máximoex >= máximo)
*/
void limita(int& x, int mínimo, int máximo)
{
}
A observação da condição objectivo conduz imediatamente às seguintes possíveis instruções controladas:
;  // para manter x com o valor inicial x.
x = mínimo;
x = máximo;
As pré-condições mais fracas para que se verifique a CO do procedimento depois de cada uma das instruções são:
// PC1: (x = x e x >= mínimo e x <= máximo) ou (x = mínimoex <= mínimo) ou
       (x = máximo e x >= máximo)
;  // para manter x com o valor inicial x.
para a primeira instrução,
// PC2: (mínimo = x e x >= mínimo e x <= máximo) ou (mínimo = mínimoex <= mínimo) ou
       (mínimo = máximo e x >= máximo)
// ou seja:
// PC2: (mínimo = x e x >= mínimo e x <= máximo) ou x <= mínimoou
       (mínimo = máximo e x >= máximo)
x = mínimo;
para a segunda instrução, e
// PC3: (máximo = x e x >= mínimo e x <= máximo) ou (máximo = mínimoex <= mínimo) ou
       (máximo = máximo e x >= máximo)
// ou seja:
// PC3: (máximo = x e x >= mínimo e x <= máximo) ou (máximo = mínimoex <= mínimo) ou
       x >= máximo
x = máximo;
para a última instrução.

A determinação das guardas faz-se de modo a que PCeGi implique PCi.

No primeiro caso tem-se da PC que x = x, pelo que a guarda mais fraca é:

// G1: (x >= mínimo e x <= máximo) ou (x = mínimo) ou (x =máximo)
// ou seja:
// G1: x >= mínimoex <= máximo
pois os casos x = mínimo e x = máximo são cobertos pela primeira conjunção dado que a PC garante que mínimo <= máximo.

No segundo caso, pelas mesmas razões, tem-se que:

// G2: (mínimo = x e x <= máximo) oux <= mínimo ou
       (mínimo = máximo e x >= máximo)
// ou seja:
// G2: mínimo = x ou x <= mínimo ou (mínimo = máximo e x >= máximo)
// ou seja:
// G2: x <= mínimoou (mínimo = máximo e x >= máximo)
pois a PC mínimo <= máximo obriga a que, sendo x = mínimo, também será x <= máximo.

Da mesma forma se obtém:

// G3: (máximo = mínimo e x <= máximo) oux >= máximo
É evidente que há sempre pelo menos uma destas guardas válidas quaisquer que sejam os valores dos argumentos verificando as PC.  Logo, não são necessárias quaisquer outras instruções controladas.

Ou seja, a estrutura da instrução de selecção é (trocando a ordem das instruções de modo a que a instrução nula fique em último lugar):

if(C1)
    // G2: x <= mínimo ou (mínimo = máximoex >= máximo)
    x = mínimo;
else if(C2)
    // G3: (máximo = mínimo e x <= máximo) oux >= máximo
    x = máximo;
else
    // G1: x >= mínimo e x <= máximo
    ;
A observação das guardas demonstra que elas são redundantes para algumas combinações de valores.  Por exemplo, se mínimo = máximo qualquer das guardas G2 e G3 se verifica.  É fácil verificar, portanto, que as guardas podem ser simplificadas (reforçadas) para:
if(C1)
    // G2: x <= mínimo
    x = mínimo;
else if(C2)
    // G3: x >= máximo
    x = máximo;
else
    // G1: x >= mínimo e x <= máximo
    ;
de modo a que, nesse caso, seja válida apenas uma das guardas (excepto, claro, quando x = mínimo = máximo, em que se verificam de novo as duas).  De igual modo se podem eliminar as redundâncias no caso de x ter como valor um dos extremos do intervalo, pois nesse caso a guarda G1 "dá conta do recado":
if(C1)
    // G2: x < mínimo
    x = mínimo;
else if(C2)
    // G3: x > máximo
    x = máximo;
else
    // G1: x >= mínimo e x <= máximo
    ;


Assim, as condições das instruções if são:

if(x < mínimo)
    // G2: x < mínimo
    x = mínimo;
else if(x > máximo)
    // G3: x > máximo
    x = máximo;
else
    // G1: x >= mínimo e x <= máximo
    ;
Finalmente, pode-se eliminar o último else, pelo que o procedimento fica:
/*
   Este procedimento limita x ao intervalo [mínimo, máximo].
   PC: mínimo <= máximo e x = x
   CO: (x = xex >= mínimo e x <= máximo) ou (x = mínimo e x <= mínimo) ou
      (x = máximoex >= máximo)
*/
void limita(int& x, int mínimo, int máximo)
{
    if(x < mínimo)
        // G2: x < mínimo
        x = mínimo;
    else if(x > máximo)
        // G3: x > máximo
        x = máximo;
    /*
    else
        // G1: x >= mínimo e x <= máximo
        ;
    */
}
que pode ser simplificado para
/*
   Este procedimento limita x ao intervalo [mínimo, máximo].
   PC: mínimo <= máximo e x = x
   CO: (x = xex >= mínimo e x <= máximo) ou (x = mínimo e x <= mínimo) ou
      (x = máximoex >= máximo)
*/
void limita(int& x, int mínimo, int máximo)
{
    if(x < mínimo)
        x = mínimo;
    else if(x > máximo)
        x = máximo;
}

4.1.4  O operador ?:

Seja a função que calcula o valor absoluto de um número desenvolvida nas secções anteriores:
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(const int x)
{
    if(x <= 0)
        return -x;
    return x;
}
Será possível simplificá-la mais?  Sim.  A linguagem C++ fornece um operador ternário (com três operandos), que permite escrever a solução como
/*
   Esta função devolve o valor absoluto do argumento.
   PC: V
   CO: absoluto = |x|, ou seja, absoluto >= 0 e (absoluto = -x ou absoluto = x)
*/
int absoluto(const int x)
{
    return x <= 0? -x : x;
}
Este operador, ? :, tem a seguinte sintaxe:
condição ? expressão1 : expressão2
O resultado do operador é o resultado da expressão expressão1, se condição for verdadeira (nesse caso expressão2 não chega a ser calculada), ou o resultado da expressão expressão2, se condição for falsa (nesse caso expressão1 não chega a ser calculada).

4.1.5  A instrução switch

Suponha-se que se pretende escrever um procedimento que, dado um inteiro entre 0 e 9, o escreve por extenso no ecrã, em português (escrevendo "erro" se o inteiro for inválido).  Os nomes dos dígitos decimais em Português, como em qualquer outra língua natural, não obedecem a qualquer regra lógica.  Assim, a função terá de prever cada um dos 10 casos separadamente.  Usando a instrução de selecção encadeada:
// CO: surgiu no ecrã a versão por extenso de dígito.
// PC: 0 <= dígito < 10
void escreveDígitoPorExtenso(int dígito)
{
    if(dígito == 0)
        cout << "zero";
    else if(dígito == 1)
        cout << "um";
    else if(dígito == 2)
        cout << "dois";
    else if(dígito == 3)
        cout << "três";
    else if(dígito == 4)
        cout << "quatro";
    else if(dígito == 5)
        cout << "cinco";
    else if(dígito == 6)
        cout << "seis";
    else if(dígito == 7)
        cout << "sete";
    else if(dígito == 8)
        cout << "oito";
    else
        cout << "nove";
 }
Em C++ existe uma solução mais usual para este problema usando a instrução de selecção switch.  Quando é necessário comparar uma variável com vários valores possíveis, e executar uma acção diferente em cada um dos casos, deve-se usar esta instrução.  O procedimento anterior ficaria:
// CO: surgiu no ecrã a versão por extenso de dígito.
// PC: 0 <= dígito < 10
void escreveDígitoPorExtenso(int dígito)
{
    switch(dígito) {
      case 0:
        cout << "zero";
        break;
      case 1:
        cout << "um";
        break;
      case 2:
        cout << "dois";
        break;
      case 3:
        cout << "três";
        break;
      case 4:
        cout << "quatro";
        break;
      case 5:
        cout << "cinco";
        break;
      case 6:
        cout << "seis";
        break;
      case 7:
        cout << "sete";
        break;
      case 8:
        cout << "oito";
        break;
      default:
        cout << "nove";
        break;
    }
}
Esta instrução não permite a especificação de gamas de valores nem de desigualdades: construções como case 1..10: ou case < 10: são inválidas.  Assim, é possível usar como expressão de controlo do switch (i.e., a expressão que se coloca entre parênteses após a palavra-chave switch) apenas expressões de um dos tipos inteiros ou de um tipo enumerado (ver Secção 5.1), devendo as constantes colocadas nos case ...: respectivos ser do mesmo tipo.

É possível agrupar várias alternativas:

switch(valor) {
  case 1:
  case 2:
  case 3:
    cout "1, 2, ou 3";
    break;
  ...
}
Isto acontece porque a construção case n: apenas indica qual o ponto de entrada nas instruções que compõem o switch quando a sua expressão de controlo (entre parênteses após a palavra-chave switch) tem valor n.  A execução do corpo do switch (o bloco de instruções entre {}) só termina quando for atingida a chaveta final ou quando for executada uma instrução de break (terminado o switch a execução continua sequencialmente após a chaveta final).  A consequência deste facto é que, se no exemplo anterior se eliminarem os break:
// CO: surgiu no ecrã a versão por extenso de dígito.
// PC: 0 <= dígito < 10
void escreveDígitoPorExtenso(int dígito)
{
    switch(dígito) {
      case 0:
        cout << "zero";
      case 1:
        cout << "um";
      case 2:
        cout << "dois";
      case 3:
        cout << "três";
      case 4:
        cout << "quatro";
      case 5:
        cout << "cinco";
      case 6:
        cout << "seis";
      case 7:
        cout << "sete";
      case 8:
        cout << "oito";
      default:
        cout << "nove";
    }
}
uma chamada escreveDígitoPorExtenso(7) resulta em:
seteoitonove
que não é o que se pretendia!

Note-se que, pelas razões que se indicaram atrás, não é possível usar a instrução switch (pelo menos de uma forma imediata) como alternativa a:

// PC: 0 <= valore valor < 10000.
// CO: surgiu no ecrã a ordem de grandeza de valor.
void escreveGrandeza(double x)
{
    if(x < 10)
        cout << "unidades";
    else if(x < 100)
        cout << "dezenas";
    else if(x < 1000)
        cout << "centenas";
    else
        cout << "milhares";
}
A ordem pela qual se fazem as comparações em instruções de selecção encadeadas pode ser muito relevante em termos do tempo de execução do programa, embora seja irrelevante no que diz respeito aos resultados.  Suponha-se que os valores passados como argumento a escreveGrandeza() são equiprováveis.  Nesse caso consegue-se demostrar que, em média, são necessárias 2,989 comparações ao executar o procedimento e que, invertendo a ordem das instruções controladas para
// PC: 0 <= valore valor < 10000.
// CO: surgiu no ecrã a ordem de grandeza de valor.
void escreveGrandeza(double x)
{
    if(x >= 1000)
        cout << "milhares";
    else if(x >= 100)
        cout << "centenas";
    else if(x >= 10)
        cout << "dezenas";
    else
        cout << "unidades";
}
são necessárias 1,11 comparações (demonstre-o)!

No caso da instrução de selecção switch a ordem é irrelevante desde que todos os conjuntos de instruções tenham o respectivo break.

4.1.6  Exercícios

1.  Escreva uma função que devolva zero caso o argumento (de vírgula flutuante) seja menor do que zero, devolva o próprio argumento caso esteja entre zero e um e devolva 1 caso o argumento seja maior do que 1.  Crie um pequeno programa para testar esta função e faça o traçado desse programa.

2.  Escreva um programa que, dado um número inteiro entre 0 e 99 o escreva por extenso.  Exemplo:

Introduza um número:
35
O número introduzido foi três dezenas e cinco unidades.
Se se desejasse que o programa escrevesse
O número introduzido foi trinta e cinco.
o que se deveria fazer?

3.  Escreva uma função que devolva o número de dias de um mês num dado ano passado como argumento.  Lembre-se que os anos múltiplos de 4 são bissextos, com excepção dos múltiplos de 100, mas incluindo os múltiplos de 400 (esta fórmula só é válida para datas ocidentais em geral depois de 1752).

4.a)  Faça um programa que, dado um número inteiro que representa a idade de uma pessoa, escreva qual a sua faixa etária.  As faixas etárias são definidas do seguinte modo: entre 0 e 12 anos - criança, de 12 a 20 - adolescente, de 20 a 60 - adulto, acima de 60 - idoso.

4.b)  Modifique o programa anterior de modo a que seja inserido o sexo da pessoa (m ou f) em questão e as respostas sejam adequadas a esta informação.  Por exemplo, se a pessoa em questão for do sexo feminino em vez de escrever "adulto" deve escrever "adulta".

5.  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 é introduzido um conjunto de zeros e uns apartir do teclado descodifique a primeira letra que aparece.  Por exemplo:

Introduza um conjunto de zeros e uns:
1001001100110
C
6.  Caso já se sinta apto a usar a noção de ciclo, tente fazer um programa que descodifique toda a cadeia de zeros e uns.  Atenção: pode haver sequências que não terminam com a codificação total de um caracter (por exemplo 01 = A?).  O seu programa deve estar preparado para lidar com esse tipo de erros do utilizador.  Exemplo:
Introduza um conjunto de zeros e uns:
10010011001101
CCBAABA?

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

Na maioria dos programas há conjuntos de operações que é necessário repetir várias vezes.  Para controlar o fluxo do programa de modo a que um conjunto de instruções sejam repetidos condicionalmente usam-se as instruções de iteração * ou repetição (ciclos), explicadas nesta secção.

O conhecimento da sintaxe e da semântica das instruções de iteração do C++ não é suficiente para o desenvolvimento de bom código que as utilize.  Por um lado, o desenvolvimento de bom código C++ obriga à demonstração formal ou informal do seu correcto funcionamento.  Por outro lado, o desenvolvimento de ciclos não é uma tarefa fácil, sobretudo para o principiante.  Assim, na Secção 4.3 apresenta-se um método de demonstração da correcção de ciclos e faz-se uma pequena introdução à metodologia de Dijkstra, que fundamenta e disciplina a construção de ciclos (sem no entanto a mecanizar).

* O adjectivo "recursivo" é um neologismo informático que ainda não consta nos dicionários.  Foi construído por analogia com o adjectivo "iterativo" e tem essencialmente o mesmo significado que "recorrente".  De acordo com [1], iterar significa "tornar a fazer (...) repetir (...)" enquanto recorrer significa "tornar a correr (...)".  Recursão e iteração 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]).

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

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

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

while(G)
    passo
O fluxo de execução de um programa é muitas vezes mais fácil de representar na forma de um fluxograma ou diagrama de fluxo.  Neste diagramas as caixas rectangulares correspondem a instruções (em rigor "processos"), as caixas rômbicas (em forma de losango) correspondem a decisões, e as caixas rectangulares com cantos arredondados marcam o início e o fim do troço de código representado pelo diagrama.  As decisões correspondem a pontos da execução em que, dependendo do valor lógico (V ou F) de uma dada condição, a execução pode tomar um de dois caminhas alternativos.  A execução da instrução de iteração while pode ser representada pelo seguinte fluxograma:

Exemplo

O procedimento que se segue, por exemplo, escreva no ecrã uma linha com todos os números inteiros de zero a n (exclusivé), sendo n o seu único parâmetro:
// PC: n >= 0
// CO: o ecrã contém uma linha adicional com os inteiros de 0 a n exclusivé separados por espaços.
void escreveInteiros(const int n)     // 1
{                               // 2
    int i = 0;                  // 3
    while(i != n) {             // 4
        cout << i << ' ';       // 5
        ++i;                    // 6
    }                           // 7
    cout << endl;               // 8
}                               // 9
O fluxograma correspondente é:

Seguindo o fluxograma, 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 depois a verificar a condição (volta a A.).
  3. Se a condição for falsa, o ciclo termina.
Depois de terminado o ciclo, escreve-se um fim-de-linha (linha 8).

4.2.2  Variantes do ciclo while: for e do while

A maior parte dos ciclos usando a instrução while têm a forma
inic
while(G) {
    acção
    prog
}
onde inic são as instruções de inicialização (que preparam as variáveis envolvidas no ciclo), prog é uma instrução chamada progresso do ciclo (que garante que o ciclo termina em tempo finito), e acção é a acção do ciclo (ver Correcção total).  Nestes ciclos, portanto, o passo subdivide-se em acção e prog.

O C++ possui uma outra instrução de iteração, a instrução for, que permite escrever abreviadamente este tipo de ciclos:

for(inic; G; prog)
    acção
em que inic e prog têm de ser expressões em C++, embora inic possa definir variáveis locais.

O procedimento escreveInteiros() desenvolvido acima pode portanto ser reescrito como:

// PC: n >= 0
// CO: o ecrã contém uma linha adicional com os inteiros de 0 a n exclusivé separados por espaços.
void escreveInteiros(const int n)  // 1
{                                  // 2
    for(int i = 0; i != n; ++i)    // 3
        cout << i << ' ';          // 4
    cout << endl;                  // 5
}
Note-se que, ao converter o while num for, se aproveitou para restringir ainda mais a visibilidade da variável i que, estando definida no cabeçalho do for, só é visível nessa instrução (linhas 3 e 4).  Relembra-se que é de toda a conveniência que a visibilidade das variáveis seja sempre o mais restrita possível à zona onde de facto são necessárias.

Quaisquer das expressões no cabeçalho de uma instrução for (inic, G e prog) pode ser omitida.  A omissão da guarda G é equivalente à utilização de uma guarda sempre verdadeira, gerando por isso um ciclo infinito, ou laço (loop).  Ou seja, escrever

for(inic; ; prog)
    acção
é o mesmo que escrever
for(inic; true; prog)
    acção
cujo fluxograma é:

Este tipo de ciclos só é verdadeiramente interessante se passo contiver alguma instrução return ou break (ver abaixo) que obrigue o ciclo a terminar alguma vez.

Um outro tipo de ciclo em C++ corresponde à instrução dowhile.  A sintaxe desta instrução é:

do
    instrução_controlada
while(expressão_booleana);
A execução da instrução do while consiste na execução repetida de instrução_controlada 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 instrução_controlada.  Quando o resultado é falso o fluxo de execução passa para a instrução subsequente ao do while.  Note-se que instrução_controlada pode consistir em qualquer instrução do C++, nomeadamente um bloco de instruções.

Tal como no caso da instrução while, à instrução controlada pela instrução dowhile chama-se passo e à condição que a controla guarda G.  A execução da instrução de iteração do while pode ser representada pelo seguinte fluxograma:

Note-se que, ao contrário do que se passa com a instrução while, durante a execução de uma instrução dowhile o passo é executado sempre pelo menos uma vez.

Exemplo com for

O ciclo for é usado frequentemente para repetições ou contagens simples.  Por exemplo, se se pretender escrever no ecrã os inteiros de 0 a 9 (um por linha), pode-se usar o seguinte código:
for(int i = 0; i != 10; ++i)
    cout << i << endl;
Se se pretender escrever no ecrã uma linha com 10 asteriscos, pode-se usar o seguinte código:
for(int i = 0; i != 10; ++i)
    cout << '*';
cout << endl; // para terminar a linha.
É importante observar que, em C++, é típico começar as contagens em zero (0) e usar como guarda o total de repetições a efectuar.  Nos ciclos acima a variável i toma todos os valores entre 0 e 10 inclusivé, embora para i = 10 a acção do ciclo não chegue a ser executada.  Alterando o primeiro ciclo de modo a que a definição da variável i seja feita fora do ciclo e o seu valor no final do ciclo mostrado no ecrã
int i = 0;
for(; i != 10; ++i)
    cout << i << endl;
cout << "O valor final é " << i << endl;
então a execução deste troço de código resultaria em
0
1
2
3
4
5
6
7
8
9
O valor final é 10

Exemplo com do while

Se se pretender validar uma entrada de dados por um utilizador (e.g., se o utilizador apenas devesse escrever números entre 0 e 10 inclusivé) e se se quiser obrigar à repetição da entrada de dados até o utilizador escrever o número correcto, então pode-se usar um ciclo:
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, ou seja, a variável n tem de ter um valor que não seja arbitrário.  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.

Equivalências entre instruções de iteração

É sempre possível converter um ciclo de modo a usar qualquer das instruções de iteração.  No entanto, a maior parte dos problemas resolvem-se de um modo mais óbvio e mais legível com uma destas instruções do que com as outras.
 
Equivalências entre ciclos
inic
while(G) {
    acção
    prog
}
é equivalente a for(inic; G; prog)
    acção
inic
while(G)
    passo
é equivalente a for(inic; G;)
    passo
inic
while(G)
    passo
é equivalente a // Má ideia...
inic
if(G)
    do
        passo
    while(G);
do
    passo
while(G);
é equivalente a passo
while(G)
    passo

Embora sejam válidas em geral, estas equivalências não são verdadeiras se as instruções controladas incluirem as instruções return, break, continue, ou goto (ver abaixo).  Também há diferença quanto ao âmbito de variáveis definidas nestas instruções.

4.2.3  Exemplo simples

A título de exemplo de utilização simultânea de instruções de iteração e de selecção e de instruções de iteração encadeadas, segue-se um programa que escreve um triângulo rectângulo de asteriscos com o interior vazio:
#include <iostream>
using namespace std;

// Escreve um triângulo oco de asteriscos com a altura passada como argumento:
// PC: altura >= 0
// CO: o ecrã contém altura linhas adicionais representando um
// triângulo rectângulo oco de asteriscos.
void escreveTriânguloOco(const int altura)
{
    for(int i = 0; i != altura; ++i) {
        for(int j = 0; j <= i; ++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ânguloOco(a);
}

Sugere-se que o leitor faça o traçado deste programa e/ou o execute em modo de depuração para compreender os dois ciclos encadeados.

4.2.4  return, break, continue, e goto em ciclos

Se um ciclo estiver dentro de uma função ou procedimento e se pretender retornar (sair da função ou procedimento) a meio do ciclo, então pode-se usar a instrução de retorno.  Por exemplo, se se pretender escrever uma função que devolva V caso o seu parâmetro (inteiro) seja primo e F no caso contrário, ver-se-á mais tarde que uma possibilidade é (ver Secção 4.3.2 e Outro exemplo):
// PC: n >= 0
// CO: éPrimo = ((Qi : 2 <= i < n : n ÷ i <> 0) e n > 1)
bool éPrimo(const int n)
{
    if(n <= 1)
        return false;

    for(k = 2; k != n; ++k)
        if(n % k == 0)
            // Se se encontrou um divisor >= 2 e < n, então n não é primo
            // e pode-se retornar imediatamente:
            return false;
    return true;
}

Este tipo de terminação abrupta de ciclos pode ser muito útil, mas também pode contribuir para tornar difícil a compreensão dos programas.  Deve portanto ser usado com precaução e parcimónia e apenas em funções ou procedimentos muito curtos.  Noutros casos torna-se preferível usar técnicas de reforço das guardas do ciclo (ver também Secção 4.4.4, Exemplo 1).

Outras possibilidades de alterar o funcionamento normal dos ciclos são através das instruções de break, continue, e goto.

A sintaxe da última encontra-se em qualquer livro sobre o C++ (e.g., [6]), e não será explicada aqui.  Desaconselha-se vivamente a sua utilização.

A instrução break serve para terminar abruptamente a instrução while, for, do while, ou switch mais interior dentro da qual se encontre (ou seja, se existirem duas dessas instruções encadeadas, uma instrução de break termina apenas a instrução interior), continuando a execução na instrução subsequente.

A instrução continue é semelhante à instrução break, mas serve para começar antecipadamente a próxima iteração do ciclo (apenas no caso das instruções while, for e do while).

Às instruções break e continue aplicam-se as recomendações feitas quanto à utilização da instrução return dentro de ciclos: usar pouco e com cuidado.

Exemplo

Considerem-se de novo os dois ciclos alternativos para validar uma entrada de dados por um utilizador:
cout << "Introduza valor (0 a 10): ";
cin >> n;
while(n < 0 || n > 10) {
    cout << "Incorrecto, introduza de novo (0 a 10): ";
    cin >> n;
}
e
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);
Ambas deixam um amargo de boca.  A primeira porque obriga à repetição da instrução de leitura do valor, que portanto aparece antes da instrução while e no seu passo.  A segunda porque obriga a uma instrução alternativa cuja guarda é a negação da guarda do ciclo.  O problema está em que teste da guarda deveria ser feito não antes do passo (como na instrução while), nem depois do passo (como na instrução do while), mas dentro do passo!  Ou seja, negando a guarda da instrução de selecção e usando uma instrução break,
cout << "Introduza valor (0 a 10): ";
do {
    cin >> n;
    if(n >= 0 || n <= 10)
        break;
    cout << "Incorrecto, introduza de novo (0 a 10): ";
} while(n < 0 || n > 10);
que, como a guarda do ciclo é sempre verdadeira quando é verificada (quando n é válido o ciclo termina na instrução break, sem se testar a guarda), é equivalente a
cout << "Introduza valor (0 a 10): ";
do {
    cin >> n;
    if(n >= 0 || n <= 10)
        break;
    cout << "Incorrecto, introduza de novo (0 a 10): ";
} while(true);
que é mais comum escrever como
cout << "Introduza valor (0 a 10): ";
while(true) {
    cin >> n;
    if(n >= 0 || n <= 10)
        break;
    cout << "Incorrecto, introduza de novo (0 a 10): ";
}
Os ciclos do while(true), for(;;), e while(true) são ciclos infinitos ou laços, que só terminam com recurso a uma instrução break ou return *.

* Noutras linguagens existe uma construção loop para este efeito, que pode ser simulada em C++ recorrendo ao pré-processador (ver capítulos posteriores):

#define loop while(true)

loop {
    cout << "Introduza valor (0 a 10): ";
    cin >> n;
    if(0 <= n && n <= 10)
        break;
    cout << "Incorrecto! ";
}

4.2.5  Problemas comuns

De longe o problema mais comum ao escrever ciclos em C++ é o "falhanço por um" (off by one).  Por exemplo, quando se desenvolve um ciclo for para escrever 10 asteriscos no ecrã, é comum falhar a escrita da guarda e usar:
for(int i = 0; i <= 10; ++i)
    cout << '*';
que escreve um asterisco a mais.  É necessário ter muito cuidado com a guarda dos ciclos, pois estes erros são muito comuns e, por vezes, difíceis de detectar.

Outro problema comum corresponde a colocar um ; após o cabeçalho de um ciclo.  Por exemplo:

int i = 0;
while(i != 10);
{
    cout << '*';
    ++i;
}
Neste caso o ciclo nunca termina, pois a instrução controlada pelo while (o seu passo) é a instrução nula, que não afecta o valor de i.  Um caso pior ocorre quando se usa um ciclo for:
for(int i = 0; i != 10; ++i);
    cout << '*';
Este caso é mais grave porque o ciclo termina!  Mas, ao contrário do que o programador pretendia, este pedaço de código escreve apenas um asterisco, e não 10!

4.3  Asserções com quantificadores

A especificação de de um problema sem quaisquer ambiguidades é, como se viu, o primeiro passo a realizar para a sua solução.  A especificação de um problema faz-se tipicamente indicando a pré-condição (PC) e a condição objectivo (CO).  A PC é um predicado acerca das variáveis do problema que se assume ser verdadeiro no início.  A CO é um predicado que se pretende que seja verdadeiro depois de resolvido o problema, i.e., depois de executado o troço de código que o resolve.  A PC e a CO não passam, como se viu antes, de asserções ou afirmações feitas acerca das variáveis de um programa, i.e., acerca do seu estado, sendo o estado de um programa dado pelo valor das suas variáveis.

A escrita de asserções para problemas um pouco mais complicados que os vistos até aqui requer a utilização de quantificadores.  Quantificadores são formas matemáticas abreviadas de escrever expressões que envolvem a repetição de uma dada operação.  Exemplos são os quantificadores aritméticos somatório e produto, e os quantificadores lógicos universal ("qualquer que seja") e existencial ("existe pelo menos um").

Apresentam-se aqui algumas notas sobre os quantificadores que são mais úteis para a construção de asserções acerca do estado dos programas.  A notação utilizada encontra-se resumida no Apêndice A.

Os quantificadores escrever-se-ão sempre da seguinte forma:

(X v : predicado(v) : expressão(v))
onde

4.3.1  Somas

O quantificador soma corresponde ao usual somatório.  Na notação utilizada, (S i : m <= i < n : expressão(i)) tem exactamente o mesmo significado que o somatório (representado pela letra grega sigma maiúscula) de expressão(i) com i variando entre m e n - 1.

Por exemplo, é um facto conhecido que o somatório do primeiros n inteiros não negativos (S i : 0 <= i < n : i) tem valor n (n - 1) / 2 se n > 0.  Que acontece se n <= 0?  Neste caso é evidente que a variável muda i não pode tomar quaisquer valores, e portanto o resultado é a soma de zero (0) termos.  A soma de zero termos é zero (0), por ser esse o elemento neutro da soma.  Logo, (S i : 0 <= i < n : i) = 0 se n <= 0.  Em geral, (S i : m <= i < n : expressão(i)) = 0 se n <= m.

Se se pretender desenvolver uma função que calcule a soma dos n primeiros números ímpares positivos (sendo n um parâmetro da função), pode-se começar por escrever o seu cabeçalho bem como a PC e a CO, como habitual:

// PC: n >= 0
// CO: somaÍmpares = (S i : 0 <= i < n : 2 i + 1)
int somaÍmpares(const int n)
{
    ...
}
Note-se que, mais uma vez, se fez o parâmetro da função constante, de modo a deixar claro que a função não lhe altera o valor.

4.3.2  Produtos

O quantificador produto corresponde ao produto de termos por vezes conhecido como "produtório" ou mesmo "piatório".  Na notação utilizada, (P i : m <= i < n : expressão(i)) tem exactamente o mesmo significado que o usual produto (representado pela letra grega pi maiúscula) de expressão(i) com i variando entre m e n (exclusivé).

Por exemplo, a definição de factorial é n! = (P i : 1 <= i <= n : i ) se n >= 0.

O produto de zero termos é um (1), por ser esse o elemento neutro da multiplicação.  Ou seja, (P i : m <= i < n : expressão(i)) = 1 se n <= m.

Se se pretender desenvolver uma função que calcule o factorial de n (sendo n um parâmetro da função), pode-se começar por escrever o seu cabeçalho bem como a PC e a CO:

// PC: n >= 0
// CO: factorial = n! = (P  i : 1 <= i <= n : i)
int factorial(const int n)
{
    ...
}

4.3.3  Conjunções e o quantificador universal

O quantificador universal corresponde à conjunção (e) de vários predicados usualmente conhecida por "qualquer que seja" *.  Na notação utilizada, (Q i : m <= i < n : predicado(i)) tem exactamente o mesmo significado que a conjunção dos predicados predicado(i) com i variando entre m e n (exclusivé).

A conjunção de zero termos tem valor V (verdadeiro), por ser esse o elemento neutro da conjunção.  Ou seja, (Qi : m <= i < n : predicado(i)) = V se n <= m *.

Por exemplo, a definição da função (matemática!) primo(n), que tem valor V se n é primo e F no caso contrário, é:

primo(n) = (Q i : 2 <= i < n : n ÷ i <> 0), se n >= 2, e
primo(n) = F, se 0 <= n < 2,
ou seja,
primo(n) = ((Q i : 2 <= i < n : n ÷ i <> 0) e n >= 2) para n >= 0
sendo ÷ a operação resto da divisão inteira (como o operador % em C++).

Se se pretender desenvolver uma função (agora em C++) que devolva o valor lógico verdadeiro quando n (sendo n um parâmetro da função) é um número primo, e falso no caso contrário, pode-se começar por escrever o seu cabeçalho bem como a PC e a CO:

// PC: n >= 0
// CO: éPrimo = ((Qi : 2 <= i < n : n ÷ i <> 0) e n >= 2)
bool éPrimo(const int n)
{
    ...
}
* Se não é claro para si  que o quantificador universal corresponde a uma sequência de conjunções, pense no significado de escrever "todos os humanos têm cabeça".  A tradução para linguagem (quase) matemática seria "qualquer que seja h pertencente ao conjunto dos humanos, h tem cabeça".  Como o conjunto dos humanos é finito, pode-se escrever por extenso, listando todos os possíveis humanos: "o Yeltsin (aparenta) ter cabeça e o Sampaio tem cabeça e ...".  Isto é, a conjunção de todas as afirmações.

* A afirmação "todos os marcianos têm cabeça" é verdadeira, pois não existem marcianos.  Esta propriedade é menos intuitiva que no caso dos quantificadores soma e produto, mas é importante.

4.3.4  Disjunções e o quantificador existencial

O quantificador existencial corresponde à disjunção de vários predicados usualmente conhecida por "existe pelo menos um" *.  Na notação utilizada, (E i : m <= i < n : predicado(i)) tem exactamente o mesmo significado que a disjunção dos predicados predicado(i) com i variando entre m e n (exclusivé) e pode-se ler como "existe um valor i entre m e n (exclusivé) tal que predicado(i) é verdadeiro".

A disjunção de zero termos tem valor F (falso), por ser esse o elemento neutro da disjunção.  Ou seja, (Ei : m <= i < n : predicado(i)) = F se n <= m *.

Este quantificador está estreitamente relacionado com o quantificador universal.  É sempre verdade que ¬(Q i : m <= i < n : predicado(i)) = (Ei : m <= i < n : ¬predicado(i)), ou seja, se não é verdade que para qualquer i o predicado predicado(i) é verdadeiro, então existe pelo menos um i para o qual o predicado predicado(i) é falso.  Aplicado à definição de número primo acima, tem-se ¬primo(n) = ¬(Qi : 2 <= i < n : n ÷ i <> 0) = (Ei : 2 <= i < n : n ÷ i = 0), para n > 1.  I.e., um número maior do que 1 não é primo se for divisível por algum inteiro maior do que 1 e menor que ele próprio.

Se se pretender desenvolver uma função que devolva o valor lógico verdadeiro quando existir um número primo entre m e n exclusivé (sendo m e n parâmetros da função, com m não negativo), e falso no caso contrário, pode-se começar por escrever o seu cabeçalho bem como a PC e a CO:

// PC: m >= 0
// CO: existePrimoNoIntervalo = (E i : m <= i < n : primo(i))
bool existePrimoNoIntervalo(const int m, const int n)
{
    ...
}
* Se não é claro para si  que o quantificador existencial corresponde a uma sequência de disjunções, pense no significado de escrever "existe pelo menos um humano com cabeça".  A tradução para linguagem (quase) matemática seria "existe pelo menos um h pertencente ao conjunto dos humanos tal que h tem cabeça".  Como o conjunto dos humanos é finito, pode-se escrever por extenso, listando todos os possíveis humanos: "o Yeltsin tem cabeça ou o Sampaio tem cabeça ou ...".  Isto é, a disjunção de todas as afirmações.

* A afirmação "existe pelo menos um marciano com cabeça" é falsa, pois não existem marcianos.

4.3.5  Contagens

O quantificador de contagem (N i : m <= i < n : predicado(i)) tem como valor (inteiro) o número de predicados predicado(i) verdadeiros para i no conjunto indicado implicitamente.  Por exemplo, (Ni : 1 <= i < 10 : primo(i)) = 4, ou seja, "existem quatro primos entre 1 e 10 exclusivé", é uma afirmação verdadeira.

Este quantificador é extremamente útil quando é necessário especificar condições em que o número de ordem é fundamental.  Se se pretender desenvolver uma função que devolva o n-ésimo número primo (sendo n parâmetro da função), pode-se começar por escrever o seu cabeçalho bem como a PC e a CO:

// PC: n >= 1
// CO: primo(primo) e (N i : 0 <= i < primo : primo(i)) = n - 1
int primo(const int n)
{
    ...
}
ou seja, o valor devolvido é um primo e existem n - 1 primos de valor inferior.

O resto da divisão

Quando se apresentou o operador ÷, resto da definição inteira, não se fez uma definição formal dele.  Esta pode ser feita à custa de alguns dos quantificadores apresentados.  Seja m ÷ n = resto(m, n) (com m >= 0 e n > 0 *).  Então, resto(m, n) é o único elemento do conjunto {r : 0 <= r < n e (E q : q >= 0 : m = q × n + r)}.  Claro que a definição está incompleta enquanto não se demonstrar que de facto o conjunto tem um único elemento, ou seja que, quando m >= 0 e n > 0, se tem (N r : 0 <= r < n : (E q : q >= 0 : m = q × n + r)) = 1.  Essa demonstração deixa-se como exercício para o leitor.

* A definição de resto pode ser generalizada para englobar valores negativos de m e n.  Em qualquer dos casos tem de existir um quociente q tal que m = q × n + r.  Mas o intervalo onde r se encontra varia:

0 <= r < n se m >= 0 e n > 0 (neste caso q >= 0)
0 <= r < -n se m >= 0 e n < 0 (neste caso q <= 0)
-n < r <= 0 se m <= 0 e n > 0 (neste caso q <= 0)
n < r <= 0 se m <= 0 e n < 0 (neste caso q >= 0)

4.4  Desenvolvimento de ciclos

O desenvolvimento de programas usando ciclos é simultaneamente uma arte [3] e uma ciência [4].  Embora a intuição seja muito importante, por vezes, pelo menos para os problemas menos evidentes, é fundamental usar metologias mais ou menos formais de desenvolvimento de ciclos que permitam garantir simultaneamente a sua correcção.  Embora esta matéria seja formalizada na cadeira de Computação e Algoritmia, apresentam-se aqui os conceitos básicos da metodologia de Dijkstra para o desenvolvimento de ciclos.  Para uma apresentação mais completa consultar [4].

Suponha-se que se pretende desenvolver uma função para calcular a potência n de x, isto é, uma função que, sendo x e n os seus dois parâmetros, devolva xn.  A sua estrutura básica é portanto:

double potência(double x, int n)
{
    ...
}
É claro que xn = x × x ... × x, ou seja, xn pode ser obtido por multiplicação repetida de x.  Assim, uma solução é usar um ciclo que no seu passo faça cada uma dessas multiplicações:
double potência(double x, int n)
{
    int i = 1;    // usada para contar o número de x já incluidos no produto.
    double r = x; // usada para acumular os produtos de x.
    while(i <= n) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    return r;
}
Será que o ciclo está correcto?  Fazendo um traçado da função admitindo que é chamada com os argumentos 5,0 e 2, verifica-se facilmente que devolve 125,0 e não 25,0!  Isso significa que é feita uma multplicação a mais.  Observando com atenção a guarda da instrução de iteração, conclui-se que esta não deveria deixar o contador i atingir o valor de n.  Ou seja, a guarda deveria ser i < n e não i <= n.  Corrigindo a função:
double potência(double x, int n)
{
    int i = 1;    // usada para contar o número de x já incluidos no produto.
    double r = x; // usada para acumular os produtos de x.
    while(i < n) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    return r;
}
Estará o ciclo definitivamente correcto?  Fazendo traçados da função admitindo que é chamada com argumentos 5,0 e 2 e com 5,0 e 3, facilmente se verifica que devolve respectivamente 25,0 e 125,0, pelo que aparenta estar correcta.  Mas estará correcta para todos os valores?  E se os argumentos forem 5,0 e 0?  Nesse caso a função devolve 5,0 em vez do valor correcto, que é 5,00 = 1,0!  Observando com atenção a inicialização do contador e do acumulador dos produtos, conclui-se que estes deveriam ser inicializados com 0 e 1,0, respectivamente.  Corrigindo a função:
double potência(double x, int n)
{
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i < n) {
         r *= x;   // o mesmo que r = r * x;
         ++i;      // o mesmo que i = i + 1;
    }
    return r;
}
Neste momento a função parece estar correcta.  Mas que acontece se o expoente for negativo?  Fazendo o traçado da função admitindo que é chamada com os argumentos 5,0 e -1, facilmente se verifica que devolve 1,0 em vez de 5,0-1 = 0,2!

Os vários problemas que surgiram ao longo desde desenvolvimento atribulado deveram-se a que:

  1. O problema não foi bem especificado através da escrita da PC e da CO.  Em particular a PC deveria estabelecer claramente se a função deve saber lidar com expoentes negativos ou não.
  2. O desenvolvimento foi feito "ao sabor da pena", duma forma pouco disciplinada.
Retrocedendo um pouco na resolução do problema de escrever a função potência(), é fundamental, pelo que se viu, começar por especificar o problema sem ambiguidades.  Para isso formalizam-se a PC e a CO da função.  Para simplificar, suponha-se que a função só deve garantir bom funcionamento para valores não-negativos do expoente.  Aproveite-se também para indicar claramente que os valores dos parâmetros não devem poder ser alterados, i.e., que os parâmetros não são variáveis mas sim constantes:
// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i < n) {
         r *= x;   // o mesmo que r = r * x;
         ++i;      // o mesmo que i = i + 1;
    }
    return r;
}
É importante verificar agora o que acontece se o programador utilizador da função se enganar e, violando o contrato expresso pela PC e pela CO, a invocar com um expoente negativo.  Como se viu, a função simplesmente devolve um valor errado: 1,0.  Isso acontece porque, sendo n negativo e i inicializado com 0, a guarda é inicialmente falsa, não sendo o passo do ciclo executado nenhuma vez.  É possível enfraquecer a guarda, de modo a que seja falsa em menos circunstâncias, e de tal forma que o contrato da função se mantenha: basta substituir i < n por i != n.  É óbvio que para valores do expoente não-negativos as duas guardas são equivalentes.  Mas para expoentes negativos a nova guarda leva a um ciclo infinito!  O contador i vai crescendo a partir de zero, afastando-se irremediavelmente do valor negativo de n.

Qual das guardas será preferível?  A primeira, que em caso de engano por parte do programador utilizador da função devolve um valor errado, ou a segunda, que nesse caso entra num ciclo infinito, não chegando a terminar?  A verdade é que é preferível a segunda, pois o programador  utilizador mais facilmente se apercebe do erro e o corrige em tempo útil *.  Com a segunda guarda o problema pode só ser detectado demasiado tarde.  Assim, a nova versão da função será:

// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i != n) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    return r;
}
Isto resolve o primeiro problema.  E o segundo?

É possível certamente desenvolver código C++ "ao sabor da pena", mas é importante que seja desenvolvido correctamente.  Significa isto que, para ciclos mais simples ou conhecidos, o programador desenvolve-os rapidamente, sem grandes preocupações.  Mas para ciclos mais complicados o programador deve ter especial atenção à sua correcção.  Das duas uma, ou os desenvolve primeiro e depois demonstra a sua correcção, ou usa uma metodologia de desenvolvimento que garanta a sua correcção.  As próximas secções lidam com estes dois problemas: o da demonstração de correcção de ciclos e o de metodologias de desenvolvimento de ciclos.  Antes, porém, é fundamental apresentar a noção de invariante um ciclo.

Em alguns casos, porém, a utilização desta metodologia não é prática, uma vez que requer um arsenal considerável de modelos: é o caso de ciclos que envolvam leituras do teclado e/ou escritas no ecrã.  Nesses casos a metodologia continua aplicável, como é óbvio, embora seja vulgar que as asserções sejam escritas com muito menos formalismo.

Note-se que na realidade o ciclo não é infinito, pois os int são limitados e incrementações sucessivas levarão o valor do contador ao limite superior dos int.  O que acontece depois depende do compilador, sistema operativo e máquina em que o programa foi compilado e executado.  Normalmente o que acontece é que uma incrementação feita ao contador quando o seu valor já atingiu o limite superior dos int leva o valor a "dar a volta" aos inteiros, passando para o limite inferior dos int.  Incrementações posteriores levarão o contador até zero, pelo que em rigor o ciclo não é infinito...  Mas demora muito tempo a executar, pelo menos, o que mantém a validade do argumento usado para justificar a fraqueza das guardas.

4.4.1  Noção de invariante

Considerando de novo a função desenvolvida, assinalem-se com números todos os intervalos entre instruções da função, i.e., os instantes de tempo entre as execuções das instruções:
// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
ponto 1, depois da inicialização do ciclo, i.e., depois da inicialização das variáveis nele envolvidas.
    while(i != n) {
ponto 2, depois de se verificar que a guarda é verdadeira.
        r *= x;     // o mesmo que r = r * x;
ponto 3, depois de acumular mais uma multiplicação de x em r.
        ++i;      // o mesmo que i = i + 1;
ponto 4, depois de incrementar o contador do número de x já incluidos no produto r.
    }
ponto 5, depois de se verificar que a guarda é falsa, imediatamente antes do retorno.
    return r;
}
Suponha-se que a função é invocada com os argumentos 3,0 e 4.  Isto é, suponha-se que quando a função começa a ser executada as constantes x e n têm os valores 3,0 e 4.  Faça-se um traçado da execução da função anotando o valor das suas variáveis em cada um dos pontos entre instruções assinalados.  Obtém-se a seguinte tabela:
 
ponto
i
r
comentários
1
0
1,0
como 0 <> 4, o passo do ciclo será executado, passando-se ao ponto 2 
2
0
1,0
 
3
0
3,0
 
4
1
3,0
como 1 <> 4, o passo do ciclo será executado, passando-se ao ponto 2
2
1
3,0
 
3
1
9,0
 
4
2
9,0
como 2 <> 4, o passo do ciclo será executado, passando-se ao ponto 2
2
2
9,0
 
3
2
27,0
 
4
3
27,0
como 3 <> 4, o passo do ciclo será executado, passando-se ao ponto 2
2
3
27,0
 
3
3
81,0
 
4
4
81,0
como 4 <> 4 é falso, o ciclo termina, passando-se ao ponto 5
5
4
81,0
 

É fácil verificar que há uma condição relacionando x, r, n e i que se verifica em todos os pontos do ciclo com excepção do ponto 3.  I.e., que se verifica depois da inicialização, antes do passo, depois do passo, e no final do ciclo.  A relação é r = xi e 0 <= i <= n.  Esta relação diz algo razoavelmente óbvio: em cada instante (excepto no ponto 3) o acumulador possui a potência i do valor em x, tendo i um valor entre 0 e n.  Esta condição, por ser verdadeira ao longo de todo o ciclo (excepto a meio do passo, no ponto 3), diz-se uma "invariante" do ciclo.

As condições invariantes são centrais na demonstração da correcção de ciclos e durante o desenvolvimento disciplinado de ciclos, como se verá nas próximas secções.

Em geral, para um ciclo da forma

inic
while(G)
    passo
uma condição diz-se invariante (CI) se
  1. for verdadeira logo após a inicialização do ciclo (inic);
  2. for verdadeira imediatamente antes do passo (passo);
  3. for verdadeira imediatamente após o passo; e
  4. for verdadeira depois de terminado o ciclo.
Representando o ciclo na forma de um fluxograma e colocando as asserções que se sabe serem verdadeiras em cada seta do diagrama, é mais fácil compreender a noção de invariante:







Note-se que se incluiram asserções adicionais.  Antes da inicialização assume-se que a pré-condição (PC) do ciclo se verifica, e no final do ciclo pretende-se que se verifique a sua condição objectivo (CO).  Além disso, depois da decisão do ciclo, em que se verifica a veracidade da guarda, sabe-se que a guarda (G) é verdadeira antes de executar o passo e falsa (ou seja, verdadeira a sua negação ¬G) depois de terminado o ciclo.  Ou seja, antes do passo sabe-se que CI e G é uma asserção verdadeira e no final do ciclo sabe-se que CI e ¬G é também uma asserção verdadeira.

Note-se que, no caso da função potência(), a condição objectivo do ciclo é diferente da condição objectivo da função, pois refere-se à variável r, que será posteriormente devolvida pela função.  I.e.:

// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    // PC: n >= 0
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i != n) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    // CO': r = xn
    return r;
}

Demonstração de invariância

Um passo fundamental na demonstração da correcção de um ciclo é a demonstração de invariância de uma dada asserção CI.

Para que a asserção CI possa ser invariante tem de ser verdadeira depois da inicialização (ver fluxograma).  I.e., assumindo que a PC do ciclo se verifica, então a inicialização inic tem de conduzir forçosamente à veracidade da asserção CI.  É necessário portanto demonstrar que:

// PC
inic
// implica CI
Para o exemplo do cálculo da potência isso é uma tarefa simples.  Nesse caso tem-se:
// PC: n >= 0
int i = 0;
double r = 1.0;
// CI: r = xie 0 <= i <= n
A demonstração neste pode ser feita substituindo na CI os valores iniciais de r e i:
CI: 1,0 = x0 e 0 <= 0 <= n, ou seja
CI: 1,0 = 1,0 e 0 <= 0 e 0 <= n, ou seja
CI: V e V e 0 <= n, ou seja
CI: 0 <= n, que é garantidamente verdadeira dada a PC, ou seja
CI: V
Neste momento demonstrou-se a veracidade da asserção CI depois da inicialização.  Falta demonstrar que é verdadeira antes do passo, depois do passo, e no fim do ciclo.  A demonstração pode ser feita por indução.  Supõe-se que a asserção CI é verdadeira antes do passo numa qualquer iteração do ciclo e demonstra-se que é também verdadeira depois do passo nessa mesma iteração.  I.e., como antes do passo a guarda é forçosamente verdadeira, pois de outra forma o ciclo teria terminado, é necessário demonstrar que:
// CI e G
passo
// implica CI
Se esta demonstração for possível, então por indução a asserção CI será verdadeira ao longo de todo o ciclo, i.e., antes e depois do passo em qualquer iteração do ciclo e no final do ciclo.  Isso pode ser visto claramente observando o fluxograma.  Como se demonstrou que a asserção CI é verdadeira depois da inicialização, então também será verdadeira depois de verificada a guarda pela primeira vez, visto que a verificação da guarda não altera qualquer variável do programa.  Ou seja, se a guarda for verdadeira a asserção CI será verdadeira antes do passo na primeira iteração do ciclo e, se a guarda for falsa, a asserção CI será verdadeira no final do ciclo.  Se a guarda for verdadeira, como se demonstrou que a veracidade de CI antes do passo numa qualquer iteração implica a sua veracidade depois do passo na mesma iteração, conclui-se que, quando se for verificar de novo a guarda do ciclo (ver fluxograma) a asserção CI é verdadeira.  Logo, pode-se repetir o argumento para a segunda iteração do ciclo e assim sucessivamente.

Para o caso dado, é necessário demonstrar que:

// CI e G: r = xi e 0 <= i <= n ei <> n, ou seja, simplificando
// r = xi e 0 <= i < n
r *= x;   // o mesmo que r = r * x;
++i;      // o mesmo que i = i + 1;
// CI: r = xie 0 <= i <= n
A demonstração pode ser feita verificando qual a pré-condição mais fraca de cada uma das atribuições, do fim para o princípio (ver Dedução da pré-condição mais fraca de uma atribuição).  Obtém-se:
// r = xi+1e 0 <= i + 1 <= n, ou seja
// r = xi × xe -1 <= i <= n - 1, ou seja
// r = xi × xe -1 <= i < n
++i;      // o mesmo que i = i + 1;
// CI: r = xie 0 <= i <= n
ou seja, para que a asserção CI se verifique depois da incrementação de i é necessário que se verifique a asserção
r = xi × x e -1 <= i < n
antes da incrementação.  Aplicando a mesma técnica à atribuição anterior tem-se que:
// r × x = xi × x e -1 <= i < n, ou seja, eliminando x de ambos os lados da igualdade,
// r = xi e -1 <= i < n
r *= x;   // o mesmo que r = r * x;
// r = xi × xe -1 <= i < n
Falta portanto demonstrar que
CI e G: r = xie 0 <= i < n
implica r = xi e -1 <= i < n
mas isso verifica-se por mera observação (se i >= 0, então forçosamente i >= - 1).

Em resumo, para provar a invariância de uma asserção CI é necessário:

  1. Mostrar que, admitindo a veracidade da PC antes da inicialização, a asserção CI é verdadeira depois da inicialização inic.  Ou seja:
    1. // PC
      inic
      // implica CI
  2. Mostrar que, se CI for verdadeira no início do passo numa qualquer iteração do ciclo, então também o será depois do passo nessa mesma iteração.  Ou seja:
    1. // CI e G
      passo
      // implica CI
    sendo a demonstração feita, comummente, do fim para o princípio, deduzindo as pré-condições mais fracas de cada instrução do passo.

4.4.2  Correcção de ciclos

Viu-se que a demonstração da correcção de ciclos é muito importante.  Mas como fazê-la?  Há que demonstrar dois factos: que o ciclo quando termina garante que a CO se verifica e que o ciclo termina sempre ao fim de um número finito de iterações.  Diz-se que se demonstrou a correcção parcial de um ciclo se se demonstrou que a CO se verifica quando o ciclo termina, assumindo que a PC se verifica no seu início.  Diz-se que se demonstrou a correcção total de um ciclo se, para além disso, se demonstrou que o ciclo termina sempre ao fim de um número finito de iterações.

Correcção parcial

A determinação de uma condição invariante CI apropriada para a demonstração é muito importante.  É que, como a CI é verdadeira no final do ciclo e a guarda G é falsa, para demonstrar a correcção parcial do ciclo basta mostrar que:
    CI e ¬G implica CO
No caso da função potência essa demonstração é simples:
CI e ¬G: r = xie 0 <= i <= n e i = n é o mesmo que
r = xi e i = n , ou seja
r = xn e i = n , que implica naturalmente
CO': r = xn
Em resumo, para provar a correcção parcial de um ciclo é necessário:
  1. encontrar uma asserção CI apropriada (note-se que a asserção V é trivialmente invariante de qualquer ciclo e não ajuda nada na demonstração de correcção parcial: é necessário que a CI seja rica em informação);
  2. demonstrar que essa asserção CI é de facto uma invariante do ciclo (ver Secção anterior); e
  3. demonstrar que CI e ¬G implica CO.

Correcção total

A correcção parcial é insuficiente.  Suponha-se a seguinte versão da função potência():
// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    // PC: n >= 0
    int i = 0;
    double r = 1.0;
    while(i != n)
        ; // instrução vazia!
    // CO': r = xn
    return r;
}
É fácil demonstrar a correcção parcial deste ciclo (porquê?)!  Mas este ciclo não termina nunca excepto quando n é 0.  I.e., de acordo com a definição dada na Secção 1.3, este ciclo não implementa um algoritmo, pois não verifica a propriedade da finitude.

A demonstração formal de terminação de um ciclo ao fim de um número finito de iterações faz-se usando o conceito de "função de limitação" (bound functions) [4], que não será abordado neste texto.  Neste contexto será suficiente a demonstração informal desse facto.  Por exemplo, na versão original da função potência()

// PC: n >= 0
// CO: potência = xn
double potência(const double x, const int n)
{
    // PC: n >= 0
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i != n) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    // CO': r = xn
    return r;
}
é evidente que, como a variável i começa com o valor zero e é incrementada de uma unidade ao fim de cada passo, fatalmente tem de atingir o valor de n (que, pela PC, é não-negativo).  Em particular é fácil verificar que o número exacto de iterações necessário para isso acontecer é exactamente n.

O passo é, tipicamente, dividido em duas partes: a acção e o progresso.  I.e., os ciclos têm tipicamente a seguinte forma:

inic
while(G) {
    acção
    prog
}
onde o progresso prog corresponde ao conjunto de instruções que garante a terminação do ciclo e a acção acção corresponde ao conjunto de instruções que garante a invariância de CI apesar do progresso.

No caso do ciclo da função potência() a acção e o progresso são:

r *= x;   // acção
++i;      // prog

Resumo

Em resumo, para demonstrar a correcção total de um ciclo:
// PC
inic
while(G) {
    acção
    prog
}
// CO
é necessário:
  1. encontrar uma asserção CI apropriada (note-se que a asserção V é trivialmente invariante de qualquer ciclo e não ajuda nada na demonstração de correcção parcial: é necessário que a CI seja rica em informação);
  2. demonstrar que essa asserção CI é de facto uma invariante do ciclo:
    1. mostrar que, admitindo a veracidade da PC antes da inicialização, a asserção CI é verdadeira depois da inicialização inic, ou seja:
      1. // PC
        inic
        // implica CI; e
    2. mostrar que, se CI for verdadeira no início do passo numa qualquer iteração do ciclo, então também o será depois do passo nessa mesma iteração, ou seja:
      1. // CI e G
        passo
        // implica CI;
  3. demonstrar que CI e ¬G implica CO; e
  4. demonstrar que o ciclo termina sempre ao fim de um número finito de iterações, para o que se raciocina normalmente em termos da inicialização inic, da guarda G e do progresso prog.

4.4.3  Melhorando a função potência()

Ao se especificar sem ambiguidades o problema que a função potência() deveria resolver fez-se uma simplificação: admitiu-se que se deveriam apenas considerar expoentes não-negativos.  Como relaxar esta exigência?  Acontece que, se o expoente tomar valores negativos, então a base da potência não pode ser nula, sob pena de o resultado ser infinitamente grande.  Então, a nova especificação será
// PC: n >= 0 oux <> 0
// CO: potência = xn
double potência(const double x, const int n)
{
    ...
}
É fácil verificar que a resolução do problema exige o tratamento de dois casos distintos, resoluveis através de uma instrução de selecção, ou, mais simplesmente, através do operador ?::
// PC: n >= 0 oux <> 0
// CO: potência = xn
double potência(const double x, const int n)
{
    const int exp = n < 0 ? -n : n;
    int i = 0;      // usada para contar o número de x já incluidos no produto.
    double r = 1.0; // usada para acumular os produtos de x.
    while(i != exp) {
        r *= x;   // o mesmo que r = r * x;
        ++i;      // o mesmo que i = i + 1;
    }
    return n < 0 ? 1.0 / r : r;
}
ou ainda, convertendo para a instrução de iteração for e eliminando os comentários redundantes
// PC: n >= 0 oux <> 0
// CO: potência = xn
double potência(const double x, const int n)
{
    const int exp = n < 0 ? -n : n;
    double r = 1.0;
    for(int i = 0; i != exp; ++i)
        r *= x;   // o mesmo que r = r * x;
    return n < 0 ? 1.0 / r : r;
}

4.4.4  Metodologia de Dijkstra

A programação deve ser uma actividade orientada pelos objectivos, como se viu atrás.  Assim, é natural que a metodologia de desenvolvimento de ciclos de Dijkstra comece por prescrever olhar com atenção para a CO do ciclo.  A noção de invariante, por outro lado, é central nesta metodologia: o desenvolvimento de um ciclo deve começar pela determinação de possíveis CI por observação da CO.  Os passos seguintes tentam integrar o desenvolvimento do código com a demonstração da sua correcção e incluem a determinação da guarda G, da inicialização inic e do passo, normalmente decomposto na acção acção e no progresso prog.  Em linhas gerais os passos a seguir são:
  1. Especificar o problema sem margem para ambiguidades: definir a PC e a CO.
  2. Tentar perceber se um ciclo é, de facto, necessário.  Este passo é muitas vezes descurado, com consequências infelizes, como se verá.
  3. Olhando para a CO, determinar uma CI para o ciclo.  A CI escolhida é uma versão enfraquecida da guarda.
  4. Escolher uma guarda G tal que CI e ¬G implica CO.
  5. Escolher uma inicialização inic de modo a garantir a veracidade da CI logo no início do ciclo, assumindo portanto que a sua PC se verifica.
  6. Escolher o passo do ciclo de modo a que a CI se mantenha verdadeira (i.e., de modo a garantir que é de facto invariante).  É muito importante que a escolha do passo garanta a terminação do ciclo: o passo é usualmente dividido num progresso prog e numa acção acção, sendo o progresso que garante a terminação do ciclo e a acção que garante que, apesar do progresso, a CI se mantém válida.
As próximas secções detalham cada um destes passos para duas funções de exemplo.

Definição da pré-condição e da condição objectivo

A pré-condição (PC) e a condição objectivo (CO) devem definir de um modo rigoroso e sem ambiguidade (tanto quanto possível) quais são os valores permitidos no início de um ciclo e quais são os valores garantidos no seu final.

A definição destas condições, no caso de uma função ou procedimento, funciona como um contrato com o programador utilizador da função ou procedimento: "se o programador utilizador desta função (procedimento) respeitar a PC, o programador fabricante desta função (procedimento) garante que depois de executada(o) a CO é verdadeira".

Se se pretender escrever uma função int somaQuadrados(const int n) que some os quadrados dos primeiros n inteiros não-negativos, então a estrutura da função, com a PC e a CO, poderá ser:

// PC: n >= 0
// CO: somaQuadrados = (S k : 0 <= k < n : k2)
int somaQuadrados(const int n)
{
    int resultado = ???;

    // CO': somaQuadrados = (S k : 0 <= k < n : k2)
    return resultado;
}

onde CO' é a condição objectivo do corpo da função antes da instrução de retorno.

Se se pretender escrever uma função int raiz(const int x) que, assumindo que x é não-negativo, devolva o maior inteiro menor ou igual à raiz quadrada de x, então a estrutura da função, com a PC e a CO, poderá ser:

// PC: x >= 0
// CO: 0 <= raizInteira <= x½ < raizInteira + 1, ou seja,
// 0 <= raizInteiraeraizInteira2 <= x < (raizInteira + 1)2
int raizInteira(const int x)
{
    int r = ???;

    // CO': 0 <=r e r2 <= x < (r + 1)2
    return r;
}

onde CO' é a condição objectivo do corpo da função antes da instrução de retorno.  Esta função, no fundo, devolve uma aproximação da raiz quadrada de um inteiro.  Note-se que é a relação x < (r + 1)2 que garante que o valor devolvido é o maior inteiro menor ou igual a x½.

(Determinando se um ciclo é necessário

A discussão deste passo será feita mais tarde, por motivos que ficarão claros a seu tempo.  Para já admite-se que sim, que ambas as funções necessitam de ciclos.)

Determinação da condição invariante e da guarda

A escolha da CI faz-se sempre olhando para a CO do ciclo.  Esta escolha é, porventura, a fase mais difícil no desenvolvimento de um ciclo.  Não existem panaceias para esta dificuldade.  É necessário usar de intuição, arte, engenhosidade, conhecimento de casos semelhantes, etc.  Mas existe uma metodologia, desenvolvida por Edsger Dijkstra [5], que funciona bem para um grande número de casos.  A ideia subjacente à metodologia é que a CI se deve obter por enfraquecimento da CO, pois pretende-se que CI e ¬G implique CO.

Por enfraquecimento entende-se a obtenção de uma condição invariante CI tal que, de entre todas as combinações de valores das variáveis que a tornam verdadeira, contenha todas as combinações de valores de variáveis que tornam verdadeira a CO.  Ou seja, o conjunto dos estados do programa que verificam a CI deve conter o conjunto dos estados do programa que verificam a CO.  Ou ainda, CO implica CI.

Apresentar-se-ão aqui apenas dois dos vários possíveis métodos para obter a CI por enfraquecimento da CO:

  1. Substituir uma das constantes da CO por uma variável com limites apropriados, obtendo-se assim a CI.  A maior parte das vezes substitui-se uma constante que seja limite de um quantificador.  A negação da guarda ¬G é depois escolhida de modo a que CI e ¬G impliqueCO, o que normalmente é conseguido escolhendo para ¬G o predicado que afirma que a variável introduzida tem exactamente o valor da constante que substituiu.
  2. Se a CO corresponde à conjunção de várias proposições, escolher parte delas para a CI e outra parte para a negação da guarda ¬G.  Por exemplo, se CO: C0 e C1 eCm-1, então poder-se-ia escolher CI: C0eC1e ... Cm-2 e ¬G: Cm-1, por exemplo.  A este método chama-se factorização da CO.
Note-se que muitas vezes só ao se desenvolver o passo do ciclo se verifica que a CI escolhida não é apropriada.  Sempre que se verificar que a CI não é apropriada, deve-se voltar atrás e escolher uma nova CI.  Finalmente, é importante que, pelas razões que se viram mais atrás, a guarda escolhida seja o mais fraca possível.
Substituição de uma constante por uma variável
Muitas vezes é possível obter uma CI para o ciclo substituindo uma constante presente na CO por uma variável criada para o efeito.  A "constante" pode corresponder a uma variável que não seja suposto ser alterada pelo ciclo, i.e., é constante do ponto de vista lógico e não de facto.

Voltando à função somaQuadrados(), é evidente que o corpo da função pode consistir num ciclo cuja condição objectivo já foi apresentada:

CO': resultado = (S k: 0 <= k < n : k2).
A CI do ciclo pode ser obtida substituindo na CO' a constante n por uma nova variável C++ i, com limites apropriados, ficando portanto:
CI: resultado = (S k: 0 <= k < i : k2) e 0 <= i <= n.
Note-se que só se pode confirmar se a escolha da CI foi apropriada depois de completado o desenvolvimento do ciclo.  Se o não for, há que voltar a este passo e tentar de novo.

Em rigor, a escolha feita para CI não corresponde ao simples enfraquecimento da CO'.  Na verdade, a introdução de uma nova variável é feita em duas etapas, que normalmente se omitem.  O primeiro passo obriga a uma reformulação da CO' de modo a reflectir a existência da nova variável.  Substitui-se a constante pela nova variável, mas força-se também a nova variável a tomar o valor da constante que substituiu, acrescentando para tal uma conjunção à CO'

CO'': resultado = (S k: 0 <= k < i : k2) ei = n,
de modo a garantir que CO'' implica CO'.

A CI é obtida da nova CO'' por enfraquecimento do termo que fixa o valor da variável i, que na CI é deixada mais "livre" que na CO''.  A CI obtida é de facto mais fraca que a CO'', pois aceita mais possíveis valores para a variável resultado do que a CO'', que só aceita o resultado final pretendido.  Os valores aceites para essa variável pela CI correspondem a valores intermédios durante o cálculo.  Neste caso correspondem a todas as possíveis somas de quadrados de inteiros positivos: desde a soma com zero termos até à soma com os n termos pretendidos.

A escolha da guarda também é simples.  Basta escolher para negação da guarda ¬G a conjunção que se acrescentou à CO' para se obter a CO''.  Dessa forma tem-se forçosamente que (CI e ¬G)equivaleCO''implica CO', como pretendido.

A escolha da G pode também ser feita observando que no final do ciclo a guarda será forçosamente falsa e a CI verdadeira (i.e., que CI e ¬G), e que pretende, nessa altura, que a CO' do ciclo seja verdadeira.  Ou seja, sabe-se que no final do ciclo

resultado = (S k: 0 <= k < i: k2) e 0 <= i <= n e ¬G
e pretende-se que
CO': resultado = (S k: 0 <= k < n : k2).
A escolha mais simples da guarda que garante a verificação da CO' é
¬G: i = n,
ou seja,
G: i <> n
Factorização da condição objectivo
Quando a CO é composta pela conjunção de várias condições, pode-se muitas vezes utilizar parte delas como negação da guarda ¬G e a parte restante da CO como CI.

Voltando à função raiz(), é evidente que o corpo da função pode consistir num ciclo cuja condição objectivo já foi apresentada:

CO': 0 <= rer2 <= x < (r + 1)2,
ou seja,
CO': 0 <= rer2 <= xe x < (r + 1)2.
Escolhendo para negação da guarda ¬G o predicado x < (r + 1)2, ou seja, escolhendo
G: (r + 1)2 <= x,
obtém-se
CI: 0 <= rer2 <= x.
Esta escolha faz com que CI e ¬G seja equivalente a CO', pelo que, se o ciclo terminar, termina com o valor correcto.

Esta escolha de CI corresponde, mais uma vez, a enfraquecer a CO', pois a CI aceita mais possíveis valores para a variável r do que a CO', que só aceita o resultado final pretendido.

Escolha da inicialização

A inicialização de um ciclo é feita normalmente de modo a, assumindo a veracidade da PC, garantir a verificação da CI da forma mais simples possível.
Função somaQuadrados()
No caso da função somaQuadrados(), a CI é
CI: resultado = (S k: 0 <= k < i : k2) e 0 <= i <= n,
pelo que a forma mais simples de se inicializar o ciclo é com as instruções
int resultado = 0;
int i = 0;
pois nesse caso a CI fica
0 = (S k: 0 <= k < 0 : k2) e 0 <= 0 <= n,
que tem sempre valor V, pois a PC indica que n >= 0.
Função raiz()
No caso da função raiz(), a CI é
CI: 0 <= rer2 <= x,
pelo que a forma mais simples de se inicializar o ciclo é com a instrução
int r = 0;
pois nesse caso a CI fica
0 <= 0 e 0 <= x,
que tem sempre valor V, pois a PC indica que x >= 0.

Determinação do passo: progresso e acção

A construção de um ciclo fica completa depois de determinado o passo, normalmente contituido pela acção acção e pelo progresso prog.  O progresso é escolhido de modo a garantir que o ciclo termina, i.e., de modo a garantir que ao fim de um número finito de repetições do passo a guarda se torna falsa.  A acção é escolhida de modo a garantir a invariância da CI apesar do progresso.
Função somaQuadrados()
No caso da função somaQuadrados(), a G é
G: i <> n
pelo que o progresso mais simples é a simples incrementação de i.  Este progresso garante que o ciclo termina ao fim de no máximo n iterações.  Ou seja, o passo do ciclo é
// CI eG: resultado = (S k : 0 <= k < i : k2) e 0 <= i <= ne i <> n,
// ou seja,
// resultado = (Sk : 0 <= k < i : k2) e 0 <= i < n
acção
++i; // i = i + 1;
// CI: resultado = (S k : 0 <= k < i : k2) e 0 <= i <= n
onde se assume a CI como verdadeira antes da acção e se pretende escolher uma acção de modo a que o seja também depois do passo, ou melhor, apesar do passo.  Usando a semântica da operação de atribuição, pode-se deduzir a condição mais fraca antes do progresso de modo a que CI seja verdadeira no final do passo:
// resultado = (S k : 0 <= k < i + 1 : k2) e 0 <= i + 1 <= n,
// ou seja, extraíndo o último termo do somatório,
// resultado = (Sk : 0 <= k < i : k2) + i2e -1 <= i < n,
++i; // i = i + 1;
// CI: resultado = (S k : 0 <= k < i : k2) e 0 <= i <= n
Falta determinar uma acção tal que
// resultado = (S k : 0 <= k < i : k2) e 0 <= i < n
acção
// resultado = (Sk : 0 <= k < i : k2) + i2e -1 <= i < n
A acção mais simples é portanto:
resultado += i * i;
pelo que o o ciclo (e a função) completo é
// PC: n >= 0
// CO: somaQuadrados = (S k : 0 <= k < n : k2)
int somaQuadrados(const int n)
{
    int resultado = 0;
    int i = 0;

    // CI: resultado = (S k : 0 <= k < i : k2) e 0 <= i <= n
    while(i != n) {
        resultado += i * i;
        ++i;
    }

    return resultado;
}

ou, substituindo pelo ciclo com a instrução for equivalente:
// PC: n >= 0
// CO: somaQuadrados = (S k : 0 <= k < n : k2)
int somaQuadrados(const int n)
{
    int resultado = 0;

    // CI: resultado = (S k : 0 <= k < i : k2) e 0 <= i <= n
    for(int i = 0; i != n; ++i)
        resultado += i * i;

    return resultado;
}

Função raiz()
No caso da função raiz(), a G é
G: (r + 1)2 <= x,
pelo que o progresso mais simples é a simples incrementação de r, que levará forçosamente à falsidade da guarda.  Ou seja, o passo do ciclo é
// CI eG: 0 <= r e r2 <= xe (r + 1)2 <= x,
acção
++r; // r = r + 1;
// CI: 0 <= rer2 <= x
onde se assume a CI como verdadeira no antes da acção e se pretende escolher uma acção de modo a que o seja também depois do passo.  Usando a semântica da operação de atribuição, pode-se deduzir a condição mais fraca antes do progresso de modo a que CI seja verdadeira no final do passo:
// CI eG: 0 <= r e r2 <= xe (r + 1)2 <= x,
acção
// 0 <= r + 1 e (r + 1)2 <= x, ou seja,
// -1 <= re (r + 1)2 <= x
++r; // r = r + 1;
// CI: 0 <= rer2 <= x
Como 0 <= r implica -1 <= r, então é evidente que neste caso não é necessária qualquer acção!  Assim, o ciclo (a função) completo é:
// PC: x >= 0
// CO: 0 <= raizInteira <= x½ < raizInteira + 1
int raizInteira(const int x)
{
    int r = 0;

    // CI: 0 <= r e r2 <= x
    while((r + 1) * (r + 1) <= x)
        ++r;

    return r;
}

A guarda do ciclo pode ser simplificada se se adiantar a variável r de uma unidade.  Ou seja:
// PC: x >= 0
// CO: 0 <= raizInteira <= x½ < raizInteira + 1
int raizInteira(const int x)
{
    int r = 1;

    // CI: 1 <= r e (r - 1)2 <= x
    while(r * r <= x)
        ++r;

    return r - 1;
}

Determinando se um ciclo é necessário

Deixou-se para o fim a discussão deste passo, que em rigor deveria ter tido lugar logo após a especificação do problema.  É esta a tendência natural do programador principiante.  Considere-se de novo a função int somaQuadrados(const int n).  Será mesmo necessário um ciclo?  Acontece que a soma dos quadrados dos primeiros n inteiros não-negativos,  pode ser expressa simplesmente por n × (n - 1) × (2 × n - 1) / 6 (a demonstração faz-se utilizando a propriedade telescópica no somatório dos cubos).  Logo, não é necessário qualquer ciclo na função, que pode ser implementada simplesmente como:
// PC: n >= 0
// CO: somaQuadrados = (S k : 0 <= k < n : k2)
int somaQuadrados(const int n)
{
    return n * (n - 1) / 2 * (2 * n - 1) / 3;
}

Exemplo 1

Pretende-se desenvolver uma função que devolva true se o valor do argumento inteiro for primo e false no caso contrário.  Um número inteiro não-negativo é primo se for apenas é divisível por 1 e por si mesmo, com excepção do 1 e do 0, que se consideram não-primos.

O primeiro passo é a escrita da estrutura da função, incluindo as respectivas PC e CO (ver Secção 4.4.4):

// PC: n >= 0
// CO: éPrimo = ((Qj : 2 <= j < n : n ÷ j <> 0) e n >= 2)
bool éPrimo(const int n)
{
    ...
}
Como abordar este problema?  Em primeiro lugar, é conveniente verificar que os valores 0 e 1 são casos particulares.  O primeiro porque é divisível por todos os inteiros positivos e portanto não pode ser primo.  O segundo porque, apesar de só ser divisível por 1 e por si próprio, não é considerado, por convenção, um número primo.  Estes casos particulares podem ser tratados com recurso a uma simples instrução condicional, pelo que se pode reescrever a função como
// PC: n >= 0
// CO: éPrimo = ((Q j : 2 <= j < n : n ÷ j <> 0) e n >= 2)
bool éPrimo(const int n)
{
    if(n == 0 || n == 1)
        return false;

    // PC': n >= 2
    bool é_primo;
    ...
    // CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
    return é_primo;
}

uma vez que, se a guarda da instrução condicional for falsa, e sabendo que n >= 0 pela PC, então forçosamente n >= 2 depois da instrução condicional.

Assim, o problema resume-se a escrever o código que leve de PC' e CO'.  Um ciclo parece ser apropriado para resolver este problema, pois para verificar se um número n é primo pode-se ir verificando se é divisível por algum inteiro entre 2 e n - 1.

A condição invariante do ciclo pode ser obtida substituindo o limite superior da conjunção (a constante n) por uma variável i criada para o efeito.  Assim, obtém-se a seguinte estrutura para o ciclo

// PC': n >= 2
int i;
bool é_primo;
inic
// CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
while(G)
    passo
// CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
O que significa esta condição invariante?  Simplesmente que a variável é_primo tem valor lógico verdadeiro se e só se não existirem divisores de n superiores a 1 e inferiores a i, sendo i um valor entre 2 e n.  Ou melhor, significa que, num dado passo do ciclo, já se testaram todos os potenciais divisores de n entre 2 e i exclusivé.

A escolha da guarda é muito simples: quando o ciclo terminar CI e ¬G deve implicar a CO.  Isso consegue-se simplesmente fazendo ¬G: i = n, ou seja, G: k <> n.  Quanto à inicialização, também é simples, pois basta atribuir 2 a i para que a conjunção não tenha qualquer termo e portanto tenha valor lógico V.  Assim, a inicialização é:

int i = 2;
bool é_primo = true;
Ou seja
// PC': n >= 2
int i = 2;
bool é_primo = true;
// CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
while(i != n)
    passo
// CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
Que progresso utilizar?  A forma mais simples de garantir a terminação do ciclo é simplesmente incrementar i.  Dessa forma, como i começa com valor 2 e n >= 2 pela pré-condição, a guarda torna-se falsa, e o ciclo termina, ao fim de exactamente n - 2 iterações do ciclo.  Ou seja
// PC': n >= 2
int i = 2;
bool é_primo = true;
// CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
while(i != n) {
    acção
    ++i;
}
// CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
Finalmente, falta determinar a acção a executar para garantir a veracidade da CI apesar do progresso.  No início do passo admite-se que a CI é verdadeira e sabe-se que a G é verdadeira, ou seja:
CI e G: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n ei <> n
ou seja,
(é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n
Por outro lado, no final do passo deseja-se que a CI seja verdadeira.  Logo, o passo com as respectivas asserções é:
// Aqui admite-se que CI eG: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n
acção
++i;
// Aqui pretende-se que: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
Antes de determinar a acção, é conveniente verificar qual a pré-condição mais fraca do progresso que é necessário impor para que, depois dele, se verifique a CI do ciclo.  Usando a transformação de variáveis correspondente à atribuição i = i + 1 (equivalente a ++i), chega-se a:
// Aqui admite-se que CI eG: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n
acção
// (é_primo = (Q j : 2 <= j < i + 1 : n ÷ j <> 0)) e 2 <= i + 1 <= n, ou seja,
// (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i <= n - 1, ou seja,
// (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n.
++i;
// Aqui pretende-se que: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
Assim sendo, a acção deve ser tal que:
// (é_primo = (Qj : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n
acção
// implica (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n.
É evidente, então, que a acção pode ser simplesmente:
é_primo = (é_primo && n % i != 0);
onde os parênteses são dispensáveis dadas as regras de precedência dos operadores em C++ (Secção 2.5.6, Tabela Precedência de operadores).

A correcção da acção determinada pode ser verificada facilmente calculando a respectiva pré-condição mais fraca:

// (é_primo en ÷ i <> 0 = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n, ou seja,
// (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 1 <= i < n.
é_primo = é_primo && n % i != 0;
// implica (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n.
e observando que CI e G leva forçosamente à sua verificação:
CI e G: (é_primo = (Qj : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n
implica (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 1 <= i < n
Escrevendo agora o ciclo completo tem-se:
// PC': n >= 2
int i = 2;
bool é_primo = true;
// CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
while(i != n) {
    é_primo = é_primo && n % i != 0;
    ++i;
}
// CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
Reforço da guarda
A observação atenta deste ciclo revela que a variável é_primo, se alguma vez se tornar falsa, nunca mais deixará de o ser.  Tal deve-se a que F é o elemento neutro da conjunção.  Assim, é evidente que o ciclo poderia terminar antecipadamente, logo que essa variável tomasse o valor F.  Ou seja, o ciclo só deveria continuar enquanto é_primo e i <> n.  Será que esta nova guarda G: é_primo e i <> n, reforçada com uma conjunção, é mesmo apropriada?

Em primeiro lugar é necessário demonstrar que, quando o ciclo termina, se atinge a CO'.  Ou seja, será que CI e ¬G implica CO'?  Neste caso

CI e ¬G: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n eé_primo ou i = n)
Considere-se o último termo da conjunção, ou seja, a disjunção (¬é_primo ou i = n).  Quando o ciclo termina pelo menos um dos termos da disjunção é verdadeiro.

Suponha-se que ¬é_primo é V.  Então ¬(Q j : 2 <= j < i : n ÷ j <> 0) é verdadeira, ou seja, (E j : 2 <= j < i : n ÷ j = 0) é verdadeira, pelo que (E j : 2 <= j < n : n ÷ j = 0) também o é (simplesmente se acrescentaram termos à disjunção) e portanto (Q j : 2 <= j < n : n ÷ j <> 0) é falsa.  Ou seja, CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0) é verdadeira (representa a tautologia F = F)!

Suponha-se que i = n.  Este caso é idêntico ao caso sem reforço da guarda.

Logo, a CO' é de facto atingida * em qualquer dos casos.

Quanto ao passo (acção e progresso), é evidente que o reforço da guarda não altera a sua validade.  No entanto, é instrutivo determinar de novo a acção do passo.

A acção deve garantir a veracidade da CI apesar do progresso.  No início do passo admite-se que a CI é verdadeira e sabe-se que a G é verdadeira, ou seja:

CI e G: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n eé_primo e i <> n
ou seja,
(é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i < n e é_primo
ou ainda,
(Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i < ne é_primo
Por outro lado, no final do passo deseja-se que a CI seja verdadeira.  Logo, o passo com as respectivas asserções é:
// Aqui admite-se que CI eG: (Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i < ne é_primo.
acção
++i;
// Aqui pretende-se que: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n.
Antes de determinar a acção, é conveniente verificar qual a pré-condição mais fraca do progresso que é necessário impor para que, depois dele, se verifique a CI do ciclo.  Usando a transformação de variáveis correspondente à atribuição i = i + 1 (equivalente a ++i), chega-se como anteriormente a:
// Aqui admite-se que CI eG: (Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i < ne é_primo.
acção
// (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n,
++i;
// Aqui pretende-se que: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n.
Assim sendo, a acção deve ser tal que:
// (Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i < n e é_primo
acção
// implica (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n.
É evidente, então, que a acção pode ser simplesmente:
é_primo = n % i != 0;
Note-se que a acção se simplificou face à acção no ciclo com a guarda original.  A alteração da acção pode ser percebida observando que, sendo a G sempre verdadeira no início do passo do ciclo, a variável é_primo tem aí sempre o valor V, pelo que a acção antiga tinha uma conjunção com a veracidade.  Como V e P é o mesmo que P, pois V é o elemento neutro da conjunção, a conjunção pode ser eliminada.

A correcção da acção determinada pode ser verificada facilmente calculando a respectiva pré-condição mais fraca:

// (n ÷ i <> 0 = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n, ou seja,
// V = (Q j : 2 <= j < i : n ÷ j <> 0) e 1 <= i < n, ou seja,
// (Q j : 2 <= j < i : n ÷ j <> 0) e 1 <= i < n.
é_primo = n % i != 0;
// implica (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0) e n ÷ i <> 0) e 1 <= i < n.
e observando que CI e G leva forçosamente à sua verificação:
CI e G: (Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i < n e é_primo
implica (Q j : 2 <= j < i : n ÷ j <> 0) e 1 <= i < n
Escrevendo agora o ciclo completo tem-se:
// PC': n >= 2
int i = 2;
bool é_primo = true;
// CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
while(é_primo && i != n) {
    é_primo = n % i != 0;
    ++i;
}
// CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
* Se não lhe pareceu claro lembre-se que (A ou B) implica C é o mesmo que A implica C e B implica C.  Lembre-se ainda que A e (B ou C) é o mesmo que (A e B) ou (A eC).
Simplificações finais
Convertendo para a instrução for e inserindo o ciclo na função tem-se:
// PC: n >= 0
// CO: éPrimo = ((Q j : 2 <= j < n : n ÷ j <> 0) e n >= 2)
bool éPrimo(const int n)
{
    if(n <= 1)
        return false;

    // PC': n >= 2

    // CI: (é_primo = (Q j : 2 <= j < i : n ÷ j <> 0)) e 2 <= i <= n
    bool é_primo = true;
    for(int i = 2; é_primo && i != n; ++i)
        é_primo = n % i != 0;

    // CO': é_primo = (Q j : 2 <= j < n : n ÷ j <> 0)
    return é_primo;
}

Mas uma observação atenta da função revela que ainda pode ser simplificada (do ponto de vista da sua escrita) para
// PC: n >= 0
// CO: éPrimo = ((Q j : 2 <= j < n : n ÷ j <> 0) e n >= 2)
bool éPrimo(const int n)
{
    if(n <= 1)
        return false;

    // PC': n >= 2

    // CI: (Q j : 2 <= j < i : n ÷ j <> 0) e 2 <= i <= n
    for(int i = 2; i != n; ++i)
        if(n % i == 0)
            return false;
    return true;
}

Este último formato é muito comum em programas escritos em C ou C++.  A demonstração da correcção de ciclos incluindo saídas antecipadas no seu passo é um pouco mais complicada.  Veja-se a resolução do exercício 5.a) da Aula 8 para uma versão um pouco mais eficiente desta função e para uma demonstração de correcção com saída do ciclo antecipada dentro do passo.

Exemplo 2

Suponha-se que se pretende desenvolver 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 inteira do dividendo pelo divisor e os guarde em variáveis externas à função (usando passagem de argumentos por referência), sem que se possa fazer uso dos operadores *, / e % do C++ nem tão pouco dos operadores de deslocamento bit-a-bit.  A estrutura do procedimento é (ver Secção 4.3, O resto da divisão):
// PC: dividendo >= 0 e divisor > 0
// CO: 0 <= r < divisor e dividendo = q × divisor + r
void divisãoInteira(const int dividendo, const int divisor, int& q, int& r)
{
    ...
}
Repare-se que a CO é uma definição de divisão inteira!  I.e., não só o quociente (q) multiplicado pelo divisor e somado do resto (r) tem de resultar no dividendo, como o resto tem de ser não-negativo e menor que o divisor (senão não estaria completa a divisão!), existindo apenas uma solução nestas circunstâncias.

Como é evidente a divisão tem de ser conseguida através de um ciclo.  Qual a sua CI?  Neste caso, como a CO é  a conjunção de três termos

CO: 0 <= r e r < divisore dividendo = q × divisor + r
 a solução passa por factorizar a CO.

Mas quais das proposições usar para ¬G e quais usar para CI?  Um pouco de experimentação e alguns falhanços levam a que se perceba que a ¬G deve corresponder ao segundo predicado da conjunção, ou seja

G: r >= divisor
CI: dividendo = q × divisor + r e 0 <= r
Dada esta escolha, qual a forma mais simples de inicializar o ciclo?  É evidente que é fazendo
r = dividendo;
q = 0;
pois substituindo na CI obtém-se
CI: dividendo = 0 × divisor + dividendo e 0 <= dividendo
e a PC garante que dividendo é não negativo.

Que progresso utilizar?  O progresso deve ser tal que a guarda se torne falsa ao fim de um número finito de iteração do ciclo.  Neste caso é claro que basta diminuir sempre o valor do resto para que isso aconteça.  A forma mais simples de o fazer é decrementá-lo:

--r;
Para que a CI seja de facto invariante, há que escolher uma acção tal que:
// Aqui admite-se que CI eG: dividendo = q × divisor + re 0 <= r e r >= divisor, ou seja,
// dividendo = q × divisor + r e r >= divisor.
acção
--r;
// Aqui pretende-se que CI: dividendo = q × divisor + r e 0 <= r
Antes de determinar a acção, verifique-se qual a pré-condição mais fraca do progresso que leva à veracidade da CI depois do progresso:
// dividendo = q × divisor + r - 1 e 0 <= r - 1
--r;
// Aqui pretende-se que CI: dividendo = q × divisor + r e 0 <= r
A acção deve ser tal que:
// dividendo = q × divisor + r e r >= divisor.
acção
// implica dividendo = q × divisor + r - 1 e 1 <= r
É evidente que, em geral, não existe nenhuma acção que o consiga fazer sem alterar a variável resto!  Uma observação mais cuidada das razões leva a compreender que o progresso não pode fazer o resto decrescer de 1 em 1, mas de divisor em divisor!  Se assim for:
// dividendo = q × divisor + r e r >= divisor.
acção
// dividendo = q × divisor + r - divisor e 0 <= r - divisor, ou seja,
// dividendo = (q - 1) × divisor + r e divisor <= r.
r -= divisor;
// Aqui pretende-se que CI: dividendo = q × divisor + r e 0 <= r.
Donde a acção pode ser simplesmente
++q;
Tal pode ser confirmado determinando a pré-condição mais fraca da acção:
// dividendo = (q + 1 - 1) × divisor + r e divisor <= r, ou seja,
// dividendo = q × divisor + r e divisor <= r.
++q;
// dividendo = (q - 1) × divisor + r e 0 <= r.
e observando que CI e G implica essa pré-condição.

Transformando o ciclo num ciclo for e colocando no procedimento:

// PC: dividendo >= 0 e divisor > 0
// CO: 0 <= r < divisor e dividendo = q × divisor + r
void divisãoInteira(const int dividendo, const int divisor, int& q, int& r)
{
    // CI: dividendo = q × divisor + r e 0 <= r
    for(q = 0, r = dividendo; r >= divisor; r -= divisor)
        ++q;
}
Que também é comum ver escrito como:
// PC: dividendo >= 0 e divisor > 0
// CO: 0 <= r < divisor e dividendo = q × divisor + r
void divisãoInteira(const int dividendo, const int divisor, int& q, int& r)
{
    // CI: dividendo = q × divisor + r e 0 <= r
    for(q = 0, r = dividendo; r >= divisor; r -= divisor, ++q)
        ;
}
onde se colocou a acção no progresso do for.

Note-se que a vírgula usada para separar o progresso e a acção do ciclo é o operador de sequenciamento do C++.  Esse operador garante que os seus primeiro operando é calculado antes do segundo operando, e tem como resultado o valor do segundo operando.  Por exemplo, as instruções:

int n;
n = (1, 2, 3, 4);
colocam o valor 4 na variável n.

4.4.5  Exercícios

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

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

2.  Implemente versões iterativa e recursiva de funções que devolvam o n-ésimo termo da sucessão de Fibonacci.  Esta sucessão é definida recorrentemente por:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2, se n > 1
Teste ambas as funções utilizando o programa dado abaixo.  Dê o nome fib ao ficheiro executável. #include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>

using namespace std;

long int fibonacciRecursiva(int n)
{
    // a completar...
}

long int fibonacciIterativa(int n)
{
    // a completar...
}

/*
   Para testar este programa deve executá-lo na janela de comandos
   com um argumento inteiro positivo (de preferência menor que 35) ou
   em modo de depuração dando a instrução: set arg 30 (ou outro número
   inteiro positivo menor que 35) antes da instrução r(un).
*/
int main(int numero_de_argumentos, char* argumentos[])
{
    if(numero_de_argumentos != 2) {
        cerr << "Utilização: " << argumentos[0]
             << " inteiro" << endl;
        return 1;
    }

    int n = atoi(argumentos[1]) + 1;

    cout.precision(6);

    for(int i = 0; i != n; i++) {
        cout << "Fibonacci(" << i << ") = "
             << fibonacciIterativa(i);

        const int repeticoesIterativa = 1000000 / (i + 1) + 1;
        const int repeticoesRecursiva =
            int(1000000 / pow((1.0 + sqrt(5)) / 2.0, i)) + 1;

        clock_t t = clock();
        for(int j = 0; j != repeticoesRecursiva; ++j)
            fibonacciRecursiva(i);
        t = clock() - t;
        cout << ", recursiva "
             << t / double(CLOCKS_PER_SEC) / repeticoesRecursiva * 1000.0
             << " ms";

        t = clock();
        for(int j = 0; j != repeticoesIterativa; ++j)
            fibonacciIterativa(i);
        t = clock() - t;
        cout << ", iterativa "
             << t / double(CLOCKS_PER_SEC) / repeticoesIterativa * 1000.0
             << " ms" << endl;
    }
}

Para ver um gráfico dos tempos que cada função demora a fazer os seus cálculos, dê o comando que se segue (uma só linha) numa consola do Linux (tem de ter o gnuplot instalado!):
fib 37 | awk '{print $5, $8}' > ! tempos ; echo "set xlabel 'n'; set ylabel 'tempo [ms]'; set xtics 1; set grid; set logscale y; set ytics 0.0001,1.584893192,10000; plot 'tempos' using 2 title 'iterativa' with lines, 'tempos' using 1 title 'recursiva' with lines; pause -1" > ! g ; gnuplot g ; rm -f g tempos
3.  Complete o procedimento void leLinha(string& linha), do programa abaixo, de modo a que permita ao utilizador inserir caracteres até que este introduza o caracter '\n', que corresponde a um fim-de-linha.  Esses caracteres devem ser guardados na cadeia de caracteres passada por referência, que deve ser limpa antes da leitura começar.

Para resolver este problema precisar de saber que:

4.  Desenvolva um programa que calcule a soma de n números introduzidos pelo utilizador (o valor n também é introduzido pelo utilizador).

5.a)  Sabendo que todos os números primos maiores que 3 se podem escrever na forma 6 × m - 1 ou 6 × m + 1 (mas nem todos os números desta forma são primos!) uma função que devolva true se o valor do argumento inteiro for primo e false no caso contrário.

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

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

6.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
****
*  *
*  *
****
6.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
********
**    **
* *  * *
*  **  *
*  **  *
* *  * *
**    **
********
7.  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.
8.  Escreva uma função que calcule a multiplicação inteira de dois números recorrendo apenas aos operadores aritméticos + e -.  Qual é o progresso mais simples?  E o mais eficiente?

9.  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'.

10.  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.

11.  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.5  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]. *

[5]  Edsger Dijkstra, "A Discipline of Programming", Prentice Hall, 1976. *

[6]  Bjarne Stroustrup, "The C++ Programming Language", terceira edição, Addison-Wesley, Reading, Massachusetts, 1998. *

* Existe na biblioteca do ISCTE.