Erros mais Frequentes no Problema 1

Este documento apresenta os erros mais comuns cometidos nas resoluções do Problema 1.  Os erros são apontados através de troços de código extraídos dos programas realizados pelos alunos.  Esses troços não são identificados.  Pretende-se ilustrar os erros cometidos para que possam ser corrigidos em trabalhos posteriores.

Muitos dos erros encontrados mostram claramente que os alunos não leram as folhas teóricas, o que é lamentável.  Chama-se também a atenção para que é altamente recomendável os alunos imitarem o estilo de expressão C++ usado ao longo desta disciplina, desde as folhas teóricas às aulas práticas.

Este documento está dividido em secções correspondentes a tipos genéricos de erros.

1  Expressão escrita

É comum ouvir-se entre os alunos e encontrar nos relatórios a expressão "é um void",  querendo com isso os alunos referir-se a um procedimento.  Um procedimento não é "um void".  Um procedimento tem tipo de devolução void, o que significa que não devolve nada ao retornar.  Um procedimento não calcula, faz.

Não é correcto dizer "tipo de retorno" ou "retorna um dado valor".  O verbo "retornar" significa "voltar ao ponto de onde se partiu", que é exactamente o que sucede no final de uma rotina, seja ela função ou procedimento.  Pelo contrário, a palavra "devolver" significa (embora não seja a acepção mais comum) "dizer em resposta", que é exactamente o que sucede no final de uma função, mas não no final de um procedimento.

Em português é correcto dizer "retornar a algum lado" ou simplesmente "retornar", mas é incorrecto dizer "retornar alguma coisa".  Por outro lado, é perfeitamente correcto dizer "devolver alguma coisa".  Assim, deve-se dizer:

  1. Um procedimento, ao terminar, retorna (ao ponto onde foi invocado).
  2. Uma função, ao terminar, retorna (ao ponto onde foi invocada) devolvendo um dado valor.

2  Relação entre conceitos e ferramentas do C++

É importante perceber-se que há conceitos diferentes em programação que têm idêntica expressão na linguagem C++.  Um exemplo é o da distinção entre os conceitos de função e procedimento, que a linguagem C++ não distingue, possuindo apenas o conceito de função.  Há inúmeros exemplos semelhantes a este, quase todos com a complicação adicional de a ferramenta da linguagem C++ ter um nome que induz em erro, pois é o mesmo que um dos conceitos entre muitos que podem ser representados à sua custa.

A linguagem C++ tem uma ferramenta, os comentários, que podem servir para representar dois conceitos distintos: os comentários (propriamente ditos) e a documentação.  Um comentário serve para explicar uma parte pouco clara do código.  A documentação serve para colocar a especificação dos módulos constituintes de um programa.  Ambos se representam à custa dos comentários C++, embora convencionalmente se introduza uma distinção gráfica:

// ... ou /* ... */
Comentários (de linha e de bloco).
/// ... ou /** ... */
Documentação (em linha ou em bloco).

Assim, enquanto é desejável que o código seja escrito de uma forma tão clara que os comentários sejam dispensáveis (pior que um comentário acerca de um pedaço de código perfeitamente óbvio só mesmo um comentário errado!), a documentação é sempre imprescindível.

A documentação consiste sempre pelo menos numa primeira parte com uma descrição informal do objectivo do módulo e da sua especificação formal, na forma de uma pré-condição e uma condição objectivo, pelo menos no caso da rotinas.

3  C-zices

Por razões desconhecidas, alguns alunos parecem procurar activamente no C++ as heranças pesadas da velha linguagem C.  Seguem-se alguns exemplos.

Deve-se usar sempre a versão C++ dos operadores lógicos (booleanos).  Isto é, deve-se usar and, or e not, e não os correspondentes &&, || e !, pois são muito menos claros.  É verdade que alguns compiladores não suportam os primeiros.  Nesse caso a melhor solução é colocar as seguintes linhas no início do programa:

#define and &&
#define or ||
#define not !

Também é comum encontrar a utilização de velhas convenções do C, tais como colocar os nomes de constantes em maiúsculas.  Essa convenção em C era necessária, pois nas suas primeiras versões não existia o conceito de constante, obtendo-se um efeito semelhante à custa das chamadas macros.  Em C++ a utilização de macros é muito mais restrita.  O C++ suporta directamente o conceito de constante.  Por isso devem-se usar para os nomes das constantes exactamente a mesma convenção que no caso das variáveis.  Por exemplo, em vez de

int const MAXFALHANCOS = 6;//numero maximo de erros permitidos.

deve-se escrever

int const máximo_de_falhancos = 6;

ou pelo menos

int const maximo_de_falhancos = 6;

enquanto os compiladores não lidarem bem com caracteres acentuados.

Nota:  Neste documento usam-se caracteres acentuados nos identificadores para aumentar a clareza da nomenclatura usada.

Finalmente, com alguma frequência surgem casos de utilização de variáveis booleanas como se tivessem valores inteiros, ou a utilização de variáveis inteiras para guardar valores booleanos.  Por exemplo:

/** verifica se a letra pretendida se encontra na string */
bool verificaletra(char x, string palavra)
{
    bool aux=0;
    for (string::size_type i = 0; i !=palavra.length(); ++i)
    {
        if(palavra[i] == x)
            aux = aux + 1;
    }
    return aux;
}

Deve-se sempre seleccionar o tipo mais restrito que permita representar os valores em causa (mas, no caso dos inteiros, fugir à utilização de short int ou mesmo char face ao mais claro int).  Por exemplo, se se pretender guardar o número de falhanços ocorridos durante o jogo da forca, escolher-se-á para a variável o tipo int, e não double.  Aparte questões de precisão, uma variável double também resolveria o problema, mas permitindo guardar valores que não inteiros, o programador fica mais à mercê dos seus próprios erros.  Mais tarde se verá que muitas vezes a solução passa por definir novos tipos de dados (Tipos Abstractos de Dados) especializados para os valores a guardar.

Uma versão mais apropriada do código seria:

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    bool contém = falso;

    for(string::size_type i = 0; i != cadeia.size(); ++i)
        contém = contém or cadeia[i] == caractere;

    return contém;
}

ou mesmo,

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    bool contém = falso;

    for(string::size_type i = 0; i != cadeia.size(); ++i)
        if(cadeia[i] == caractere)
            contém = true;

    return contém;
}

ou ainda,

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    for(string::size_type i = 0; i != cadeia.size(); ++i)
        if(cadeia[i] == caractere)
            return true;

    return false;
}

4  Modularização

