Introdução C++

De MediaWiki do Campus São José
Revisão de 12h43min de 4 de setembro de 2015 por Jean.ss (discussão | contribs)
Ir para navegação Ir para pesquisar

Bibliografia

Introdução

A linguagem de programa C++ estende a linguagem C, de forma a aderir ao paradigma de orientação a objetos. Com isso, a linguagem oferece construções que possibilitam a expressão de programas modelados segundo uma análise orientada a objetos. A linguagem se apresenta como um superconjunto da linguagem C, o que significa que um programa em C pode ser visto também como um programa C++, mas não o contrário.

Este primeiro exemplo apresenta o clássico programa de demonstração hello world escrito em C++:

#include <iostream>

using namespace std;

int main() {
  cout << "Hello world !" << endl;

  return 0;
}

Para compilar este exemplo, deve-se gravá-lo em um arquivo com extensão .cc ou .cpp. O compilador C++ disponível no Linux se chama g++, sendo da mesma família de compiladores do gcc. Supondo que o nome do arquivo seja hello.cc, pode-se compilá-lo assim:

g++ -o hello hello.cc

O comando acima deve gerar um arquivo executável chamado hello. Para executá-lo, faça o seguinte:

./hello

O exemplo acima pode ser estendido para mostrar como ler dados do teclado:

#include <iostream>
#include <stdio.h>
 
using namespace std;
 
int main() {
  int x;

  cout << "Hello world !" << endl;
  cout << "x: ";
  cin >> x;
  cout << "Voce digitou " << x << endl; 
  return 0;
}

Classes e objetos

A linguagem C foi projetada segundo um paradigma de programação chamado de programação estruturada. Essa forma de expressar programas se baseia em subrotinas (funções no caso da linguagem C), blocos de instruções e laços de repetição, os quais atuam sobre variáveis que contêm os dados a serem acessados e transformados. O que se deve notar é a forma com que se costuma modelar um programa nesse caso, dando-se ênfase aos algoritmos que modificam os dados. Não é à toa que uma ferramenta de modelagem tradicional e ainda muito usada seja o fluxograma. Porém existem outras formas de pensar os problemas a serem resolvidos e expressar programas, dentre elas o paradigma de orientação a objetos seguido por diversas linguagens tais como C++, Java e Python.


Um programa orientado a objetos é composto por objetos que interagem. Um objeto é uma abstração que representa uma coisa que faz parte do problema a ser resolvido. Um objeto contém dados que representam seu estado (chamados de atributos), e procedimentos que representam seu comportamento (chamados de métodos). No caso do exemplo dos horários, um horário pode ser representado por um objeto. O estado desse objeto Horario é definido pelos valores de hora, minuto e segundo, e seu comportamento é dado pelos métodos mostreSe e acrescenta. Isso deve ser entendido da seguinte forma: um objeto 'Horario pode ser mostrado, ao se chamar seu método 'mostreSe, e pode ser somado a outro objeto Horario, ao se chamar seu método acrescenta. Essas são as únicas operações definidas para esse objeto, e assim nada mais se consegue fazer com ele ... a não ser que se definam novas operações.


Muitos objetos similares podem existir. Por exemplo, pode haver muitos diferentes objetos Horario, que se diferenciam por seus estados (valores de hora, minuto e segundo). Todos os objetos cujos estados são definidos pelo mesmo conjunto de atributos, e que possuem o mesmo comportamento (conjunto de métodos), pertencem à mesma classe. O conceito de classe é semelhante ao de tipo de dados, porém tem uma implicação mais profunda. Um tipo de dados define como o conteúdo de uma variável deve ser interpretado (ex: um número inteiro, ponto flutuante, uma string, ...), porém não define os procedimentos que podem manipular essas variáveis. Desta forma, uma classe vincula a seus objetos os algoritmos que atuam sobre eles. Isso tem uma importante implicação no acoplamento e coesão de um programa, que são métricas fundamentais de qualidade de um projeto de software modular.


