AULA 15 - Programação II - Graduação

De IFSC
Revisão de 16h50min de 2 de maio de 2018 por imported>Fargoud (→‎Formatação de dados)
Ir para navegação Ir para pesquisar

Fluxos

Um fluxo, ou stream, é uma abstração representando a transmissão de dados entre:

  • um emissor, aquele que produz informação;
  • um receptor, aquele que consome a informação produzida.

Em C++, as instruções de E/S não fazem parte da linguagem. Elas são oferecidas em uma biblioteca padrão, que implementa os fluxos a partir de uma hierarquia de classes. Por default, cada programa C++ pode utilizar três fluxos:

  • cout: que corresponde à saída padrão.
  • cin: que corresponde à entrada padrão.
  • cerr: que corresponde à saída padrão de erros.

Para utilizar outros fluxos, devemos criá-los e associá-los a arquivos, normais ou especiais (dispositivos), ou então a vetores de caracteres.

Fluxos e classes

As classes declaradas em iostream.h permitem a manipulação dos periféricos padrões, como por exemplo vídeo e teclado:

PRGfluxo1.png
  • ios: classe base de E/S em fluxos; ela contém um objeto da classe streambuf para a

gestão dos buffers de entrada e saída.

  • istream : classe derivada de ios para os fluxos de entrada.
  • ostream : classe derivada de ios para os fluxos de saída.
  • iostream : classe derivada de istream e de ostream para os fluxos bidirecionais.
  • istream_withassign, ostream_withassign e iostream_withassign: classes derivadas respectivamente de istream, ostream e iostream, que oferecem o operador de atribuição.

Os fluxos padrões cin, cout et cerr são instâncias dessas classes.

As classes declaradas em fstream.h permitem a manipulação de arquivos em disco:

PRGfluxo2.png
  • fstreambase : classe base para as classes derivadas ifstream, ofstream e fstream. Ela

mesma é derivada de ios e contém um objeto da classe filebuf.

  • ifstream : classe que permite realizar entrada a partir de arquivos.
  • ofstream : classe que permite efetuar saída em arquivos.
  • fstream : classe que permite realizar entrada/saída em arquivos.

As classes declaradas em strstream.h permitem simular operações de entrada e saída a partir de buffers na memória principal. Elas operam da mesma maneira que as funções sprintf() e sscanf() da linguagem C.

PRGfluxo3.png
  • strtreambase : classe base para as classes seguinte. Ela contém um objeto da classe

strstreambuf (derivada de streambuf).

  • istrstream : derivada de strtreambase e de istream, permite leitura a partir de um

buffer, da mesma maneira que a função sscanf().

  • ostrstream : derivada de strtreambase e de ostream, permite escrita em buffer, do

mesmo modo que a função sprintf().

  • strstream : derivada de istrstream e de iostream, permite leitura e escrita em buffer.

O fluxo de saída padrão

O fluxo de saída padrão, ostream, permite:

  • saída formatada e não formatada (em um streambuf)
  • sobrecarga do operador de inserção <<.
  • exibir tipos predefinidos da linguagem C++.
  • exibir tipos definidos pelo programador.

O exemplo a seguir ilustra a sobrecarga do operador de inserção em fluxo (>>) para a classe de números complexos, vista anteriormente:

Exemplo:

#include <iostream.h>
#include <math.h>
class Complexo {
 public:
  Complexo(double a, double b) { r=a; i=b; }
  Complexo operator+(Complexo c) { return Complexo(r+c.r,i+c.i); }
  friend ostream & operator<<(ostream &out,const Complexo &x) {
    return out << x.r << (x.i<0 ? "-" : "+") << fabs(x.i) << "i";
  }
 private:
   double r; // parte real
   double i; // parte imaginária
};
void main(void)
{
  Complexo a(1,2), b(3,-4);
  cout << a+b << endl;
}


A função operator<< retorna uma referência ao objeto ostream para o qual foi chamada. Isso permite chamá-la, de forma encadeada, várias vezes numa mesma instrução.

Exemplo:

void main(void)
{
  Complexo a(1,2), b(3,-4);
  cout << a << “ + ” << b << “ = “ << a+b << endl;
}

Além do operador de inserção (<<), a classe ostream possui os seguintes métodos:

ostream & put(char c) insere um caracter num fluxo
ostream & write(const char *, int n) insere n caracteres num fluxo
streampos tellp() retorna a posição corrente dentro do fluxo
ostream & seekp(streampos n) posiciona-se a n bytes a partir do início