A modularização é o mecanismo de redução de complexidade por excelência usado na programação.  Uma boa modularização é meio caminho andado para o sucesso.

4.1  Repetições

Um erro comum foi a definição de duas rotinas rigorosamente iguais, excepto no seu nome e no nome dos seus parâmetros, sem que os alunos tenham compreendido que ambas as rotinas podiam e deviam ser vistas como  uma só rotina dedicada a resolver o caso genérico.  Como exemplo, numa das resoluções foram definidas três funções devolvendo valores booleanos:

bool letraExiste(char letra_escolhida, string palavra_a_advinhar)
{
    for(string::size_type i = 0; i != palavra_a_advinhar.length(); ++i)
        if(palavra_a_advinhar[i] == letra_escolhida)
            return true;

    return false;
}

bool letraJaFoiEscolhida(char letra, string letras_disponíveis)
{
    for(string::size_type i = 0; i != letras_disponíveis.length(); ++i)
        if(letras_disponíveis[i] == letra) 
            return false;

    return true;
}

bool jogoEstáGanho(string palavra_escondida)
{
    for(string::size_type i = 0; i != palavra_escondida.length(); ++i)
        if(palavra_escondida[i] == '-')
            return false;

    return true;
}

Como é evidente, basta um destas funções para resolver o problema em qualquer dos casos:

bool estáContidoEm(char caractere, string cadeia)
{
    for(string::size_type i = 0; i != cadeia.length(); ++i)
        if(cadeia[i] == caractere)
            return true;

    return false;
}

Note-se como o nome da função e os nomes dos parâmetros permitem perceber imediatamente que a função tem utilização genérica.

4.2  Dois-em-um ou "coisos"

Uma rotina deve ter uma função bem definida.  Veja-se, por exemplo, a seguinte definição:

bool éLetra(char caractere)
{
    for(string::size_type i = 0; i != letras.length(); ++i)
        if(letras[i] == letra)
            return true;

    cout << "Deve apenas inserir letras!"<< endl;
    return false;
}

Esta função começa por ser inútil, pois existe uma função isalpha() na biblioteca do C++ que faz o mesmo efeito.  Ainda que a sua definição fizesse sentido, a função tem um erro grave.  Ao contrário do que o nome indicia, não se limita a devolver true se o argumento for uma letra e false no caso contrário: se não for uma letra surgirá uma mensagem no ecrã!  Assim, um nome apropriado para a função seria éLetraEAssinalaErro()...  Embora o nome ficasse mais correcto, continuariam a existir dois problemas graves relacionados:

  1. Uma função devolve um valor e não faz nada, ou seja, não altera o estado do programa.  Um procedimento faz alguma coisa, i.e., altera o estado do programa, e não devolve nada.  A função em causa é função porque devolve alguma coisa e é procedimento porque altera (ou pode alterar) o estado do programa (altera pelo menos o canal de saída cout, através da inserção nele realizada, e o ecrã).  Ou seja, é aquilo a que se chama uma função com efeitos laterais, e que nesta disciplina merece o nome de "coiso".  Os coisos são muito problemáticos!  Isso é claro se se tentar imaginar uma futura utilização desta função.  Com a escrita em cout, torna-se impossível reutilizar esta rotina em contextos em que o caractere não tenha sido dada pelo utilizador, pois nesse caso a mensagem de erro não faria qualquer sentido.
  2. Como um módulo deve ter uma função bem definida, a utilização da conjunção "e" no seu nome indicia claramente que a rotina deveria ser partida em duas.  Uma para verificar se um caractere é letra, outra para assinalar um erro ao utilizador se este não introduzir uma letra (esta última talvez não precisasse de ser uma rotina independente).

Estes dois problemas são maneiras diferentes de ver o problema geral do "dois em um":  um módulo com duas funções bem (?) definidas...  Veja-se por exemplo a seguinte rotina (coiso):

string letraSubstituídaNaPalavraOculta(char const letra, 
                                       string const& palavra,
                                       string& palavra_oculta)
{
    for(string::size_type i = 0; i != palavra.length(); ++i)
        if(palavra[i] == letra)
            palavra_oculta[i] = letra;

    return palavra_oculta;
}
 

Esta rotina regista na palavra oculta os acertos de uma letra na palavra.  Por isso recebe a palavra oculta por referência, de modo a que quaisquer alterações que essa palavra sofra internamente à rotina se reflictam no respectivo parâmetro.  O problema é que, além disso, devolve a palavra oculta já com os acertos registados!  A rotina deveria ser transformada ou numa função ou num procedimento.  Por exemplo:

void registaAcertosNaPalavraOculta(char const letra, 
                                   string const& palavra,
                                   string& palavra_oculta)
{
    for(string::size_type i = 0; i != palavra.length(); ++i)
        if(palavra[i] == letra)
            palavra_oculta[i] = letra;
}
 

Regra geral:

  1. Se uma rotina receber algum argumento por referência (não constante) ou se alterar alguma variável global (cin, cout, cerr, e clog são variáveis globais), então diz-se que tem efeitos laterais e, por isso, só deve ser um procedimento.
  2. Se uma rotina devolver alguma coisa, então não deve ter qualquer efeito lateral, dizendo-se uma função.
  3. Como qualquer regra geral, pode haver (e há) excelentes razões para a violar.  Mas pense várias vezes antes de o fazer.

5  Convenções gráficas

5.1  Grafismo de nomes de entidades

É conveniente a utilização de convenções para a forma de grafar os nomes das entidades usadas num programa, de acordo com a sua categoria.  A utilização de convenções permite que a leitura de código escrito por outrem seja facilitada.  Afinal, programar é comunicar simultaneamente com o compilador e com programadores humanos, pelo que a utilização de convenções diferentes da usual só poderá levar a que o leitor (humano) do código tenha de perder tempo lutando contra um aspecto gráfico diferente, em vez de dedicar a sua atenção ao essencial: o conteúdo.  Fazendo uma analogia com a língua portuguesa, diria que usar uma convenção pouco habitual na pontuação, por exemplo, como o Saramago faz, pode ter intuitos artísticos, mas claramente não facilita a comunicação.

Assim, ao longo desta disciplina usa-se e recomenda-se vivamente a utilização por parte dos alunos das seguintes convenções para os nomes das entidades:

5.2  Indentação e espaços

Tal como em português, também em C++ existe uma forma usual de organizar graficamente um programa.  A utilização criteriosa de espaços e linhas em branco, por exemplo, podem contribuir em muito para clarificar a estrutura de um programa.  