Mas isso está um tanto abstrato ... o exemplo abaixo pode esclarecer melhor os conceitos de classe e objeto. Ele declara uma classe Vetor, que representa vetores bidimensionais. As operações sobre tais objetos são: módulo, produto escalar, subtração e escrita em um arquivo. Veja como pode ser escrita essa classe.

// a classe Vetor define objetos que representam vetores bidimensionais
class Vetor {
 private: // as declarações abaixo são privativas (podem ser acessadas somente dentro da classe)

  double x, y;

 public: // as declarações abaixo são públicas, podendo ser acessadas de fora da classe

  // construtor da classe: um método especial que cria objetos desta classe
  Vetor(double px, double py) {
    // inicia o estado de um objeto
    x = px;
    y = py;
  }

  // destrutor da classe: um método especial que destrói objetos desta classe
  ~Vetor() {} // nada demais precisa ser feito

  // este método retorna o módulo do vetor
  double modulo() {
    return sqrt(x*x + y*y);
  }

  // este método retorna o produto escalar deste vetor com outro vetor
  double produto_escalar(const Vetor & outro) {
    return x*outro.x + y*outro.y;
  }

  // este método calcula a soma entre este e outro vetor (este + outro),
  // retornando um novo vetor como resultado
  Vetor adicione(const Vetor & outro) {
    Vetor novo(x+outro.x, y+outro.y);

    return novo;
  }

  // este método calcula a diferença entre este e outro vetor (este - outro),
  // retornando um novo vetor como resultado
  Vetor subtraia(const Vetor & outro) {
    Vetor novo(x-outro.x, y-outro.y);

    return novo;
  }

  // este método escreve uma representação do objeto no arquivo "out"
  // (que é um objeto da classe ostream)
  void mostreSe(ostream & out) {
    out << "(";
    out << x;
    out << ", ";
    out << y;
    out << ")";
  }

};

Uma vez existindo a classe Vetor, pode-se escrever um programa que use objetos dessa classe. No exemplo abaixo, calcula-se o vetor resultante de dois outros vetores:

int main() {
  int x, y;

  cout << "Coordenada X do vetor 1: ";
  cin >> x;
  cout << "Coordenada Y do vetor 1: ";
  cin >> y;
  cout << endl;

  // Aqui se cria o primeiro objeto Vetor
  Vetor v1(x, y);

  cout << "Coordenada X do vetor 2: ";
  // Aqui se lê pela entrada padrão a coordenada X
  cin >> x;
  cout << "Coordenada Y do vetor 2: ";
  // Aqui se lê pela entrada padrão a coordenada Y
  cin >> y;
  cout << endl;

  // Aqui se cria o segundo objeto Vetor
  Vetor v2(x, y);

  // Aqui se obtém a resultante dos vetores v1 e v2. Note como a operação "adicione"
  // do objeto "v1" é chamada.
  Vetor v3 = v1.adicione(v2);

  cout << "Vetor resultante: ";
  // Aqui se escreve uma representação de "v3" na tela. "cout" é um objeto da biblioteca C++ padrão
  // que representa a saída padrão de um processo. A saída padrão funciona como um arquivo. O objeto 
  // "cout" é similar ao "stdout" da linguagem C.
  v3.mostreSe(cout);
  cout << endl;
}

O programa em si se apresenta como um pequeno algoritmo que lê pela entrada padrão valores de coordenadas e cria os respectivos vetores (v1 e v2), calculando sua resultante a apresentando na saída padrão o novo vetor (v3).


