Resumo da Aula 12

Sumário

Referências constantes (de novo)

A passagem por referência é, na maioria dos casos, mais eficiente do que a passagem por valor: numa passagem por valor faz-se uma cópia integral do argumento ao construir o parâmetro respectivo, enquanto numa passagem por referência o parâmetro se torna um simples sinónimo do argumento.  Em alguns casos, quando se utilizam parâmetros de tipos que ocupem potencialmente uma quantidade considerável de memória, é importante, por questões de eficiência, passar os argumentos por referência, mesmo quando estes não são alterados pela rotina em questão!  Neste caso é importante indicar claramente ao compilador que, apesar de serem passados por referência, os argumentos não serão alterados pela rotina.  Esta indicação, por um lado, mostra ao programador consumidor da classe que a instância passada como argumento não será alterada e, por outro lado, impede o programador produtor da classe de alterar acidentalmente o valor do parâmetro correspondente (e portanto também do argumento) dentro da rotina.

1.1  Exemplo

int númeroDeItensVerdadeirosEm(vector<bool> const& v)
{
    int contador = 0;
    for(vector<bool>::size_type i = 0; i != v.size(); ++i)
        if(v[i])
            ++contador;
    return contador;
}

...

int main()
{
    vector<bool> v(1000);

    ...

    cout << númeroDeItensVerdadeirosEm(v) << endl;
}

Operador ++ sufixo e prefixo

Como já se viu, o operador ++ prefixo para uma classe (neste caso a classe Racional) pode ser definido do seguinte modo:

...

class Racional {
  public:

    ...

    /** Adiciona um à instância implícita, devolvendo-a por referência.
       
@pre *this = r.
       
@post *this = r + 1 e operator+= idêntico a *this. */
    Racional& operator++();

    ...

};

...

Racional& Racional::operator++()
{

    assert(cumpreInvariante());

    numerador_ += denominador_;

    assert(cumpreInvariante());

    return *this;
}

o que permite escrever as seguintes instruções:

Racional r = 1;
++ ++r;

A definição do operador ++ sufixo para a classe Racional é feita acrescentando um parâmetro postiço do tipo int:

...

class Racional {
  public:

    ...

    /** Adiciona um à instância implícita, devolvendo-a por referência.
       
@pre *this = r.
       
@post *this = r + 1 e operator+= idêntico a *this. */
    Racional& operator ++ ();

    /** Adiciona um à instância implícita, devolvendo o racional antes de incrementado.
       
@pre *this = r.
       
@post *this = r + 1 e operator+= = r. */
    Racional& operator ++(int);

    ...

};

...

Racional& Racional::operator++()
{

    assert(cumpreInvariante());

    numerador_ += denominador_;

    assert(cumpreInvariante());

    return *this;
}

Racional Racional::operator++(int)
{

    assert(cumpreInvariante());

    // O parâmetro não é usado para nada!  É um "truque sujo" do C++...

    Racional cópia = *this; // cria-se uma cópia da instância 
                            //  implícita antes de incrementada!

    operator ++();           // invoca-se explicitamente a incrementação prefixa
                            // para incrementar a instância implícita.
    // Também podia ser: ++*this;

    assert(cumpreInvariante() and cópia.cumpreInvariante());

    return cópia;           // devolve-se a cópia com o valor da instância implícita
                            // antes de incrementada!
}

O que permite escrever as seguintes instruções:

Racional r = 1;
r++;    // utilização do operador sufixo.
++ ++r; // utilização do operador prefixo.

No caso de o operador ser definido como rotina normal (não membro de uma classe), é necessário usar um parâmetro adicional para representar a variável a incrementar (visto que não há instância implícita numa rotina normal).  Por exemplo:

enum DiaDaSemana {
    segunda_feira,
    ...
    domingo
};

int const número_de_dias_da_semana = 7;

// Incrementação prefixa:
DiaDaSemana& operator++(DiaDaSemana& dia)
{
    if(dia == domingo)
        return dia = segunda_feira;
    else
        return dia = DiaDaSemana(int(dia) + 1);
}

// Incrementação sufixa:
DiaDaSemana operator++(DiaDaSemana& dia, int)
{
    DiaDaSemana cópia = dia;
    ++dia;

    return cópia;
}

O que permite escrever as seguintes instruções:

DiaDaSemana d = terça_feira;
++d; // utilização do operador prefixo.
d++; // utilização do operador sufixo.

Canais de ligação a ficheiros

Para escrever no ecrã e ler valores do teclado usam-se os chamados canais (streams) de entrada e saída.  O canal de saída para o ecrã é designado por cout.  O canal de entrada de dados pelo teclado é cin.  Para efectuar uma operação de leitura de dados do teclado  usa-se o operador de extracção >>.  Para efectuar uma operação de escrita de dados no ecrã usa-se o operador de inserção <<.

Por exemplo,

int a = 10;
cout << "Vou escrever o valor de 'a' a seguir a esta frase: " << a;

e

int a;
cout << "Introduza um valor: ";
cin >> a;