Ao efeito obtido pela colocação de espaços no início das linhas chama-se indentação, pois a margem esquerda do programa fica como que marcada por uma mordidela (marcas dos dentes).  A indentação é fundamental para identificar instruções relacionadas e separar visualmente as instruções controladas por instruções de iteração ou selecção, por exemplo.

Está nos plano desta disciplina produzir um documento concentrando todas as regras de organização gráfica de um programa.  Por enquanto, e à falta desse documento, recomenda-se a observação atenta de todos os exemplos de código dados na disciplina.  E recomenda-se vivamente que usem o comando <ctrl-c s> do XEmacs para que o código seja indentado correctamente.

6  Nomenclatura

A atribuição de nomes às entidades (rotinas, variáveis, constantes, tipos, etc.) usadas num programa não é um problema menor.  Muitos alunos tendem a escolher nomes razoavelmente arbitrários argumentando que (a) os nomes não são fundamentais e (b) a posteriori, quando houver tempo, alterarão os nomes para que "agradem mais aos docentes da disciplina".

É verdade que a correcção de um programa, no sentido em que produz os resultados desejados, não depende dos nomes usados para as entidades.  Mas é um argumento falacioso.  Quando se escreve um programa em C++ o objectivo não é simplesmente conseguir que ele produza os resultados pretendidos.  Um programa em C++ é um texto escrito numa linguagem específica e limitada, o C++.  Mas esse texto tem como objectivo não apenas ser traduzido para linguagem máquina por um compilador imbecil, para quem os nomes das entidades nada importam, mas fundamentalmente comunicar com leitores humanos.  Esses leitores podem ser o próprio programador (o aluno), elementos da sua equipa (do seu grupo de alunos) ou mesmo com o docente da disciplina.  É muito importante, por isso, que o programa seja possível de ler com facilidade.

Existem algumas convenções e boas regras que se devem usar ao atribuir nomes a entidades.  Essas convenções e regras são seguidas em todo o material disponível nesta disciplina (salvo ocasionais falhas) e são apresentadas explicitamente nas folhas teóricas.  

Os nomes devem ser escolhidos de forma a tornar o código tão legível quanto possível em português, ou pelo menos numa língua natural dominada por todos os potenciais programadores envolvidos no projecto.  No caso desta disciplina trata-se do português, muito embora os alunos também possam usar inglês, desde que o façam de forma consistente.

Uma forma simples de atribuir nomes a entidades de um programava é responder da forma mais sintética possível mas sem perda de precisão (nem pontapés na gramática ou abreviaturas) às perguntas abaixo, retirando a parte a sublinhado das respostas:

As variáveis ou constantes têm usualmente por nome um sintagma nominal, embora normalmente extirpado do artigo inicial, por exemplo, int número_de_alunos, int palavra_adivinhada, Carro carro e Barco barco (mas é defensável usar como nome, por exemplo, o_número_de_alunos).  Quando as variáveis não guardam nada de especial, é típico usar-se um nome parecido com o respectivo tipo.  Por exemplo, int inteiro, vector<int> vector_de_inteiros e Racional racional (este último exemplo ajuda a compreender a razão para a convenção gráfica proposta na secção anterior!).  Nestes casos também é usual usar-se nomes muito abreviados, na tradição da matemática, tais como i, v ou r.  No entanto, há que resistir à tentação de dar este tipo de nomes quando as variáveis guardarem algo de mais concreto, por exemplo, posições_da_letra_na_palavra, é muito mais claro que v.

As variáveis booleanas têm usualmente por nome uma proposição, i.e., uma frase que pode ser verdadeira ou falsa, por exemplo, bool existe_um_divisor, bool jogo_foi_abortado, bool janela_está_aberta, a maior parte das vezes também sem o artigo inicial (mas é defensável usar como nome, por exemplo, bool a_janela_está_aberta).  

As funções têm o mesmo nome (à parte variações do grafismo) que variáveis que guardem o valor por elas devolvido, por exemplo, bool existe_ímpar_no_vector e bool denominador_do_racional vs. bool existeÍmparEm(vector<int> vector) e bool denominadorDe(Racional racional).  

Os parâmetros das funções e procedimentos fazem parte da das respostas às perguntas propostas atrás, mas não devem fazer parte do nome.  É o que se passa no caso int denominadorDe(Racional racional), onde a resposta à pergunta sugerida seria "Esta função devolve o denominador de um Racional".  Desta resposta o sintagma nominal "um Racional" transforma-se num parâmetro da função.  Por vezes a sintaxe do C++ obriga a alterar a ordem natural das palavras.  É o caso de uma função que indica se um inteiro está contido num vector (de inteiros), que se traduz para bool estáContidoEm(int inteiro, Vector<int> vector), onde o sujeito da frase teve de se transformar no primeiro parâmetro da função (os complementos da frase correspondem normalmente a parâmetros).  Uma alternativa seria dizer que a função indica se um vector (de inteiros) contém um inteiro,  que se traduz para bool contém(Vector<int> vector, int inteiro).  A frase só encontra uma tradução verdadeiramente natural quando a função é membro da classe C++ Vector<int>, pois nesse caso fica bool vector<int>::contém(int inteiro), sendo a variável implícita (o vector) o sujeito activo da frase.

Os procedimentos têm sempre como nome uma frase imperativa, pelo que devem começar por um verbo no imperativo (segunda pessoa do singular).  Presume-se que a ordem é dada à entidade encarregue de executar o programa, por exemplo, o processo ou o computador.  Por exemplo, se a resposta à pergunta sugerida acima for "Este procedimento mostra um racional no ecrã", o procedimento será void mostraNoEcrã(Racional racional), onde foi necessário fazer um ajustamento na ordem das palavras.  No caso das classes a ordem presume-se dada à variável através da qual o procedimento foi invocado.  Por exemplo, 

FilaDeString fila_de_BI; 

...

fila_de_BI.tiraPrimeiroItem();

Deve-se ler "fila de bilhetes de identidade, tira o (teu) primeiro item!"

7  Expressões idiomáticas em C++

Qualquer linguagem tem as suas expressões idiomáticas.  A linguagem C++ não é excepção.  As expressões idiomáticas têm a vantagem de ser conhecidas por todos os que conhecem a linguagem e de permitirem exprimir ideias recorrentes de uma forma condensada.  Seguem-se alguns exemplos.

É comum encontrar-se código com o seguinte aspecto:

if(contém(palavra_a_adivinhar, letra) == true)
   
...

if(contém(palavra_a_adivinhar, letra) != false)
   
...

ou

if(contém(palavra_a_adivinhar, letra) != true)
   
...