Além das regras de escrita da linguagem, o que se denomina sintaxe, ressalta-se o uso dos objetos feito no programa. O pequeno programa criado trata de calcular a resultante de dois vetores bidimensionais. As coisas a serem diretamente manipuladas pelo programa são os vetores, sendo estes representado por objetos. Outros objetos usados são os arquivos que representam a entrada e saída padrão do programa (respectivamente cin e cout). O programa usa os objetos da classe Vetor, explorando as operações que eles oferecem. Assim, a declaração da classe Vetor se preocupa em definir o que é um objeto Vetor (seus atributos) e o que ele é capaz de fazer (seu comportamento). Já o programa apenas utiliza esses objetos.

Templates

Templates possibilita a parametrização de tipos em C++. Com isso, podem-se escrever funções, structs e classes que referenciam ou armazenam dados cujos tipos sejam desconhecidos. No nosso caso, isso será usado para generalizar as estruturas de dados. Port exemplo, a lista pode ficar assim:


// Declaração de um template de uma struct: inicia-se com "template <class T>",
// sendo "T" o tipo de dados parametrizado.
template <class T> struct Nodo {
  Nodo * proximo;
  T dado; // aqui se usa o tipo parametrizado

  Nodo(T &umDado) { // aqui também
    dado = umDado;
    proximo = NULL;
  }
};

// Template de uma classe: também inicia com "template <class T>"
template <class T> class Lista {
 public:
  Lista();
  ~Lista();
  // notar abaixo o uso do tipo parametrizado
  void adiciona(T &dado);
  void anexa(T &dado);
  void insere(T &dado, int pos);
  T remove(int pos);
  T& obtem(int pos);
  T& iniciaIteracao();
  T& proximo();
  int comprimento();
  void esvazia();
  void esvazia(bool tudo);
  bool vazia();
  void ordena(Comparador<T> compare);
 protected:
  int N;
  Nodo<T> * primeiro, * ultimo, * atual;
};

// demonstração da implementação do construtor  da Lista.
// As implementações dos métodos (funções-membros) devem também ser
// prefixadas com "template <class T>".
template <class T> Lista<T>::Lista() {
  primeiro = NULL;
  ultimo = NULL;
  atual = NULL;
  N = 0;
}

// Definição de um método ... novamente, notar o prefixo "template <class T>"
template <class T> bool Lista<T>::vazia() {
  return N == 0;
}

Um template define um padrão de geração de código para o compilador (maiores detalhes). Quer dizer, um template em si não é compilável, pois ele representa código-fonte ainda incompleto. No entanto, no momento em que se usa um template, o código -fonte é completado e pode ser compilado. Em outras palavras, o template da lista se torna uma Lista de fato. O programa de teste abaixo mostra a criação de duas listas: uma que armazena strings e outra que armazena inteiros.

#include <iostream>
#include "lista.h"

using namespace std;

int main() {
  // Uma lista de strings
  Lista<string> lista1;
  
  // Uma lista de inteiros
  Lista<int> lista2;
  
  // Armazena três strings na lista de strings
  for (int x=0; x < 3; x++) {
    string palavra;
    
    cout << "Digite uma palavra: ";
    cin >> palavra;
    lista1.anexa(palavra);
  }
  
  // Armazena alguns números na lista de inteiros
  for(int x=0; x < 100; x += 5) lista2.anexa(x);
  
  // Mostra os dados da lista de strings
  for (int x=0; x < lista1.comprimento(); x++) {
    cout << "lista1[" << x << "]=" << lista1[x] << endl;
  }
  
  // Mostra os dados da lista de inteiros
  for (int x=0; x < lista2.comprimento(); x++) {
    cout << "lista2[" << x << "]=" << lista2[x] << endl;
  }

}

Aproveitando o momento: operador de atribuição

Em C++ pode-se definir como um operador específico atua sobre um objeto. Operadores típicos são = (atribuição), + (adição), - (subtração), e outros. Um operador é definido por uma função ou método que implementa sua operação. No caso da Fila, um operador útil é o de atribuição, pois pode ser usado para que uma fila existente seja modificada para se tornar idêntica a outra fila. Supondo que esse operador tenha sido implementado, o exemplo a seguir mostra a fila f1 sendo atribuída a f2 (linha 19), a qual já existe e tem inclusive dados armazenados, e demonstra em seguida como ficou o conteúdo de f2.