do arquivo. O tipo streampos corresponde à uma posição do arquivo, que inicia-se em 0.

ostream & seekp(streamoff d, seek_dir r) Posiciona-se a d bytes a partir do início

do arquivo (r = beg), a posição corrente (r = cur) ou o final do fluxo (r = end)

ostream & flush() esvazia o buffer do fluxo



Exemplo:

#include <ostream.h>
void main(void)
{
   cout.write("Hello world!", 12);
   cout.put('\n');
}

O fluxo de entrada padrão

O fluxo de entrada padrão, istream, permite:

  • entrada formatada e não formatada (em um streambuf)
  • sobrecarga do operador de extração >>.
  • entrada de tipos predefinidos da linguagem C++.
  • entrada de tipos definidos pelo programador.

O próximo exemplo mostra a sobrecarga do operador >> para a classe Complexo:

Exemplo:

class Complexo {
friend istream & operator>>(istream &in, Complexo &x);
...
};

Assim como no caso de inserção, para que o operador de extração possa ser usado de maneira encadeada, a função operator>> também deve retornar uma referência ao objeto istream a partir do qual ela foi chamada.

Exemplo:

istream operator>> istream & operator>>(istream &in, Complexo &x)
{
  in >> x.r;
  char sinal;
  in >> sinal;
  assert( sinal=='+' || sinal=='-' );
  in >> x.i;
  if( sinal=='-' ) x.i *= -1;
  char i;
  in >> i;
  if( i!='i' ) x.i=0;
  return in;
}
void main(void)
{
   Complexo x;
   cout << "Digite um número complexo: ";
   cin >> x;
   ...
}

Além do operador de extração, a classe istream oferece os seguintes métodos:

int get() extrai e retorna um caracter (ou EOF)
istream & get(char &c) extrai e armazena em c um caracter
int peek() informa o próximo caracter, sem extraí-lo
istream & get(char *ch, int n, char d='\n') extrai n-1 caracteres do fluxo e os armazena a partir do endereço ch, parando assim que o delimitador é encontrado.
Istream & getline(char *ch, int n, char d='\n') como o método anterior, exceto que o delimitador é extraído e descartado
istream & read(char *ch, int n) extrai um bloco de pelo menos n bytes do fluxo e os armazena a partir do endereço ch. O número de bytes efetivamente lidos é obtido através do método gcount()
int gcount() retorna o número de caracteres extraídos na última leitura
streampos tellg() retorna a posição corrente dentro do fluxo
istream & seekg(streampos n) posiciona-se a n bytes a partir do início do arquivo. O tipo streampos corresponde à uma posição do arquivo, iniciando em 0.
istream & seekg(streamoff d, seek_dir r) Posiciona-se a d bytes a partir do início do arquivo (r = beg), a posição corrente (r = cur) ou o final do fluxo (r = end)
istream & flush() esvazia o buffer do fluxo


Controle de estado de um fluxo

A classe ios descreve os aspectos comuns aos fluxos de entrada e saída.

É uma classe base virtual para todos os fluxos e, portanto, não podemos jamais criar um objeto dessa classe. Porém, podemos utilizar seus métodos para testar o estado de um fluxo ou para controlar o formato das informações.

Além disso, essa classe oferece uma série de constantes muito úteis na manipulação de fluxos.


A classe ios oferece os seguintes métodos:

int good() retorna um valor diferente de zero se a última operação de E/S teve sucesso
int fail() faz o inverso do método anterior
int eof() retorna um valor diferente de zero se o final do arquivo foi atingido
int bad() retorne um valor diferente de zero se uma operação foi malsucedida
int rdstate() retorna o valor da variável de estado do fluxo ou zero se tudo estiver bem
void clear() zera o indicador de erros do fluxo



Associando um fluxo a um arquivo

É possível criar um fluxo associado a um arquivo em disco. As três classes que permitem acessar arquivos em disco são definidas em fstream.h:

  • ifstream: permite acessar arquivos para leitura.
  • ofstream: permite acessar arquivos para gravação.
  • fstream: permite acessar arquivos para leitura e gravação.

Cada uma dessas classes usa um buffer da classe filebuf para sincronizar as operações entre o fluxo e o disco.

A classe fstreambase oferece uma coleção de métodos comuns a essas três classes; em particular, o método open(), que abre um arquivo em disco e o associa a um fluxo:

void open(const char *nome, int modo, int prot=filebuf::openprot);