if(contém(palavra_a_adivinhar, letra) == false)
   
...

Este código não está incorrecto, mas a forma idiomática de o escrever é

if(contém(palavra_a_adivinhar, letra))
   
...

ou

if(not contém(palavra_a_adivinhar, letra))

Há três razões para isso.

A primeira razão é que estas últimas formas são mais sintéticas do que as primeiras.  

A segunda razão é que as últimas versões podem ler em português como

"se a palavra a adivinhar contém a letra, então..."

ou

"se a palavra a adivinhar não contém a letra, então..."

em vez da leitura pouco natural e verbosa das primeiras versões, que se lêem como

"se a palavra a adivinhar contém a letra for verdadeiro, então..."

"se a palavra a adivinhar contém a letra não for falso, então..."

ou

"se a palavra a adivinhar contém a letra não for verdadeiro, então..."

"se a palavra a adivinhar contém a letra for falso, então..."

A terceira razão tem a ver com o facto de as últimas formas serem idiomáticas em C++: toda a gente está à espera de as encontrar, pelo que encontrar variações é um factor que perturba a leitura e compreensão do código.

Da mesma forma se pode objectar relativamente ao seguinte código:

if(contém(palavra_a_adivinhar, letra))
    return true;
else
    return false;

ou

if(contém(palavra_a_adivinhar, letra))
    return false;
else
    return true;

Versões muito mais sintéticas e claras passam por devolver a própria guarda da instrução de selecção ou a sua negação:

return contém(palavra_a_adivinhar, letra);

ou

return not contém(palavra_a_adivinhar, letra);

Neste caso o argumento a legibilidade não colhe, pois estas instruções têm de ser lidas de uma forma pouco natural:

"Retorna devolvendo a veracidade da afirmação 'a palavra a adivinhar contém a letra'."

ou

"Retorna devolvendo a veracidade da afirmação 'a palavra a adivinhar não contém a letra'."

Mas o argumento a favor das expressões idiomáticas fala mais alto...

Finalmente, acerca da incrementação/decrementação.  Na linguagem C é comum dar-se preferência à versão sufixo,

i++;

enquanto em C++ se prefere a versão prefixo,

++i;

Estas formas são equivalentes, pelo menos se o valor resultante da operação de incrementação não for usado para nenhum outro efeito.  Mas são equivalente apenas em resultado.  Não em eficiência.  Se para os tipos básicos do C++ são equivalentes mesmo em eficiência, para Tipos Abstractos de Dados (definidos pelo utilizador usando classes C++) a versão prefixo é sempre pelo menos tão eficiente, se não mais, do que a versão sufixo.  Por essa razão é conveniente usar sempre a versão prefixo dos operadores de incrementação e decrementação.

8  Controlo de fluxo

8.1  Recursividade?

Quando a recursividade foi apresentada no final das aulas sobre modularização, fez-se a ressalva importante de que a recursividade só deve ser usada se:

  1. Conduzir a código mais fácil de perceber.
  2. Houver a garantia de um número limitado e pequeno de invocações embutidas.

Isto faz com que os casos de utilização apropriada de recursividade sejam relativamente raros.  Em nenhum dos enunciados dos trabalhos desta disciplina há em caso algum justificação para a sua utilização.  

Qualquer iteração pode ser transformada na invocação recursiva de rotinas, e qualquer invocação recursiva de rotinas pode ser transformada em iteração.  Por exemplo, que faz o seguinte código?

void mostra(vector<int> vector_de_inteiros, vector<int>::size_type início = 0)
{
    assert(inicio <= vector_de_inteiros.size());

    if(início == vector_de_inteiros.size()
        return;
    else {
        cout << vector_de_inteiros[início] << endl;
        mostra(vector_de_inteiros, início + 1);
    }
}

...

int main()
{
    vector<int> vector_de_inteiros;

    ...

    mostra(vector_de_inteiros);
}

Convenhamos que não é muito claro...  Melhor seria escrever

void mostra(vector<int> vector_de_inteiros)
{
    for(vector<int>::size_type i == 0; i != vector_de_inteiros.size(); ++i)
        cout << vector_de_inteiros[i] << endl;
}

Por exemplo, no código

void jogaJogo()
{
   
...
}

void jogaJogos()

    cout << "1 - Jogar." << endl;
    cout << "2 - Sair." << endl;
    cout << "Introduza uma opção: " ;

    char opção_do_utilizador;
    cin >> opção_do_utilizador;

    switch(opção_do_utilizador) {
      case '1':
        jogaJogo();
        break;
      case '2':
        break;
      default:
        cout << "Opção inválida!" << endl;
        jogaJogos();
        break;
    }
}

usa-se a recursividade de uma forma absurda, pois o código é pouco claro, além de não haver um número limite natural para o número de invocações repetidas do procedimento, pois tudo depende da paciência do utilizador do programa.  Melhor seria usar um ciclo:

void jogaJogo()
{
   
...
}

void jogaJogos()

    char opção_do_utilizador;
    do {
        cout << "1 - Jogar." << endl;
        cout << "2 - Sair." << endl;
        cout << "Introduza uma opção: " ;

        cin >> opção_do_utilizador;

        switch(opção_do_utilizador) {
          case '1':
            jogaJogo();
            break;
          case '2':
            break;
          default:
            cout << "Opção inválida!" << endl;
            break;
    } while(opção_do_utilizador != '2');
}

ou ainda

void jogaJogo()
{
   
...
}

void jogaJogos()

    while(true) {
        cout << "1 - Jogar." << endl;
        cout << "2 - Sair." << endl;
        cout << "Introduza uma opção: " ;

        char opção_do_utilizador;
        cin >> opção_do_utilizador;

        switch(opção_do_utilizador) {
          case '1':
            jogaJogo();
            break;
          case '2':
            return;
          default:
            cout << "Opção inválida!" << endl;
            break;
    }
}

Um outro exemplo é

bool utilizadorQuerVoltarAJogar(char opção)
{
    if(opção == 's')
        return true;
    else if (opção == 'n')
        return false;

    cout << "Quer voltar a jogar? [(s)im ou (n)ão]: ";
    cin >> opção;
    return utilizadorQuerVoltarAJogar(tolower(opção));
}

...

void jogaJogos()
{
   
...

    cout << "Quer voltar a jogar? [(s)im ou (n)ão]: ";
    char opção;
    cin >> opção;

    bool utilizador_quer_voltar_a_jogar = 
        utilizadorQuerVoltarAJogar(tolower(opção));

    ...
}

que deveria ser reescrito como