#include <iostream>
#include "fila.h"

using namespace std;

int main() {
  Fila f1(5);
  Fila f2(7);

  f1.enfileira(2);
  f1.enfileira(7);
  f1.enfileira(4);

  f2.enfileira(3);

  cout << "Comprimento de f1: " << f1.comprimento() << endl;
  cout << "Comprimento de f2: " << f2.comprimento() << endl;

  f2 = f1; // atribui f1 a f2 ... f2 torna-se uma cópia de f1

  cout << "Comprimento de f1: " << f1.comprimento() << endl;
  cout << "Comprimento de f2: " << f2.comprimento() << endl;

  while (not f1.vazia()) cout << "Desenfileirou de f1: " << f1.desenfileira() << endl;
  
  cout << "Comprimento de f1: " << f1.comprimento() << endl;
  cout << "Comprimento de f2: " << f2.comprimento() << endl;

  while (not f2.vazia()) cout << "Desenfileirou de f2: " << f2.desenfileira() << endl;
}


O operador de atribuição da fila pode ser declarado como mostrado abaixo:

class Fila {
 public:
  Fila(int umaCapacidade); // cria uma fila capaz de guardar "capacidade" elementos
  Fila(const Fila & outra);
  ~Fila(); // destroi a fila
  Fila& operator=( const Fila& outra ); // OPERADOR DE ATRIBUIÇÃO
  // demais declarações da Fila ...
};


Como se pode notar, sua declaração é muito parecida com a de um método qualquer da classe. A palavra-chave operator indica tratar-se de um método de operador, e o símbolo que aparece logo em seguida indica o operador a ser redefinido (no caso, =). O único parâmetro a ser passado é o objeto a ser copiado (no caso, uma Fila). O valor retornado é uma referência ao próprio objeto que foi o destino da atribuição (aquele que foi modificado). A implementação desse método segue as regras usadas nos demais métodos:

Fila& Fila::operator=( const Fila& outra ) {
  // desalocar a memória de "area"
  // copiar os valores de atributos de "outra"
  // alocar memória para "area"
  // copiar conteúdo da "area" da "outra" fila
  
  // retorna uma referência ao próprio objeto 
  return *this;
}

A implementação do método do operador de atribuição em fila.cc

Passagem de parâmetros

À medida que os projetos são desenvolvidos, são apresentados novos recursos da linguagem C++. Hoje as novidades foram:

Passagem por valor Passagem por referência
#include <iostream>

using namespace std;

int incrementa(int x) {
  x++;
  return x;
}

int main() {
  int valor = 10;
  int resultado;

  resultado = incrementa(valor);
  cout << "Resultado=" << resultado;
  cout << ", e valor=" << valor << endl;
}
#include <iostream>

using namespace std;

int incrementa(int & x) {
  x++;
  return x;
}

int main() {
  int valor = 10;
  int resultado;

  resultado = incrementa(valor);
  cout << "Resultado=" << resultado;
  cout << ", e valor=" << valor << endl;
}



Programa 1 Programa 2
#include <iostream>

using namespace std;

int incrementa(const int & x) {
  x++;
  return x;
}

int main() {
  int valor = 10;
  int resultado;

  resultado = incrementa(valor);
  cout << "Resultado=" << resultado;
  cout << ", e valor=" << valor << endl;
}
#include <iostream>

using namespace std;

int incrementa(const int & x) {
  int y = x;

  y++;
  return y;
}

int main() {
  int valor = 10;
  int resultado;

  resultado = incrementa(valor);
  cout << "Resultado=" << resultado;
  cout << ", e valor=" << valor << endl;
}

streams e arquivos

