Aula 6


1  Resumo da matéria

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.  Estas instrução permitem-nos executar um conjunto de instruções caso uma condição seja verdadeira e, no caso da instrução if else, um outro conjunto caso a condição seja falsa.  Por exemplo *:
if(x < 0)  // 1
    x = 0; // 2
else       // 3
    x = 1; // 4
....       // 5
As quatro linhas, 1 a 4, correspondem a uma única instrução, dita de selecção, que é composta de uma condição (na linha 1) e duas instruções alternativas (linhas 2 e 4).  Se x fôr menor do que zero, então é executada a instrução 2, passando o valor de x a ser zero, senão é executada a instrução 4, passando a variável x a ter o valor 1.  Em qualquer dos casos, a execução continua na linha 5 (excepto em alguns casos *).

Em certos casos pretende-se apenas executar instruções se determinada condição se verificar e não se desejando fazer nada caso a condição seja falsa.  Nestes casos pode-se omitir o else.  Por exemplo:

if(x < 0)  // 1
    x = 0; // 2
....       // 3
Se x fôr menor do que zero, então é executada a instrução 2, passando o valor de x a ser zero, sendo em seguida executada a instrução na linha 5 (excepto em alguns casos *).   No caso contrário a execução passa directamente para a linha 5.

Em muitos casos é necessário que o computador execute condicionalmente, não uma instrução simples, mas um conjunto de instruções.  Para o conseguir, agrupam-se essas instruções num bloco ou instrução composta, colocando-as entre chavetas {}.  Por exemplo:

int m, n;
....
if(m < n) {
    int auxiliar = m;
    m = n;
    n = auxiliar;
}
Caso seja necessário podem-se encadear instruções de selecção umas dentro das outras.  Por exemplo, para verificar qual a posição de a relativamente a um intervalo [minimo, maximo] :
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 simplesmente:
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;
No entanto, 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 pertence ao primeiro if, ao contrário do que a indentação do código sugere: pertence ao segundo if.  Em caso de dúvida, um else pertence ao if mais próximo (e acima...) dentro mesmo bloco de instruções.  Para corrigir o exemplo anterior é necessário construir uma instrução composta, muito embora consista 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 duma forma liberal, pois construções como a que apresentou podem dar origem a erros muito difíceis de detectar e corrigir.  Os compiladores de qualidade, no entanto, avisam o programador da presença de semelhantes ambiguidades.

Um outro erro frequente é escrever:

if(x < 0);
    x = -x;
Neste caso a intenção seria que, depois do if, x conteria o módulo do valor original.  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 = -x;
Ou seja, se x fosse inicialmente positivo tornar-se-ia negativo!  Estes erros, não sendo muito comuns, são ainda mais difíceis de detectar.

* Chama-se fluxo do programa à sequência pela qual as instruções são executadas.
* Uma forma alternativa de comentários em C++ usa as construções /* e */.  Tudo o que se encontra entre /* e o */ seguinte é interpretado pelo compilador simplesmente como um espaço.
* Podem existir instruções return, break, continue ou goto que alterem a linha normal de execução.

1.2  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).  Como os nomes dos dígitos decimais em Português, como em qualquer outra língua, não obedecem a qualquer regra lógica, a função terá de prever cada um dos 10 casos separadamente.  Usando a instrução if else:
void escreveDígitoPorExtenso(int dígito)
{
    if(dígito == 0)
        cout << "zero";
    if(dígito == 1)
        cout << "um";
    if(dígito == 2)
        cout << "dois";
    if(dígito == 3)
        cout << "três";
    if(dígito == 4)
        cout << "quatro";
    if(dígito == 5)
        cout << "cinco";
    if(dígito == 6)
        cout << "seis";
    if(dígito == 7)
        cout << "sete";
    if(dígito == 8)
        cout << "oito";
    if(dígito == 9)
        cout << "nove";
    if(dígito < 0 || dígito > 9)
        cout << "erro";
}
Uma vez que dígito não pode tomar dois valores ao mesmo tempo, esta solução é ineficiente, pois obriga a onze testes qualquer que seja o dígito a imprimir.  Uma alternativa melhor é:
// Repare na estrutura, em que else e if são colocados na mesma linha
// e se repete a indentação em cada linha.  Fica mais claro que é uma
// sequência de alternativas.
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 if(dígito == 9)
        cout << "nove";
    else
        cout << "erro";
}
Existe uma solução melhor 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:
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;
      case 9: cout << "nove";
          break;
      default: cout << "erro";
        break;
    }
}
Esta instrução tem algumas particularidades.  Em primeiro lugar 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, devendo os valores colocados 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 chaveta final, ou quando for executada uma instrução de break (terminado o switch a execução continua sequencialmente).  A consequência deste facto é que, se no exemplo anterior se eliminarem os break:
 
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";
      case 9: cout << "nove";
      default: cout << "erro";
    }
}
uma chamada escreveDígitoPorExtenso(7) resulta em:
seteoitonoveerro
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 duma forma imediata) como alternativa a:

// Pré-condição: 0 <= valor e valor < 10000.
void escreveGama(double x)
{
    if(x < 10)
        cout << "unidades";
    else if(x < 100)
        cout << "dezenas";
    else if(x < 1000)
        cout << "centenas";
    else
        cout << "milhares";
}
Suponha que os valores passados como argumento a escreveGama() 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 para
// Pré-condição: 0 <= valor e valor < 10000.
void escreveGama(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)!  A ordem pela qual se fazem as comparações pode ser muito relevante!

1.3  Tipos enumerados

Além dos tipos de dados pré-definidos no C++, os chamados tipos básicos, podem-se criar tipos de dados adicionais.  Essa é, como se verá mais tarde, uma das tarefas fundamentais da programação moderna.  Para já, abordar-se-ão extensões simples aos tipos do C++: os tipos enumerados.  Uma variável dum tipo enumerado pode conter um número limitado de valores, que se enumeram na definição do tipo.  Por exemplo:
enum DiaDaSemana {
    segunda_feira,
    terça_feira,
    quarta_feira,
    quinta_feira,
    sexta_feira,
    sábado,
    domingo
};
define um tipo enumerado com sete valores possíveis, um para cada dia da semana *.  O novo tipo utiliza-se como habitualmente em C++:
DiaDaSemana dia;
Pode-se atribuir atribuir a esta variável qualquer dos valores listados na definição:
dia = terça_feira;
Cada um dos valores associados ao tipo DiaDaSemana (viz. segunda_feira, ..., domingo) é utilizado no código como um valor literal para esse tipo, tal como 10 é um valor literal do tipo int ou 'a' é uma valor literal do tipo char.  Como se trata de um tipo definido pelo utilizador, não é possível, sem mais esforço, ler valores desse tipo do teclado ou escrevê-los no ecrã usando os métodos habituais (viz. cin >> e cout <<).  Mais tarde ver-se-á como se pode "ensinar" o computador a ler e escrever tipos enumerados de dados.

Na maioria dos casos os tipos enumerados são usados para tornar mais claro o significado dos valores atribuidos a uma
variável, pois segunda_feira tem claramente mais significado que 0!  Mas, na realidade, os valores de tipos enumerados são representados como inteiros atribuidos sucessivamente a partir de zero.  Assim, segunda_feira tem representação interna 0, terça_feira tem representação 1, etc.  De facto, se se tentar imprimir segunda_feira o resultado será 0, que é a sua representação na forma de um inteiro.  É possível atribuir inteiros arbitrários a cada um dos valores duma enumeração, pelo que podem existir representações idênticas para valores com nome diferentes:

enum DiaDaSemana { // agora com nomes alternativos
    primeiro = 0,  // desnecessário
    segunda = primeiro,
    segunda_feira = segunda,
    terça,
    terça_feira = terça,
    quarta,
    quarta_feira = quarta,
    quinta,
    quinta_feira = quinta,
    sexta,
    sexta_feira = sexta,
    sábado,
    domingo,
    último = domingo,
};
Se um operando de um tipo enumerado ocorrer numa expressão, o C++ geralmente convertê-lo-á num inteiro.  Essa conversão também se pode especificar directamente, escrevendo int(segunda_feira), por exemplo.  As conversões opostas também são possíveis, usando DiaDaSemana(2), por exemplo, para obter quarta_feira.  Na próxima secção ver-se-á como redefinir os operadores do C++ para operarem sobre tipos enumerados sem surpresas desagradáveis para o programador/utilizador.

* Na realidade os enumerados podem conter valores que não correspondem aos especificados na sua definição.  Ver Stroustrup, página 77.

1.4  Sobrecarga de operadores

Da mesma forma que se podem sobrecarregar nomes de funções, i.e., dar o mesmo nome a funções que, tendo semanticamente o mesmo significado, operam com argumentos de tipos diferentes, também é possível sobrecarregar o significado dos operadores usuais do C++ de modo a que tenham um significado especial quando aplicados a tipos definidos pelo utilizador.  Se se pretender, por exemplo, sobrecarregar o operador ++ prefixo para funcionar com o tipo DiaDeSemana definido acima, pode-se definir um procedimento com uma sintaxe especial:
DiaDaSemana operator ++ (DiaDaSemana& dia)
{
    if(dia == último)
        return dia = primeiro;
    else
        return dia = DiaDeSemana(dia + 1);
}
Assim, a incrementação de uma variável do tipo DiaDeSemana conduz sempre ao dia da semana subsequente.  Note-se que se utilizou primeiro e último e não segunda_feira e domingo, pois dessa forma pode-se mais tarde decidir que a semana começa ao domingo sem ter de alterar o procedimento acima, alterando apena a definição da enumeração.


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


Exercícios propostos

1.  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
2.  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?