bool utilizadorQuerVoltarAJogar()
{
    char opção_do_utilizador;

    do {
        cout << "Quer voltar a jogar? [(s)im ou (n)ão]: ";
        cin >> opção_do_utilizador;
        opção_do_utilizador = tolower(opção_do_utilizador);
    } while(opção_do_utilizador != 's' and opção_do_utilizador != 'n');

    return opção_do_utilizador == 's';
}

...

void jogaJogos()
{
   
...

    bool utilizador_quer_voltar_a_jogar = utilizadorQuerVoltarAJogar();

    ...
}

ou mesmo

bool utilizadorQuer(string o_que_quer)
{
    char opção_do_utilizador;

    do {
        cout << "Quer " << o_que_quer << "? [(s)im ou (n)ão]: ";
        cin >> opção_do_utilizador;
        opção_do_utilizador = tolower(opção_do_utilizador);
    } while(opção_do_utilizador != 's' and opção_do_utilizador != 'n');

    return opção_do_utilizador == 's';
}

...

void jogaJogos()
{
   
...

    bool utilizador_quer_ voltar_a_jogar = utilizadorQuer("voltar a jogar");

    ...
}

8.2  Ciclos e instruções de iteração

A escrita de ciclos não é trivial.  Durante as aulas sugeriu-se que o desenvolvimento fosse disciplinado, de modo a evitar erros e a evitar a utilização de instruções de iteração não apropriadas.  Seguem abaixo alguns exemplos retirados das resoluções.

Exemplo 1:

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    for(string::size_type i = 0; i < cadeia.size(); ++i)
        if(cadeia[i] == caractere)
            return true;

    return false;
}

Diagnóstico:

  1. Utilização de guarda demasiado forte.  A utilização de uma guarda tão fraca quanto possível leva a que o não comprimento da respectiva pré-condição ou um erro na sua construção levem a um ciclo infinito.  Um ciclo infinito (ou pelo menos tão demorado que funciona como se fosse infinito) permite a detecção do, enquanto a terminação antecipada devida à demasiada força de uma guarda pode levar a valores errados e a erros detectados muito tarde e com dificuldade.

Correcção:

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    for(string::size_type i = 0; i < cadeia.size(); ++i)
        if(cadeia[i] == caractere)
            return true;

    return false;
}

Exemplo 2:

void jogaJogos()

    int opcao_do_utilizador = 0;
    while(opcao_do_utilizador != 2) {
        cout << endl
             << "Jogo do enforcado" << endl
             << "1 Novo jogo" << endl
             << "2 Sair" << endl
             << endl
             <<"Introduza uma opção: ";
        cin >> opcao_do_utilizador;
        while(not cin) {
            cout << "Escolha 1 ou 2!" << endl;
            cin.clear();
            char c;
            while(cin.get(c) and c != '\n')
                ;
            cin >> opcao_do_utilizador;
        }
        switch(opcao_do_utilizador) {
          case 1:
            jogaJogo();
            break;

          case 2:
            cout << "Terminando." << endl;
            break;

          default:
            cout << "Escolha 1 ou 2!" << endl;
            break;
        }
    }
}

Diagnóstico:

  1. O código tem problemas de modularização, que não serão corrigidos aqui, nomeadamente pelo facto de o ciclo de limpeza do canal de entrada não estar contido num módulo dedicado ao efeito nem tão pouco o código para leitura de um inteiro do teclado.
  2. Dois dos ciclos existentes podem ser melhorados.  O primeiro deveria ser um ciclo do while.  O segundo pode e deve ser reescrito usando o mesmo idioma usado no terceiro ciclo while.  Ver correcções abaixo.

Correcção:

void jogaJogos()

    int opcao_do_utilizador;
    do {
        cout << endl
             << " Jogo do enforcado " << endl
             << "1 Novo jogo" << endl
             << "2 Sair" << endl
             << endl
             <<"Introduza uma opção: ";
        while(not (cin >> opcao_do_utilizador)) {
            cout << "Escolha 1 ou 2!" << endl;
            cin.clear();
            char c;
            while(cin.get(c) and c != '\n')
                ;
        }
        switch(opcao_do_utilizador) {
          case 1:
            jogaJogo();
            break;

          case 2:
            cout << "Terminando." << endl;
            break;

          default:
            cout << "Escolha 1 ou 2!" << endl;
            break;
        }
    } while(opcao_do_utilizador != 2);
}

9  Documentação

Praticamente nenhuma resolução continha comentários, completos e correctos, de documentação dos módulos existentes no programa.  A documentação de um módulo deve conter:

  1. Uma breve descrição em português vernáculo da funcionalidade do módulo.
  2. O contrato do módulo:
    1. A pré-condição do módulo.
    2. A condição objectivo do módulo.

As partes constituintes do contrato do módulo podem ser escritas também em português, se for difícil exprimi-las em termos formais (matemáticos).

Exemplo:

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
   
...
}

Ver também a secção seguinte.

10  Programação por contrato: instruções de asserção

A documentação de todos os módulos de um programa é fundamental, como se referiu.  A documentação de um módulo corresponde grosso modo ao seu manual de utilização.  Diz o que é que o módulo faz exactamente.  Nesse sentido a documentação representa um contrato estabelecido implicitamente entre o produtor do módulo (que o desenvolveu ou produziu) e o seu consumidor (que o usa ou consome).  Esse contrato baseia-se nas noções de pré-condição e condição objectivo (ou pós-condição).  A documentação de uma rotina tem o seguinte aspecto geral:

/** Descrição breve e informal do que o módulo faz.
   
@pre PC (Pré-condição).
   
@post CO (Condição objectivo). */
declaração ou definição da rotina

O contrato estabelecido entre produtor e consumidor deve ser lido da seguinte forma:

O Produtor desta rotina compromete-se a garantir a verificação da condição CO quando a rotina retornar desde que o Consumidor se comprometa a invocá-la de modo a que a condição PC se verifique no seu início.

Note-se que o contrato acima não dá quaisquer garantias ao consumidor se este não cumprir a sua parte do contrato.  Nem prevêem qualquer penalização para o produtor caso este não cumpra a sua parte do contrato.  Há duas questões importantes a responder neste caso:

  1. A que se devem as violações do contrato, quando ocorrem?
  2. Que deve acontecer quando o contrato é violado?

A primeira questão tem uma resposta simples: as violações do contrato devem-se sempre a erros do programador.  Se a violação ocorrer por não se verificar a pré-condição no início da rotina, tal deve-se a um erro do programador consumidor da rotina.  Se a violação ocorrer por não se verificar a condição objectivo no final da rotina, apesar de se verificar a pré-condição no seu início, tal deve-se a um erro do programador produtor da rotina.