Após uma operação de entrada de dados, a variável toma o valor que foi inserido no teclado pelo utilizador do programa, desde que esse valor possa ser tomado por esse tipo de variável.  É possível verificar se o valor introduzido estava correcto, ou seja, verificar se a operação de extracção teve sucesso: basta tratar o canal como um valor booleano.  Se o resultado for verdadeiro, a leitura teve sucesso.  Se for falso, a leitura falhou.  Por exemplo:

cout << "Introduza um inteiro: ";
int i;
cin >> i;
if(not cin)
    cout << "A leitura falhou!  Não introduziu um inteiro..." << endl; 

Alternativamente, e de forma mais clara e legível, pode-se usar uma operação para saber se o canal está em "bom estado":

cout << "Introduza um inteiro: ";
int i;
cin >> i;
if(not cin.good())
    cout << "A leitura falhou!  Não introduziu um inteiro..." << endl; 

Ao ser executada uma operação de leitura como acima, o computador interrompe a execução do programa até que seja introduzido algum valor no teclado.

Quando é feita a leitura de um caracter do teclado (usando extracção do canal cin) é lido apenas um caractere, mesmo que no teclado seja inserido um código (ou mais do que um caractere), i.e., o resultado das operações

cout << "Insira um caractere: ";
char caractere;
cin >> caractere;
cout << "O caracter inserido é : " << caractere;

seria

Insira um caractere: 48
O caracter inserido é: 4

caso o utilizador inserisse 48.  O dígito '8' foi ignorado visto que se leu apenas um caractere, e não o seu código.

Mais genericamente, os canais são entidades que permitem ler ou escrever informação sequencialmente a partir de algum dispositivo.  Depois de ser fazer #include <iostream> há (pelo menos) três canais que ficam estabelecidos: cin, cout e cerr.  O primeiro é um canal de entrada e está normalmente ligado ao teclado.  O segundo é um canal de saída e está normalmente associado ao ecrã (ou à janela da consola de comandos...).  O terceiro difere do segundo em pouco, mas deve ser usado para escrever mensagens de erro.

É possível estabelecer canais de leitura (entrada) e escrita (saída) ligados a ficheiros no disco do computador.  Para isso é necessário fazer #include <fstream>, e definir variáveis das classes ifstream (para leitura) e ofstream (para escrita), passando o nome do ficheiro ao seu construtor.  O nome do ficheiro deve ser passado na forma de uma cadeia de caracteres clássica do C++ (i.e., uma matriz de caracteres com um terminador especial), e não uma string.  

A abertura de um canal de escrita para um ficheiro é destrutiva: se existir, o ficheiro é esvaziado imediatamente.  Tal como para os três canais preestabelecidos já referidos, podem-se realizar operações de extracção e inserção a partir de canais ligados a ficheiros, usando-se para isso os usuais operadores de extracção >> e de inserção <<.

Um canal pode ser interpretado como um booleano.  Se o resultado for verdadeiro, então o canal está estabelecido e em bom estado.  Caso contrário o canal está em mau estado, por exemplo porque não está estabelecido (não liga a lado nenhum) ou porque uma operação de inserção ou extracção falhou.  O mesmo efeito se pode obter recorrendo à operação good().  Todas as operações de inserção e extracção realizadas sobre um canal em mau estado falham, não produzindo qualquer efeito.  Para limpar uma condição de erro pode-se invocar o método clear() para o canal em causa (e.g., cin.clear()).

Por exemplo, o próximo programa lê um ficheiro (chamado notas1) consistindo numa sequência de linhas contendo um número de aluno e duas notas e produz um ficheiro consistindo numa sequência semelhante, mas com a média das notas (arredondada para cima):

#include <fstream>
#include <string>

using namespace std;

int main()
{
    // Guarda nomes dos ficheiros em string:
    string nome_entrada = "notas1";
    string nome_saida = "notas2";

    // Estabelece canal de entrada (usa-se a função membro string::c_str() para se obter
    // o nome do ficheiro na forma de uma cadeia de caracteres clássica):
    ifstream entrada(nome_entrada.c_str());

    if(not entrada.good()) {
        cout << "O ficheiro '" << nome_entrada << "' não parece existir!"
             << endl;
        return 1;
    }

    ofstream saida(nome_saida.c_str());

    if(not saida.good()) {
        cout << "O ficheiro '" << nome_saida
             << "' não pôde ser criado/esvaziado!" << endl;
        return 1;
    }

    int numero, nota1, nota2;
    while(entrada >> numero >> nota1 >> nota2)
        // A guarda deste ciclo são três operações de extracção.  Cada uma
        // devolve (uma referência para) o canal de entrada.  O canal devolvido
        // pela última operação é interpretado como um bool pelo while.
        // Assim, logo que a leitura falhar, presumivelmente por se ter
        // atingido o fim do ficheiro de entrada, o ciclo termina.
        saida << ' ' << numero << ' ' << (nota1 + nota2 + 1) / 2 << endl;

    // Os canais são fechados automaticamente quando a função main() termina.
}

Note-se que as classes ifstream e ofstream funcionam como especializações das classes istream e ostream, respectivamente.  Isto permite usar um ifstream onde quer que o C++ esteja à espera de um istream, por exemplo.  É este facto que permite fazer com que a sobrecarga dos operadores de inserção e extracção de canais para tipos definidos pelo utilizador também sirva para canais ligados a ficheiros, como se verá mais tarde.