Em diversas linguagens de programação o conceito de stream (correnteza) é utilizado para o acesso a arquivos em disco. Por stream entende-se um mecanismo e leitura e escrita em que dados fluem sucessivamente. Dessa forma, ao se escrever em um arquivo fazem-se sucessivas operações de escrita, e os dados são armazenados na ordem em que essas operações se realizaram. Algo parecido acontece na leitura de um arquivo, em que as operações de leitura realizadas fornecem dados de acordo com a ordem em que estão gravados no arquivo.


O acesso a arquivos apresenta algumas diferenças nas linguagens C e C++. Em C++ existem algumas classes para a manipulação de arquivos. Desta forma, arquivos nesta linguagem são acessados por meio de objetos dessas classes. No exemplo abaixo, apresentam-se dois programas em C e C++ para ler as linhas de um arquivo e mostrá-las na tela.


Linguagem C Linguagem C++
#include <stdio.h>
#include <errno.h>

#define MAX_SIZE 10240

int main() {
  FILE * arq;
  char linha[MAX_SIZE];

  // abre o arquivo
  arq = fopen("/etc/hosts", "r");

  // testa se conseguiu abrir o arquivo
  if (arq == NULL) {
    perror("Ao abrir /etc/hosts");
    return errno;
  }

  // lê cada linha do arquivo, mostrando-a na tela
  while (fgets(linha, MAX_SIZE, arq)) {
    printf("%s", linha);
  }

  // fecha o arquivo
  fclose(arq);
}
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <errno.h>
 
#define MAX_SIZE 10240

using namespace std;
 
int main() {
  // cria o objeto "arq" da classe "ifstream", que representa um arquivo para leitura
  // a criação desse objeto já abre o arquivo
  ifstream arq("/etc/hosts");
 
  // verifica se o arquivo foi de fato aberto
  if (not arq.is_open()) {
    perror("Ao abrir /etc/hosts");
    return errno;
  }

  // lê cada linha do arquivo, apresentando-a na tela
  while (not arq.eof()) {
    char linha[MAX_SIZE];
 
    arq.getline(linha, MAX_SIZE); // equivalente ao "fgets"
    cout << linha << endl;
  }
  // observe que o arquivo não precisa ser fechado ... isso ocorre automaticamente
  // quando o objeto for destruído, ficando a cargo de seu destrutor
}


Os exemplos acima demonstram que a sintaxe no acesso a arquivos pode ser diferente, porém funcionalmente parece haver uma equivalência entre as duas formas de acesso. De fato tudo que se consegue fazer em C++ é possível fazer em C, mudando apenas a praticidade ou conveniência na forma com que se expressa isso nessas linguagens. Porém como a linguagem C já é bem conhecida por vocês, interessa ver o que se pode fazer em C++.

Em C++ existem três classes para acessar arquivos:

  • ofstream: acesso a arquivos em modo escrita.
Exemplo
#include <iostream>
#include <fstream>

using namespace std;

int main() {
  ofstream arq("demo.txt");

  if (not arq.is_open()) {
    cerr << "Algum erro ao abrir o arquivo ..." << endl;
    return 0;
  }

  arq << "Iniciando uma gravação de várias linhas inúteis ..." << endl;

  for (int i=0; i < 10; i++) {
    arq << "Linha " << i << endl;    
  }
}
  • ifstream: acesso a arquivos em modo leitura.
Exemplo
#include <iostream>
#include <fstream>

using namespace std;

int main() {
  ifstream arq("/etc/hosts");

  if (not arq.is_open()) {
    cerr << "Algum erro ao abrir o arquivo ..." << endl;
    return 0;
  }

  while (not arq.eof()) {
    string algo;

    arq >> algo;
    cout << "Leu isto: " << algo << endl;
  }
}
  • fstream: acesso a arquivos em modo leitura/escrita.
Exemplo
#include <iostream>
#include <fstream>

using namespace std;

#define MAX_LINE 10240

