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

De IFSC
Revisão de 11h59min de 6 de junho de 2018 por imported>Fargoud (→‎String Streams)
Ir para navegação Ir para pesquisar

Fluxos

C++ oferece diversos recursos para acessar arquivos, ou mais genericamente streams.

O nome stream representa um fluxo de entrada ou de saída, e pode ser entendido de diversas formas.

Por exemplo, cout é um fluxo de saída e cin é um fluxo de entrada, ambos associados com o terminal.

Da mesma forma, pode-se usar fluxos de saida para strings, por exemplo, que permitem compor strings como se estivéssemos escrevendo na tela.

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.

String Streams

String stream é um recurso que permite escrevermos em uma string como se estivéssemos realizando saída na tela (ex: via cout).

Dessa forma, é possível utilizar todas as funcionalidades da iostream (formatos, alinhamento, precisão numérica, tamanho de campo, etc) em uma string.

É útil, por exemplo, para retornarmos informações sobre um objeto sob forma de string, especialmente quando é preciso combinar dados numéricos com dados não-numéricos.

Para utilizar o recurso de string stream, é necessário primeiramente incluir o header sstream.

A partir daí, deve-se declarar um objeto do tipo ostringstream (output string stream) e utilizá-lo como se fosse uma saída padrão, ou seja, da mesma forma que cout:

Finalmente, para se obter a string armazenada no objeto ostringstream usa-se o método str:

#include <iostream>
#include <iomanip>
#include <cmath>
#include <sstream>
using namespace std;
string converte(float valor)
{
 ostringstream aux;  // Declara o string stream de saída chamado aux
 aux << "Raiz do valor digitado (Exemplo de saída em string) ";  // Escreve em aux
 aux << fixed << setprecision(2) << sqrt(valor); // idem
 return aux.str(); // Retorna a string resultante a partir do stream aux
}
int main()
{
 float v;
 cout << "Digite o valor: ";
 cin >> v;
 cout << converte(v);
}


Note que se o objetivo for apenas concatenar (agrupar) duas ou mais strings, não é necessário usar string stream: basta empregar o operador de concatenação (+).

É muito comum a criação de um método denominado toString, que tem o papel de retornar uma representação do objeto armazendo como uma string. Por exemplo:

#include <iostream>
#include <sstream>
#include <string>
class Pessoa
{
  private:
    string nome;
    string CPF;
    int idade;
  public:
     Pessoa(string n, string cpf, int i) {
       ...
     }
     ...
     string toString();
     ...
};
...
string Pessoa::toString() {
    // Cria um fluxo de saída em uma string
    ostringstream aux;
    // "Escreve" os dados no fluxo
    aux << nome << " - " << CPF << " - " << idade;
    // Retorna string contida no fluxo
    return aux.str();
}

Porém, o mais usual é entender-se fluxos como arquivos físicos, como veremos a seguir.

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;
}




Funções Virtuais e Polimorfismo << AULA 15 - Streams

<<= Página do curso