A segunda questão é mais fácil de responder depois de se responder à primeira.  Quando um contrato é violado, tal deve-se sempre a um erro de programação.  Claro está que estes erros a) não deviam ocorrer e b) não são conhecidos a priori, pois de outra forma um programador (normal...) tê-los-ia já corrigido...  O problema é que estes erros infelizmente vão mesmo ocorrer, pois o programador é humano.  Como lidar com eles, então?

As instruções de asserção servem para explicitar o contrato de uma rotina no código.  As instruções de asserção permitem que as violações do contrato produzam sempre o mesmo resultado: a terminação abrupta do programa com indicação do local onde a violação foi detectada.  As instruções de asserção são importantes mesmo que o programa nos pareça totalmente correcto.  Não devemos confiar na nossa suposta capacidade de escrever programas sem erros.

Por outro lado, há que distinguir claramente entre erros do programador e erros do utilizador do programa.  Com os primeiros temos de viver o melhor possível, usando-se para isso as instruções de asserção.  Quanto aos segundos, o programa deve prevê-los e lidar com eles directamente.  Exemplifiquemos com um pequeno programa:

#include <iostream>

using namespace std;

/**
Devolve a raiz quadrada do argumento.
   
@pre 0 <= valor.
   
@post raizDe2 aproximadamente igual a valor. */
double raizDe(double const valor)
{
    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;
        raiz = (raiz_anterior * raiz_anterior + valor) / (2.0 * raiz_anterior);
    }

    return raiz;
}


int main()
{
    cout << "Qual o valor (não-negativo)? ";
    double valor;
    cin >> valor;

    cout << "A raiz de " << valor << " é " << raizDe(valor) << '.' << endl;
}

Se ao executar este programa utilizador introduzir um número negativo, o ciclo onde se calcula a raiz não termina.  Quem errou?  

Terá sido o utilizador do programa?  É verdade que o programa pedia explicitamente que o valor fosse não-negativo.  Mas erros do utilizador do programa devem ser previstos no próprio programa!  Qualquer programa que se preze verifica os erros do utilizador e dá-lhe a possibilidade de os corrigir.  Conclusão, quem errou foi o programador do programa.  Mas enquanto produtor ou enquanto consumidor da função raizDe()?

Terá sido enquanto produtor?  A verdade é que a função raizDe() tem um contrato tal que só garante bons resultados para argumentos não negativos.  Será que o produtor deverá enfraquecer a pré-condição do contrato de modo a prever também o cálculo de raízes de valores negativos?  Essa é uma boa solução, em geral, mas neste caso é impossível fazê-lo sem alterar pelo menos o tipo de devolução para suportar números complexos.  É claramente preferível não o fazer.  Quando muito poder-se-ia definir uma outra versão da função, sobreposta à primeira, lidando com números complexos.  

Então porque não corrigir a função de modo a verificar se o valor é negativo e, em caso afirmativo, pedir de novo um valor ao utilizador?

double raizDe(double const valor)
{
    while(valor < 0) {
        cout << "Tem de ser não-negativo!" << endl;
        cout << "Qual o valor (não-negativo)? ";
        cin >> valor;
    }

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;
        raiz = (raiz_anterior * raiz_anterior + valor) / (2.0 * raiz_anterior);
    }

    return raiz;
}

Esta é a pior solução possível!  Transformou-se uma modularização simples, clara e eficiente, numa grande confusão.  Experimente-se descrever claramente o que a função faz, dar-lhe um nome apropriado ou escrever a sua condição objectivo...  Melhor seria, apesar de tudo, se a leitura do valor estivesse totalmente contida na função, mas ainda assim seria uma fraca modularização.  Uma forma de o ver é tentar atribuir um nome apropriado à função.  Teria de ser algo como lêValorNãoNegativoEDevolveASuaRaiz()...  Veja-se a discussão na Secção 4.

Claramente o erro não é do programador enquanto produtor de raizDe(), mas enquanto seu produtor.  Antes de alterar o código consumidor da função de modo a garantir a passagem de um valor não-negativo há, no entanto, que fazer uma pergunta.  Se o contrato da função só garante que a condição objectivo se verifica se o consumidor passar um argumento não negativo, o que acontece quando o consumidor viola a sua parte do contrato?

Em rigor, pode acontecer qualquer coisa.  O produtor é livre de lidar (ou não) com a situação como lhe aprouver.  Pode inclusivamente decidir apagar o disco rígido do computador...  Mas o que seria desejável que acontecesse?  Como se trata de um erro de programação, dificilmente o próprio programa pode lidar com ele...  (Mais tarde se verá que o mecanismo de lançamento de excepções pode ser usado para tentar tornar os programas resistentes aos seus próprios erros.)  Assim, uma solução pode passar simplesmente por, em caso de violação do contrato por parte do consumidor da função, o programa abortar emitindo uma mensagem de erro que indique claramente o local do erro.  Isso consegue-se usando uma instrução de asserção no início da função:

#include <iostream>
#include <cassert>

using namespace std;

/**
Devolve a raiz quadrada do argumento.
   
@pre 0 <= valor.
   
@post raizDe2 aproximadamente igual a valor. */
double raizDe(double const valor)
{
    assert(0 <= valor);

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;
        raiz = (raiz_anterior * raiz_anterior + valor) / (2.0 * raiz_anterior);
    }

    return raiz;
}


int main()
{
    cout << "Qual o valor (não-negativo)? ";
    double valor;
    cin >> valor;

    cout << "A raiz de " << valor << " é " << raizDe(valor) << '.' << endl;
}

É muito razoável, pois, "armadilhar" o programa para detectar erros de programação e abortar com uma mensagem de erro minimamente simpática quando tal ocorrer.  Mas este programa, pelo menos no que à função raizDe() diz respeito, detecta apenas erros do programador consumidor.  O programador produtor também pode cometer erros!  Para os detectar, é necessário usar uma instrução de asserção no final da função.  Esta instrução de asserção deve verificar se a condição objectivo é verdadeira, abortando o programa caso não o seja.  O problema é que a condição objectivo não está bem enunciada.  O que significa "aproximadamente"?