int main() {
  fstream arq("demo.txt");

  if (not arq.is_open()) {
    cerr << "Algum erro ao abrir o arquivo ..." << endl;
    return 0;
  }

  for (int i=0; i < 10; i++) {
    arq << "Linha " << i << endl;    
  }
  arq.flush();

  arq.seekg(0);
  
  while (not arq.eof()) {
   char linha[MAX_LINE];

   arq.getline(linha, MAX_LINE);
   cout << "Leu: " << linha << endl;
  }
}


O interessante em C++ é que a saída padrão e a entrada padrão são representados por objetos stream. De fato, veja como estão declarados esses objetos na biblioteca C++ padrão:

  • cout: saída padrão
  • cin: entrada padrão
  • cerr: saída de erros padrão

Acessando strings como se fossem arquivos

Em C++ esse conceito de stream é levado um pouco além. Além de arquivos e a entrada e saída padrão, é possível também manipular strings como se fossem streams. Quer dizer, pode-se escrever em uma string (criá-la) ou extrair partes de uma string como se estivesse acessando um arquivo. Isso possibilita que funções ou objetos que escrevem ou lêem em arquivos possam fazer o mesmo em memória, o que pode ser útil em diferentes programas.

O uso de strings como se fossem streams é implementado por três classes:


O exemplo mais simples envolve criar uma string a partir dos valores de algumas variáveis. Veja como isso era feito em C e como se faz em C++:

Linguagem C Linguagem C++
#include <stdio.h>

int main() {
  int dia, mes, ano;
  char data[32];

  printf("Dia: ");
  scanf("%d", &dia);
  printf("Mes: ");
  scanf("%d", &mes);
  printf("Ano: ");
  scanf("%d", &ano);

  snprintf(data, 32, "%d/%d/%d", dia, mes, ano);

  printf("Data: %s\n", data);

}
#include <iostream>
#include <sstream>

using namespace std;

int main() {
  int dia, mes, ano;

  cout << "Dia: ";
  cin >> dia;
  cout << "Mes: ";
  cin >> mes;
  cout << "Ano: ";
  cin >> ano;

  //cria-se uma stringstream para escrita
  ostringstream out;

  // escreve-se na stringstream
  out << dia << "/";
  out << mes << "/";
  out << ano;

  // Aqui se obtém o conteúdo armazenado na stringstream
  cout << "Data: " << out.str() << endl;
}


Estes outros exemplos mostram a extração de dados que existem em strings:

Linguagem C Linguagem C++
#include <stdio.h>
 
int main() {
  int dia, mes, ano;
  char data[32];

  printf("Data (dia/mes/ano): ");
  fgets(data, 32, stdin); // lê uma linha

  // extrai os dados da linha
  sscanf(data, "%d/%d/%d", &dia, &mes, &ano);

  // Aqui se mostram os dados extraídos da linha
  printf("Dia: %d\n", dia);
  printf("Mes: %d\n", mes);
  printf("Ano: %d\n", ano);
}
#include <iostream>
#include <sstream>
 
using namespace std;
 
int main() {
  int dia, mes, ano;
  char data[32];

  cout << "Data (dia/mes/ano): "; 
  cin.getline(data, 32);

  //cria-se uma stringstream para leitura
  istringstream inp(data);
  char separador;

  // lê-se da stringstream
  inp >> dia;
  inp >> separador;
  inp >> mes;
  inp >> separador;
  inp >> ano;
 
  // Aqui se mostram os dados extraídos da stringstream
  cout << "Dia: " << dia << endl;
  cout << "Mes: " << mes << endl;
  cout << "Ano: " << ano << endl;
}

Exercícios sobre arquivos e streams

  1. Crie um programa chamado copiador, que deve ser capaz de copiar um arquivo de texto. Ele deve funcionar desta forma:
    copiador nome_arquivo_original.txt nome_novo_arquivo.txt
    
  2. Crie um programa que substitui por uma outra palavra todas as ocorrências de uma determinada palavra de um arquivo, mostrando o resultado na tela.
  3. Use o método escrevaSe da classe Lista de forma que o conteúdo de uma lista seja transformado em uma string.