Nessa sintaxe, nome refere-se ao arquivo que será associado ao fluxo, modo indica o modo de seu abertura e prot define os direitos de acesso ao arquivo (por default, os direitos de acesso são estabelecidos pelo sistema operacional).

O modo de abertura deve ser um dos elementos da enumeração a seguir:

enum open_mode {
 app, // anexa os dados ao final do arquivo
 ate, // posiciona-se no final do arquivo
 in, // abre para leitura (é o default para ifstream)
 out, // abre para gravação (é o default para ofstream)
 binary, // abre em modo binário (o default é o modo texto)
 trunc, // destrói o arquivo se ele existe e o recria
 nocreate, // se o arquivo não existe, a abertura falha
 noreplace // se o arquivo existe, a abertura falha
};


Para a classe ifstream o modo de abertura default é ios::in e para a classe ofstream, é ios::out.


Exemplo:

#include <fstream.h>
ifstream in;
in.open("teste.tmp"); // abre para leitura, por default
ofstream out;
out.open("teste.tmp"); // abre para gravação, por default
fstream io;
io.open("teste.tmp", ios::in | ios::out); // abre para leitura e gravação

Podemos também chamar os construtores dessas três classes e combinar as operações de criação e abertura de fluxo numa única instrução:

Exemplo:

#include <fstream.h>
ifstream in("teste.tmp"); // abre para leitura, por default
ofstream out("teste.tmp"); // abre para gravação, por default
fstream io("teste.tmp", ios::in | ios::out); // abre para leitura e gravação

Formatação de dados

Cada fluxo conserva permanentemente um conjunto de indicadores especificando a formatação corrente. Isso permite dar um comportamento default ao fluxo, ao contrário do que ocorre com as funções printf() e scanf() da linguagem C, que exigem a indicação de um formato para cada operação de E/S realizada.

O indicador de formato do fluxo é um inteiro longo (protegido) definido na classe ios:

class ios {
  public:
  // ...
  enum {
   skipws, // ignora os espaços na entrada
   left, // justifica as saídas à esquerda
   right, // justifica as saídas à direita
   internal, // preenche entre o sinal e o valor
   dec, // conversão em decimal
   oct, // conversão em octal
   hex, // conversão em hexadecimal
   showbase, // exibe o indicador de base
   showpoint, // exibe ponto decimal nos números reais
   uppercase, // exibe hexadecimais em maiúsculas
   showpos, // exibe sinal em números positivos
   scientific, // notação científica (1.234000E2) para reais
   fixed, // notação fixa (123.4) para reais
   unitbuf, // esvazia o fluxo após uma inserção
   stdio // permite utilizar stdout et cout
};
//...
protected:
long x_flags; // indicador de formato
//...
};


A classe ios também define constantes através das quais acessamos os indicadores:

  • static const long basefield : permite definir a base (dec, oct ou hex)
  • static const long adjustfield :permite definir o alinhamento (left, right ou internal)
  • static const long floatfield :permite definir a notação para reais (scientific ou fixed)

Os métodos seguintes (também definidos em ios) permitem ler ou modificar os valores dos indicadores de formato:

long flags() retorna o valor do indicador de formato
long flags(long f) modifica os indicadores com o valor de f
long setf(long setbits, long field) altera somente os bits do campo indicado por field
long setf(long f) modifica o valor do indicador de formato
long unsetf(long) zera o valor do indicador de formato



Exemplo:

#include <iostream.h>
#include <iomanip.h>
void main(void)
{
   cout.setf(ios::dec,ios::basefield);
   cout << 255 << endl;
   cout.setf(ios::oct,ios::basefield);
   cout << 255 << endl;
   cout.setf(ios::hex,ios::basefield);
   cout << 255 << endl;
}

A execução desse programa produzirá a seguinte saída:

255
377
ff

Os métodos a seguir permite definir tamanho de campo e caracter de preenchimento:

Int width(int) define a largura do campo para a próxima saída
Int width() devolve a largura do campo de saída
char fill(char) define o caracter de preenchimento de campo
char fill() devolve o caracter de preenchimento de campo
Int precision(int) define o número de caracteres (menos o ponto) que um real ocupa
Int precision() devolve o número de caracteres (menos o ponto) que um real ocupa



Exemplo:

#include <iostream.h>
#include <iomanip.h>
void main(void)
{
   cout << "(";
   cout.width(4);
   cout.setf(ios::right,ios::adjustfield);
   cout << -45 << ")" << endl;
   cout << "(";
   cout.width(4);
   cout.setf(ios::left,ios::adjustfield);
   cout << -45 << ")" << endl;
   cout << "(";
   cout.width(4);
   cout.setf(ios::internal,ios::adjustfield);
   cout << -45 << ")" << endl;
}

Esse outro exibirá o seguinte:

( -45)
(-45 )
(- 45)


Exemplo:

#include <iostream.h>
#include <iomanip.h>
void main(void)
{
   cout.setf(ios::scientific,ios::floatfield);
   cout << 1234.56789 << endl;
   cout.precision(2);
   cout.setf(ios::fixed,ios::floatfield);
   cout << 1234.56789 << endl;
}

A saída exibe o valor em notação científica e em notação fixa:

1.2345678e+03
1234.57

Os manipuladores

O uso dos métodos de formatação leva a instruções um tanto longas.

Por outro lado, usando manipuladores podemos escrever um código mais compacto e mais legível.

Em vez de escrever:

cout.width(10);
cout.fill('*');
cout.setf(ios::hex,ios::basefield);
cout << 123 ;
cout.flush();

vamos preferir a instrução equivalente:

cout << setw(10) << setfill('*') << hex << 123 << flush;

As classes ios, istream e ostream implementam os manipuladores predefinidos.

O arquivo de inclusão iomanip.h define um certo número desses manipuladores:


dec a próxima operação de E/S usa base decimal oct a próxima operação de E/S usa base octal hex a próxima operação de E/S usa base hexadecimal endl escreve ‘\n’ e depois descarrega o buffer ends escreve ‘\0’ e depois descarrega o buffer flush descarrega o buffer ws descarta os espaços num fluxo de entrada setbase(int b) define a base para a próxima saída setfill(int c) define o caracter de preenchimento de campo setprecision(int p) define o número de dígitos na próxima saída de número real setw(int l) define a largura do campo para a próxima saída setiosflags(long n) ativa os bits do indicador de formato especificado por n resetiosflags(long b) desativa os bits do indicador de formato indicado por b


Podemos também escrever nossos próprios manipuladores.

Por exemplo, um dos manipuladores mais utilizados, endl, é definido da seguinte maneira:

Exemplo:

ostream &flush(ostream &os) { return os.flush(); }
ostream &endl(ostream &os) { return os << '\n' << flush; }

Para utilizá-lo numa instrução do tipo:

cout << endl;

a função operator<< é sobrecarregada na classe ostream como:

Exemplo:

ostream &ostream::operator<<(ostream& (*f)(ostream &))
{
   (*f)(*this);
   return *this;
}

A função operator<< acima recebe como parâmetro um ponteiro para a função que implementa o manipulador a ser executado. Através desse ponteiro a função que implementa o manipulador é chamada e, depois, o objeto ostream é devolvido para que o operador possa ser usado de forma encadeada.

Vamos implementar, como exemplo, um manipulador para tabulação:


Exemplo:

ostream &tab(ostream &os)
{
   return os << '\t';
}

Esse manipulador poderá ser usado assim:

cout << 12 << tab << 34;


Criando manipuladores

A implementação de um novo manipulador consiste em duas partes:

  • manipulador: sua forma geral para ostream é:
ostream &<manipulador>(ostream &,<tipo>);

sendo <tipo> o tipo do parâmetro do manipulador. Essa função não pode ser chamada diretamente por uma instrução de E/S, ela será chamada somente pelo aplicador.

  • aplicador: ele chama o manipulador. É uma função global cuja forma geral é:
<x>MANIP( <tipo> )<manipulador>(<tipo> <arg>)
{
   return <x>MANIP(<tipo>) (<manipulador>,<arg>);
}

com <x> valendo O para manipuladores de ostream (e seus derivados), I, S e IO para, respectivamente, para manipuladores de istream, ios e iostream.

Como exemplo, vamos criar um manipulador para exibir um número em binário:

Exemplo:

#include <iomanip.h>
#include <limits.h> // ULONG_MAX
ostream &bin(ostream &os, long val)
{
   unsigned long mascara = ~(ULONG_MAX >> 1);
   while ( mascara ) {
       os << ((val & mascara) ? '1' : '0');
       mascara >>= 1;
   }
   return os;
}
OMANIP(long) bin(long val)
{
   return OMANIP(long) (bin, val);
}
void main()
{
   cout << "2000 em binário é " << bin(2000) << endl;
}