Uma constante importante quando se trabalha com números de vírgula flutuante é o chamado épsilon.  Esta constante é a diferença entre 1 e o menor valor superior a 1 que é representável para um dado tipo.  No caso do tipo double o valor de épsilon é 2,22045×10-16, e acede-se a ela através de numeric_limits<double>::epsilon() (tem de se fazer #incude <limits>).  Esta constante no fundo representa a precisão máxima do tipo em uso.  Assim, uma boa condição objectivo para a função seria que o erro relativo cometido fosse inferior ou igual a épsilon.  Como para calcular o erro relativo seria necessário saber o valor real (e não aproximado) da raiz, a solução passa por exprimir a condição objectivo em termos do erro relativo (em módulo) entre o valor dado e o quadrado da raiz aproximada encontrada:

#include <iostream>
#include <cassert>
#include <limits>
#include <cmath>

using namespace std;

/**
Devolve a raiz quadrada do argumento.
   
@pre 0 <= valor.
   
@post |raizDe2 - valor| <= numeric_limits<double>::epsilon() × valor. */
double raizDe(double const valor)
{
    assert(0 <= valor);

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;
        raiz = (raiz_anterior * raiz_anterior + valor) / (2.0 * raiz_anterior);
    }

    assert(abs(raiz * raiz - valor) <= 
           numeric_limits<double>::epsilon() * valor);
    return raiz;
}


int main()
{
    cout << "Qual o valor (não-negativo)? ";
    double valor;
    cin >> valor;

    cout << "A raiz de " << valor << " é " << raizDe(valor) << '.' << endl;
}

Tudo isto é muito bonito e elegante (?), mas...  O programa continua errado!  É necessário corrigir o erro do programador consumidor, i.e., tornar o programa capaz de lidar com os possíveis erros do utilizador do programa!  A solução aqui passa por garantir que não se tenta calcular a raiz de um valor negativo:

#include <iostream>
#include <cassert>
#include <limits>
#include <cmath>

using namespace std;

/**
Devolve a raiz quadrada do argumento.
   
@pre 0 <= valor.
   
@post |raizDe2 - valor| <= numeric_limits<double>::epsilon() × valor. */
double raizDe(double const valor)
{
    assert(0 <= valor);

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;
        raiz = (raiz_anterior * raiz_anterior + valor) / (2.0 * raiz_anterior);
    }

    assert(abs(raiz * raiz - valor) <= 
           numeric_limits<double>::epsilon() * valor);
    return raiz;
}


int main()
{
    double valor;

    do {
        cout << "Qual o valor (não-negativo)? ";
        cin >> valor;
    } while(valor < 0);

    cout << "A raiz de " << valor << " é " << raizDe(valor) << '.' << endl;
}

(Note-se que, de modo a deixar o código simples, não se previu o caso de o utilizador introduzir algo que não um valor aritmético.)

Uma questão se coloca aqui: uma vez corrigido o erro do programador consumidor, para que serve a primeira asserção da função raizDe()?

  1. Do ponto de vista do programa como produto acabado ambas as asserções são desnecessárias (a primeira é fácil de ver porquê, a segunda implicaria uma demonstração formal do bom funcionamento da função).
  2. Mas um programa nunca é um produto acabado.  Está sujeito a correcções, actualizações, etc.  Durante essas operações, o programador pode introduzir novos erros no programa, que é conveniente serem assinalados convenientemente.

Espero que esta explicação longa vos permita compreender o que há de errado nas seguintes frases retiradas de algumas resoluções do Problema 1:

Assim, a única explicação relevante prende-se com a ausência de asserções, facto precavido por nós, ao criarmos nos procedimentos funções [instruções?] que substituem essas mesmas asserções [...].

[...] optamos por não incluir [instruções de] asserção, pré-condições e pós-condições, porque usamos funções e/ou procedimentos ou condições para prevenir possíveis erros do utilizador.

Ao ler o código verifica[-se] a inexistência de [instruções de] asserções, pois conseguimos que o programa filtrasse os erros possíveis do utilizador.

11  Variáveis globais e fundamentalismo

Como referido ao longo da disciplina, as variáveis globais são de evitar.  Suponha-se o seguinte programa, cujo objectivo é calcular o quadrado de um número introduzido pelo utilizador:

// Programa absurdo!  Má ideia!  Não copiar!
#include <iostream>

using namespace std;

double valor;

void elevaAoQuadrado() 
{
    valor *= valor;
}

int main()
{
    cout << "Introduza um valor: ";
    cin >> valor;

    elevaAoQuadrado();

    cout << "O quadrado é " << valor << '.' << endl;
}

O programa faz o que é suposto.  Mas fá-lo mal.  Restrinja-se o olhar à função main():

  1. Onde está a variável valor?
  2. O que faz ?  Eleva ao quadrado o quê?

Estas dificuldades de leitura são escusadas, e favorecem a ocorrência e difícil correcção de erros.  Por outro lado tornam o código "prisioneiro" do problema em causa, tornando a generalização difícil.  Suponha-se, por exemplo, que o objectivo agora é encontrar as raízes de um polinómio do segundo grau.  Para manter o procedimento elevaAoQuadrado() como está, recorrendo a uma variável global, o problema tem de ser resolvido como se segue:

// Programa absurdo!  Má ideia!  Não copiar!
#include <iostream>

using namespace std;

double valor;

void elevaAoQuadrado() 
{
    valor *= valor;
}

int main()
{
    cout << "Introduza os coeficientes a, b e c (ax^2 + bx + c): ";
    double a, b, c;
    cin >> a >> b >> c;

    valor = b;
    elevaAoQuadrado();

    double discriminante = valor + 4 * a * c;

    if(discriminante < 0) {
        cerr << "Polinómio sem raízes reais." << endl;
        return 1;
    }

    double r1 = (-b - sqrt(disciminante)) / 2 / a;
    double r2 = (-b + sqrt(disciminante)) / 2 / a;

    cout << "As raízes são " << r1 << " e " << r2 << '.' << endl;
}

Uma solução muito mais razoável seria prescindir de variáveis globais:

#include <iostream>

using namespace std;

double valor;

/** Devolve o quadrado do argumento.
   
@pre V.
   
@post quadradoDe = valor2. */
double elevaAoQuadrado(double valor) 
{
    return valor * valor;
}

int main()
{
    cout << "Introduza os coeficientes a, b e c (ax^2 + bx + c): ";
    double a, b, c;
    cin >> a >> b >> c;

    double discriminante = quadrado(b) + 4 * a * c;

    if(discriminante < 0) {
        cerr << "Polinómio sem raízes reais." << endl;
        return 1;
    }

    double r1 = (-b - sqrt(disciminante)) / 2 / a;
    double r2 = (-b + sqrt(disciminante)) / 2 / a;

    cout << "As raízes são " << r1 << " e " << r2 << '.' << endl;
}