Sobrecarga de operador

Para descobrir uma solução para esse problema, deve-se entender como o operador << está definido. E basicamente ele está implementado de duas formas:

  1. Na classe ostream: a classe ostream representa arquivos/streams abertos para escrita, e já possui implementações desse operador para tipos básicos de C++.
  2. Em funções de sobrecarga de operador (operator overloading): podem-se definir funções que implementam esse operador para novos tipos/classes. Por exemplo, existe uma dessas funções para a classe string. Um exemplo de função de sobrecarga de operador para um novo tipo de dados segue abaixo:
    #include <iostream>
    
    using namespace std;
    
    // Define-se um novo tipo de dados
    struct Ponto {
      int x,y;
    };
    
    // Define-se o operador << de ostream  para poder escrever um Ponto.
    // Esse operador mostra o ponto da seguinte forma: (x,y)
    ostream& operator<<(ostream &out, const Ponto& p) {
      out << "(" << p.x << "," << p.y << ")";
      return out;
    }
    
    // Agora pode-se usar o operador << para valor do tipo "Ponto"
    int main() {
      Ponto p1 = {1,1}, p2 = {10,20};
    
      cout << "Ponto 1: " << p1 << endl;
      cout << "Ponto 2: " << p2 << endl;
    }
    

Exceções


Condições de erro podem ser avisadas por meio de exceções. Por exemplo, no caso da lista considera-se erro tentar obter um valor em uma posição inexistente. A operação para obter um dado da lista deve, portanto, comunicar de alguma forma que esse acesso inválido aconteceu. Até o momento, uma situação como essa era resolvida fazendo com que o valor retornado informasse que algo errado ocorreu (no exemplo da lista, retornava-se uma string vazia). Mas o mecanismo de exceções possibilita expressar e tratar essas condições de erro de uma forma mais prática.

O uso de exceções implica basicamente duas coisas:

  1. Na parte do código onde se detecta um erro, deve-se lançar uma exceção por meio da palavra chave throw. O valor da exceção pode ser qualquer, e no exemplo a seguir é representado por um número inteiro:
    string &Lista::operator[](int pos) {
      if (pos >= comp) throw 1; // posição inválida !!!
    
      Nodo * p;
    
      for (p=inicio; pos > 0; pos--, p=p->proximo);
      return p->dado;
    }
    
  2. Na parte do código que pode receber uma exceção, deve-se capturá-la e tratá-la. Para isso usa-se a construção try ... catch, que possibilita executar um tratador de exceção:
      try {
        string palavra = list[5];
        cout << "Palavra=" << palavra << endl;
      } catch (int excecao) { // o corpo do "catch" contém o tratador da exceção ...
        cout << "Ops: acho que posição é inválida (lançada exceção" << excecao << "!)" << endl;
      }
    

OBS: se uma exceção não for capturada usando try ... catch, ela causará o término do programa.

Conversão de string para tipos numéricos

Em C++ há diferentes formas de converter strings para dados numéricos, e vice-versa. Podem-se, por exemplo, usar as funções da biblioteca C padrão (sscanf, snprintf, strtod, ...). Mas uma forma particular existente em C++ explora o conceito de streams.

Arquivos abertos são acessados como streams em C++. Na verdade, essa é uma interface corriqueira para acesso a arquivos, estando disponível em diferentes linguagens (ex: as operações de arquivo em C, como fscanf, fprintf, fread, fwrite, fgets, ...). Em C++ streams possibilitam ler texto de um arquivo e convertê-lo para um dado numérico, e vice-versa. Isso é feito normalmente em programas, como no exemplo abaixo:

#include <iostream>

using namespace std;