É importante realizar, no entanto, que ocasionalmente (leia-se raramente) é útil recorrer a variáveis globais (e.g., cin, cout e cerr são variáveis globais).  Mas pense muitas vezes antes de as utilizar.

12  Soluções comuns: boatos

Por vezes há más soluções que se espalham como boatos.  Por exemplo, não faz qualquer sentido escrever uma função para devolver uma cadeia de caracteres com a mesma dimensão da cadeia passada como argumento mas preenchida com hífens:

/** Cria e devolve a palavra escondida com base na palavra adivinhar.
    @pre  V.
    @post  pala '-' com o mesmo de número de letras da palavra a adivinhar. */
string palavraEscondida(string palavra)
{
    string palavra_escondida;

    for(string::size_type i = 0; i != palavra.length(); ++i)
        palavra_escondida += '-';

    return tracos;
}

Nem tão pouco faz sentido continuar a usar uma função como o mesmo objectivo embora com o corpo fazendo uso de um dos construtores do tipo string:

/** Cria e devolve a palavra escondida com base na palavra adivinhar.
    @pre  V.
    @post  pala '-' com o mesmo de número de letras da palavra a adivinhar. */
string palavraEscondida(string palavra)
{
    string palavra_escondida(palavra.length(), '-');

    return tracos;
}

ou mesmo

/** Cria e devolve a palavra escondida com base na palavra adivinhar.
    @pre  V.
    @post  pala '-' com o mesmo de número de letras da palavra a adivinhar. */
string palavraEscondida(string palavra)
{
    return string(palavra.length(), '-');
}

O melhor mesmo seria usar este tipo de inicialização directamente na definição da variável, sem definir qualquer função para o efeito.

13  Exemplos gerais

Abaixo segue-se um conjunto genérico de (maus) exemplos e respectivas correcções.  Embora estes erros digam maioritariamente respeito a grafismo e nomenclatura, apresentam-se numa secção à parte para que um correcção integral possa ser apresentada.

Exemplo 1:

/** Esta rotina vai devolver uma letra minúscula. Se necessário, vai converter
   
uma letra maiúscula em minúscula. */
char conversão(char letra)
{
    if(isupper(letra))
    { 
        letra=tolower(letra);
        return letra;
    }
    else
        return letra;
}

Diagnóstico: 

  1. Maus nomes da função e do respectivo parâmetro.
  2. Má documentação: não apenas não diz nada acerca da relação entre o que devolve e o que recebe como argumento, como também está escrita num português pouco recomendável.
  3. Má utilização de espaços: faltam espaços entre o operador = e os respectivos operandos.
  4. Instrução de selecção inútil.

Correcção:

/** Devolve a versão minúscula do caractere dado.
   
@pre V.
    @post versãoMinúsculaDe = versão minúscula de caractere. */
char versãoMinúsculaDe(char caractere)
{
    return tolower(caractere);
}

Mas pode-se perguntar simplesmente qual a utilidade de semelhante função, uma vez que devolve exactamente o mesmo que a função tolower() pré-existente...

Exemplo 2:

//mostra o estado do jogo
void progresso(string alfabeto,string palavra,int falhanços)
{
    cout<<endl;
    cout<<"estado do jogo"<<endl;
    cout<<endl;
    cout<<endl;
    cout<<"palavra : "<<palavra<<endl;
    cout<<endl;
    cout<<"letras disponíveis : "<<alfabeto<<endl;
    cout<<endl;
    cout<<"falhanços disponíveis : "<<falhanços<<endl;
}

Diagnóstico:

  1. Péssimo nome para o procedimento e para os parâmetros.
  2. Escrita verbosa de múltiplas operações de inserção em cout.
  3. Má utilização de espaços: em falta após as vírgulas na lista de parâmetros e a separar operador << dos respectivos operandos..

Correcção:

/** Mostra o estado do jogo.
    @pre V.
    @post Ecrã contém estado actual do jogo. */
void mostraEstadoDoJogo(string letras_disponíveis, string palavra_adivinhada, 
                        int número_de_falhanços_disponíveis)
{
    cout << endl
         << "Estado do jogo"<<endl
         << endl
         << endl
         << "palavra adivinhada: "<< palavra_adivinhada << endl
         << endl
         << "letras disponíveis: "<< letras_disponívei << endl
         << endl
         << "falhanços disponíveis: "<< número_de_falhanços_disponíveis << endl;
}

Exemplo 3:

//Esta rotina vai verificar se a letra escolhida já foi ou não inserida anteriormente.
bool letra_no_abc(string abc , char letra)
{
    int comprimento=abc.length(); 
    for(int b=0;b!=comprimento; ++b)
    {
        if(abc[b]==letra)
        { 
            return true;
            break;
        }
    }
    return false;
}

Diagnóstico:

  1. Péssimos nomes do procedimento e dos parâmetros.
  2. Nomes muito específicos induzem potencial consumidor em erro.  A função devolve um booleano que indica se uma cadeia (de caracteres) contém um caractere.  O seu nome não precisa de ficar "amarrado" à sua utilização concreta neste programa.
  3. Utilização de uma variável espúria (comprimento), apenas porque quiseram evitar um aviso do compilador da forma errada.
  4. Uma instrução break é sempre inútil após uma instrução return, pois a última termina imediatamente a função, e por conseguinte dos os ciclos em execução.
  5. Espaços em falta onde deveriam estar e a mais onde deveriam faltar.
  6. Nome pouco idiomático para o índice usado no ciclo.

Correcção:

/** Devolve booleano que indica se a cadeia dada contém o caractere dado.
    @pre V.
    @post contém = (E j : 0 <= j < cadeia.size() : cadeia[j] == caractere). */
bool contém(string cadeia, char caractere)
{
    for(string::size_type i = 0; i != cadeia.size(); ++i)
        if(cadeia[i] == caractere)
            return true;

    return false;
}

Exemplo 4:

int main() 
{
    char jogaousai; //
variavel introduzida no menu "jogo do enforcado"

    while (jogaousai != 'q')/*
enquanto a escolha for diferente de 'q', é devolvido
                              o menu principal*/
    {
       
...

        cin >> jogaousai;

        ...
    }
}

Diagnóstico:

  1. O nome da variável que guarda a opção do utilizador é absurdo.
  2. O ciclo funciona por acaso, pois a variável não é inicializada.
  3. Comentários desnecessários e, além disso, incorrectos (que é isso de "devolver" o menu principal?).

Correcção:

int main() 
{
    char opção_do_utilizador;

    do {
       
...

        cin >> opção_do_utilizador;

        ...

    } while(opção_do_utilizador != 's');
}