int main() {
  ifstream arq("numeros.txt");
  int numero;
  string palavra;

  arq >> numero;
  arq >> palavra;

  cout << "numero=" << numero << endl;
  cout << "palavra=" << palavra << endl;

  arq.close();

  return 0;
}


Se o arquivo numeros.txt possuir este conteúdo:

2015 Furious
1999 Matrix


... a execução do programa exemplo, que lê apenas a primeira linha, resultará em:

numero=2015
palavra=Furious


Assim, o operador >> de streams possibilita converter texto para um outro tipo de dado, dependendo do tipo da variável onde o resultado deva ser gravado. No exemplo, esse operador é usado duas vezes:

arq >> numero; // converte texto para inteiro, pois "numero" 
               // é uma variável do tipo int
arq >> palavra;// converte texto para string, pois palavra é do tipo string


Parece que C++ possibilita converter facilmente texto (string) para dado numérico SE o texto estiver guardado em um arquivo. Isso não é conveniente para converter um valor que esteja guardado em uma variável string, pois tornaria necessário escrevê-la em um arquivo temporário para depois ler desse arquivo e efetuar a conversão. De fato, se fosse assim dificilmente streams seriam usadas para converter dados em C++. No entanto existe um tipo de stream que possibilita que se acesse uma string como se fosse um arquivo. Quer dizer, a leitura e a escrita dos caracteres de uma string podem ser feitas com operações de arquivo. O exemplo abaixo mostra o uso dessa stream de string (ou stringstream):

#include <iostream>
// as classes stringstream estão em sstream
#include <sstream>

using namespace std;

int main() {
  istringstream stream("2015 Furious");
  int numero;
  string palavra;

  stream >> numero;
  stream >> palavra;

  cout << "numero=" << numero << endl;
  cout << "palavra=" << palavra << endl;

  return 0;
}


O exemplo mostra uma stringstream criada com conteúdo 2015 Furious. Em seguida, usa-se o operador >> para ler um número inteiro e uma string. O resultado desse exemplo é idêntico ao do exemplo anterior, em que se liam esses dados de um arquivo.

A possibilidade de usar stringstream para converter dados torna simples criar funções de conversão, como esta que converte string para inteiro:

int converte_para_int(const string & dado) {
  istringstream stream(dado);
  int r;

  stream >> r;
  if (stream.fail()) throw 1; // erro de conversão

  return r;
}


Outras funções para tipos numéricos podem ser criadas da mesma forma:

float converte_para_float(const string & dado) {
  istringstream stream(dado);
  float r;

  stream >> r;
  if (stream.fail()) throw 1; // erro de conversão

  return r;
}


double converte_para_double(const string & dado) {
  istringstream stream(dado);
  double r;

  stream >> r;
  if (stream.fail()) throw 1; // erro de conversão

  return r;
}


Note que são todas muito parecidas. A única diferença entre elas é o tipo do dado numérico da conversão. Assim, é natural substitui-las por uma função template:

template <class T> T converte(const string & dado) {
  istringstream stream(dado);
  T r;

  stream >> r;
  if (stream.fail()) throw 1; // erro de conversão

  return r;
}


Essa função template seria usada da seguinte forma:

#include <iostream>
#include <sstream>

using namespace std;

template <class T> T converte(const string & dado) {
  istringstream stream(dado);
  T r;

  stream >> r;
  if (stream.fail()) throw 1; // erro de conversão

  return r;
}

int main() {
  string algo1 = "2015";
  string algo2 = "3.1416";
  string algo3 = "xyz";
  int ano;
  float pi;

  ano = converte<int>(algo1);
  pi = converte<float>(algo2);

  cout << "ano=" << ano << endl;
  cout << "pi=" << pi << endl;

  try {
    ano = converte<int>(algo3);
    cout << "Outro ano=" << ano << endl;
  } catch (int e) {
    cout << "Erro de conversão: " << algo3 << " não é um int" << endl;
  }
  return 0;
}