Introdução C++

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar

Bibliografia


Introdução

A linguagem de programação 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.


Declaração de classes

Uma classe é declarada usando-se a palavra-chave class seguida do nome da classe:

class MinhaClasse {
 // declarações internas da classe
};

De certa forma, isso se assemelha à declaração de struct. No entanto, as declarações internas da classe têm um significado um pouco diferente. Por exemplo, suponha-se que a classe MinhaClasse represente um número de telefone, incluindo seu código de área. A declaração seria estendida desta forma:


class MinhaClasse {
 private:
  string numero;
  string ddd;
 public:
 // declarações dos procedimentos da classe
};

A classe agora possui dois atributos:

  • string numero: armazena o número de telefone
  • string ddd: armazena o código DDD

Note-se que as declarações desses atributos na classe são precedidas pela cláusula private:. Isso significa que esses atributos não podem ser acessados diretamente de fora da classe (por procedimentos implementados fora da classe). Logo após a declaração desses atributos, aparece a cláusula public:. Ela define que o que for declarado em seguida pode ser acessado diretamente de fora da classe. Por exemplo, os métodos da classe podem ser declarados logo após public:.


class MinhaClasse {
 private:
  string numero;
  string ddd;
 public:
  // Construtor: executado quando se cria um objeto desta classe
  MinhaClasse(string umNumero, string umDDD);

  // Destrutor: executado quando se destrói um objeto desta classe
  ~MinhaClasse();

  // métodos para acessar os valores dos atributos
  string obtemNumero();
  string obtemDDD();
  string obtemNumeroDDD();
};

Um método é uma função que pertence a uma classe. A idéia é que um objeto de uma classe possui um estado dado pelos valores de seus atributos, e somente os métodos daquela classe podem acessar ou modificar esse estado. Quer dizer, somente métodos da classe podem acessar diretamente esses atributos. Dois métodos são especiais:

  • construtor: este método é executado sempre que se cria um objeto de uma classe. Ele tem por finalidade iniciar o estado daquele objeto. Isso envolve definir os valores iniciais dos atributos, e conferir se os parâmetros que porventura tenham sido passados são válidos (ex: se o número tem somente caracteres numéricos).
  • destrutor: este método é executado quando um objeto é destruído. Ele é responsável por limpar o estado desse objeto, desfazendo qualquer ação que ele tenha iniciado. Por exemplo, se o objeto alocou memória dinamicamente em algum momento, o destrutor deve liberá-la.


A declaração da classe apresentada apenas informa quais métodos ela implementa, mas não mostra como eles são implementados. Essa implementação pode ser feita de duas formas:

  • inline: a implementação de um método é feita dentro da própria declaração da classe. Por exemplo, o método obtemNumeroDDD poderia estar implementado assim:
    class MinhaClasse {
     private:
      string numero;
      string ddd;
     public:
      // Construtor: executado quando se cria um objeto desta classe
      MinhaClasse(string umNumero, string umDDD);
    
      // Destrutor: executado quando se destrói um objeto desta classe
      ~MinhaClasse();
    
      // métodos para acessar os valores dos atributos
      string obtemNumero();
      string obtemDDD();
    
      // implementação inline
      string obtemNumeroDDD() {
        string num = ddd + '-' + numero;
        return num;
      }
    };
    
  • fora da classe: a implementação é feita externamente, possivelmente em outro arquivo. No caso de obtemNumeroDDD, isso poderia ser feito assim:
    // deve-se prefixar o nome do método com o nome da classe, para indicar que é um método dessa classe
    string MinhaClasse::obtemNumeroDDD() {
      string num = ddd + '-' + numero;
      return num;
    }
    

Criação de objetos

Uma vez tendo declarado e implementado uma classe, podem-se criar objetos. A isso se chama instanciação, e os objetos também podem ser chamados de instâncias. Objetos podem ser criados diretamente, como se fossem variáveis usuais, como mostrado a seguir:

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

using namespace std;

int main() {
  string num, ddd;

  cout << "Numero: ";
  cin >> num;
  cout << "DDD: ";
  cin >> ddd;

  // o construtor da classe é chamado quando se cria um objeto
  // Aqui, o objeto é dado pela variável "fone"
  MinhaClasse fone(num, ddd);

  cout << "Numero com DDD é " << fone.obtemNumeroDDD() << endl;

  // objetos são destruídos automaticamente ao final do escopo em que foram criados
  // o destrutor é então executado 
}


Objetos podem também ser criados dinamicamente, em que a memória necessária é alocada. Para isso usa-se o operador new (e não a função malloc !). Objetos criados dessa forma são referenciados por ponteiros, e devem ser explicitamente destruídos usando-se o operador delete (e não a função free !). O exemplo a seguir mostra a criação e destruição de objetos com new e delete:

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

using namespace std;

int main() {
  string num, ddd;
  // o ponteiro fone é capaz de apontar objetos de MinhaClasse
  MinhaClasse * fone;

  cout << "Numero: ";
  cin >> num;
  cout << "DDD: ";
  cin >> ddd;

  // o construtor da classe é chamado quando se cria um objeto com o operador "new"
  // Aqui, o objeto é dado pelo ponteiro "fone"
  fone = new MinhaClasse(num, ddd);

  cout << "Numero com DDD é " << fone->obtemNumeroDDD() << endl;

  // objetos dinâmico devem ser destruídos explicitamente usando o operador "delete"
  // o destrutor é então executado 
  delete fone;
}

Destruição de objetos

A destruição de um objeto ocorre em duas situações:

  1. Quando se chega ao fim o escopo dentro do qual o objeto foi criado: isso vale para objetos declarados e instanciados diretamente. Ex:
    void ecoa() {
      string palavra; // aqui se cria um objeto string
    
      cout << "Digite uma palavra: ";
      cin >> palavra;
      cout << "Você digitou: " << palavra << endl;
    } // fim do escopo ... o objeto string "palavra" é destruido automaticamente
    
  2. Quando se destroi o objeto com o operador delete: isso vale somente para objetos criados dinamicamente com o operador new. Ex:
    void ecoa() {
      string * palavra; // aqui se declara um ponteiro para string
    
      palavra = new string; // aqui se cria dinamicamente um objeto string
      cout << "Digite uma palavra: ";
      cin >> *palavra;
      cout << "Você digitou: " << *palavra << endl;
    
      delete palavra; // aqui se destroi o objeto string
    }
    


A destruição de um objeto implica liberar as áreas de memória a ele alocadas dinamicamente em sua criação, ou durante seu tempo de vida. Essa faxina deve ser feita pelo método destrutor da classe (chamado simplesmente de destructor). Tal método é declarado da seguinte forma em uma classe:

classe Demo {
 public:
  Demo(); // constructor da classe
  ~Demo(); // destructor da classe

  // demais métodos da classe
};

// implementação do destructor
Demo::~Demo() { 
  // limpeza do objeto
}

A implementação desse método deve ser responsável pela limpeza do objeto. Apenas áreas de memória alocadas dinamicamente precisam ser liberadas. O exemplo a seguir mostra uma classe com memória alocada dinamicamente:

classe Demo {
 public:
  Demo(const string & name); // constructor da classe
  ~Demo(); // destructor da classe

  // demais métodos da classe
 private:
  // atributos privativos da classe
  string nome;
  char * buffer;
};

// implementação do constructor
Demo::Demo(const string & name) { 
  nome = name;
  buffer = new char[1024]; // aloca dinamicamente 1kB
}

// implementação do destructor
Demo::~Demo() { 
  // libera a área de memória alocada no constructor
  delete buffer;
}

Um exemplo

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.

O ponteiro predefinido this


Ao se implementarem métodos de uma classe pode-se usar uma variável predefinida chamada this, que é um ponteiro para o objeto no escopo do qual se executa um método. Sempre que dentro de um método for necessário eliminar uma ambiguidade quanto ao uso de um atributo do objeto, ou por clareza, pode-se usar esse ponteiro this. O exemplo a seguir ilustra o uso de this.

class Coisa {
 public:
  Coisa(int x, int y);
  ~Coisa() {}
  // outros métodos da classe ...
 private:
  // os atributos dos objetos desta classe
  int x,y;
};

// construtor da classe Coisa: os parâmetros "x" e "y" são usados para iniciar os 
// atributos "x" e "y". Para resolver a ambiguidade entre parâmetros e atributos, dentro do construtor
// se usa o ponteiro "this"
Coisa::Coisa(int x, int y) {
  this->x = x;
  this->y = y;
}


Existem outros casos em que o ponteiro this tem utilidade. Um exemplo pode ser visto na seção sobre construtor de cópia.

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 "buffer"
  // copiar os valores de atributos de "outra"
  // alocar memória para "buffer"
  // copiar conteúdo da "buffer" 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.cpp

Construtor de cópia

Um construtor de cópia é um método construtor para criar um objeto que é uma cópia idêntica a um objeto existente. Esse tipo de criação de objeto aparece em casos como este:

Fila<int> f1(10); // cria uma fila capaz de guardar 10 números inteiros

f1.enfileira(5);
f1.enfileira(8);
f1.enfileira(2);
f1.enfileira(4);

Fila<int> f2 = f1; // cria a fila f2, que é uma cópia idêntica de f1

f1.esvazia(); // esvazia a fila f1

// Mostra o conteúdo da fila f2: deve ser igual ao que existia em f1
while (not f2.vazia()) {
  cout << f2.desenfileira() << endl;
}

Para que o exemplo acima funcione, a classe Fila deve implementar seu construtor de cópia:

template <typename T> class Fila {
 public:
  // construtor de cópia: o objeto criado deve ser idêntico ao objeto "outra"
  Fila(const Fila<T> & outra);

  // demais declarações da Fila ...
};

O construtor de cópia deve ser responsável por copiar o estado do objeto copiado, de forma que o objeto criado seja idêntico a ele. Isso envolve copiar os valores dos atributos desses objetos, porém em certos casos isso não é suficiente. Se o objeto copiado tiver como atributos outros objetos ou variáveis criados dinamicamente, então o novo objeto deve também criar dinamicamente cópias desses objetos ou variáveis.


Deve-se observar que um construtor de cópia é muito parecido com um operador de atribuição. A diferença reside no fato de que uma atribuição ocorre entre dois objetos já existentes. Por isso um objeto que recebe uma atribuição deve primeiro limpar seu estado (se necessário, o que envolve destruir eventuais objetos e variáveis dinâmicos) para depois se tornar uma cópia do outro objeto. Apesar disso, se um operador de atribuição já existir ele pode ser usado para facilmente implementar um construtor de cópia. O exemplo a seguir mostra como isso pode ser feito para a Fila:

// implementação do construtor de cópia
template <typename T> Fila<T>::Fila(const Fila<T> & outra) {
  // primeiro devem-se atribuir valores aos atributos desta Fila 
  // para que ela pareça vazia

  // em seguida, basta atribuir a "outra" Fila a este objeto
  // Note o uso do ponteiro predefinido "this", que sempre aponta o objeto que recebe a operação
  // dentro de um método de sua classe
  *this = outra;
}

Templates


Templates possibilitam 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 de antemão. Esses tipos são denominados tipos genéricos. No nosso caso, isso será usado para generalizar as estruturas de dados a serem criadas, para que elas possam armazenar qualquer tipo de dado. Na explicação a seguir, usa-se a estrutura de dados Fila como exemplo.


A declaração de uma classe template inicia com o prefixo template <typename T>. A cláusula <typename T> serve para especificar um identificador para o tipo genérico a ser usado dentro da classe (no exemplo, escolheu-se o identificador T, mas pode ser qualquer outra coisa). No caso da Fila, deseja-se torná-la capaz de guardar qualquer tipo de dado. Quer dizer, seu funcionamento não depende do que ela estiver armazenando, portanto o tipo do dado armazenado pode ser generalizado. Esse é um bom exemplo de uso de classe template, estando exemplificado a seguir:

template <typename T> class Fila {

// declarações de atributos e métodos da classe

};


Todas as referências ao tipo genérico devem ser escritas usando o identificador especificado (no caso, T). Aproveitando o exemplo da Fila, deve-se observar que o atributo buffer depende do tipo genérico, pois trata-se de um vetor onde devem ser armazenados os dados enfileirados. Os métodos enfileira, desenfileira e frente também dependem do tipo genérico. A declaração da classe template Fila segue abaixo, porém com alguns detalhes omitidos para ficar mais evidente o uso de template.


template <typename T> class Fila {
  public:
    Fila(unsigned int N);
    ~Fila();

    // enfileira um dado do tipo T
    void enfileira(const T & algo);

    // desenfileira um dado da frente da fila, e retorna uma 
    // cópia desse dado (cujo tipo é T)
    T desenfileira();

    // Retorna uma referência ao dado de tipo T que está na frente da fila
    T & frente() const;

    bool vazia() const {return itens == 0; }
    bool cheia() const { return itens == capacidade;}
    unsigned int tamanho() const { return itens;}
  private:
    int capacidade;
    int itens, inicio, fim;
    T* buffer; // área de memória para guardar os dados de tipo T enfileirados
};


Quando se declara uma classe template, tanto a declaração da classe (sua interface) quanto a implementação de seus métodos devem estar no mesmo arquivo com extensão .h. Isso se deve a detalhes específicos da compilação do código-fonte. Assim, a implementação dos métodos pode estar dentro da própria declaração da classe, como no caso dos métodos vazia, cheia e tamanho, ou fora da classe. Quando forem implementados fora da classe, deve-se repetir o prefixo template <typename T> na frente do nome de cada método, como mostrado a seguir:

template <typename T> void Fila<T>::enfileira(const T& algo) {
  // implementação do método
}


Uma vez declarada uma classe template, ela pode ser utilizada. Somente no momento em que uma classe é usada que o tipo genérico é especificado. Por exemplo, a Fila acima declarada pode guardar qualquer coisa, o que inclui strings e números inteiros. Para declarar duas filas, de forma que uma armazene inteiros e outra strings, deve-se usar esta sintaxe:

int main() {
  // Uma fila capaz de guardar até 10 números inteiros
  Fila<int> f1(10);

  // uma fila capaz de guardar até 5 strings
  Fila<string> f2(5);

  // enfileira-se o número 23 na fila de inteiros
  f1.enfileira(23);

  // enfileira-se a string "uma coisa" na fila de strings
  string algo = "uma coisa";
  f2.enfileira(algo);

  // outras declarações da função main ...
}


Do ponto de vista da linguagem, 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 Fila se torna uma Fila de fato. O programa de teste abaixo aproveita o exemplo das filas, enfileirando e desenfileirando dados de duas filas.

#include <iostream>
#include "fila.h"
 
using namespace std;
 
int main() {
  // Uma fila de strings
  Fila<string> fila1(10);
 
  // Uma fila de inteiros
  Fila<int> fila2(10);
 
  // Armazena três strings na fila de strings
  for (int x=0; x < 3; x++) {
    string palavra;
 
    cout << "Digite uma palavra: ";
    cin >> palavra;
    fila1.enfileira(palavra);
  }
 
  // Armazena alguns números na fila de inteiros
  for(int x=0; x < 8; x += 1) lista2.enfileira(x);
 
  // Mostra os dados da fila de strings
  while (not fila1.vazia()) {
    cout << "desenfileirou: " << fila1.desenfileira() << endl;
  }
 
  // Mostra os dados da fila de inteiros
  while (not fila2.vazia()) {
    cout << "desenfileirou: " << fila2.desenfileira() << endl;
  }
 
}

Passagem de parâmetros

A passagem de parâmetros em C++ apresenta alguns refinamentos, se comparado à linguagem C:

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;

// passagem por referência: observe o operador & 
// antes do nome do parâmetro
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;
}

Alocação dinâmica de memória: operadores new e delete

string


Na linguagem C++ podem-se trabalhar com duas representações de string:

  • vetor de char (char * ou char []): essa forma é a mesma utilizada na linguagem C.
  • classe string: objetos da classe string implementam strings com diversas facilidades, possibilitando um maior nível de abstração (fica mais fácil).


A classe string foi projetada para que se possa trabalhar com strings de forma simplificada. Por exemplo, o programa abaixo mostra como criar, mostrar na tela e concatenar strings:

Usando classe string Usando vetor de char
#include <string>
#include <iostream>

using namespace std;

int main(){
  // declaração dos objetos string: 
  // inicialmente eles são com strings vazias
  string nome;
  string sobrenome;

  // atribui "Manuel" ao objeto string "nome", 
  // e "Alexievitvh" a "sobrenome"
  nome = "Manuel"
  sobrenome = "Alexievitch";

  // cria um novo objeto string, e o inicia com o 
  // resultado da concatenação de nome e sobrenome
  string nome_completo = nome + ' ' + sobrenome;

  // mostra o objeto string na tela
  cout << "Nome: " << nome_completo << endl;
}
#include <string.h>
#include <iostream>

using namespace std;

int main(){
  char nome[16];
  char sobrenome[32];

  strncpy(nome, "Manuel", 16);
  strncpy(sobrenome, "Alexievitch", 32);

  char nome_completo[64];
  strcpy(nome_completo, nome);
  nome_completo[strlen(nome_completo)] = ' ';
  nome_completo[strlen(nome_completo)] = 0;
  strcat(nome_completo, sobrenome);

  cout << "Nome: " << nome_completo << endl;
}


A classe string oferece diversas operações, tais como:

  • size: Obter o comprimento da string
  • operator[]: Obter um caractere em uma determinada posição. Esse tipo de acesso é sintaticamente idêntico ao acesso a uma posição de um vetor
  • operator+=: anexa uma string
  • push_back: anexa um caractere
  • erase: remove caracteres de uma string
  • replace: substitui parte de uma string
  • c_str: retorna uma representação em vetor de char
  • find: encontra a primeira ocorrência de uma substring ou um caractere
  • rfind: encontra a última ocorrência de uma substring ou um caractere
  • substr: gera uma substring

Lendo ou escrevendo strings em streams

Podem-se ler ou escrever strings em streams usando os operadores << ou >>. Por exemplo, para ler uma string da entrada padrão (ex: teclado), pode-se fazer assim:

string algo;

// lê uma string da entrada padrão e grava-a em "algo"
cin >> algo;


Para escrever uma string em um stream (ex: um arquivo), pode-se faz desta forma:

// abre um arquivo para escrita
ofstream arq("teste.txt");

string algo = "Um teste";

// escreve a string no arquivo
arq << algo;


No caso da leitura de uma string que contenha espaços, ou mesmo de uma linha completa, deve-se usar a função getline:

string linha;

cout << "Escreva uma frase: ";

getline(cin, linha);

cout << "Frase digitada é: " << linha;

A função getline usa o caractere newline (\n) como delimitador. Se for desejado usar outro caractere como delimitador, deve-se fazer o seguinte:

string linha;
char delimitador=',';

cout << "Escreva uma frase que contenha ao menos uma vírgula: ";

getline(cin, linha, delimitador);

cout << "Frase digitada até a vírgula é: " << linha;

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

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

Operações de streams

streams oferecem várias operações para leitura e escrita de dados. Algumas mais comuns são listadas a seguir, junto com exemplos.

Operação Entrada Saída Para que serve Exemplo
>> x leitura formatada
int x;
double y;
string palavra;

// lê um número inteiro
cin >> x;

// lê um número double
cin >> y;

// lê uma string
cin >> palavra;
<< x escrita formatada
int x = 5;
double y=3.1416;
string palavra="alguma coisa";

// escreve um número inteiro seguido de uma vírgula
cout << x << ", ";

// escreve um número double seguido de uma vírgula
cout << y << ", ";

// escreve uma string seguida de nova linha
cout << palavra << endl;

// outra forma de escrever os três valores 
cout << x << ", " << y << ", " << palavra << endl;
getline x lê uma linha (até encontrar '\n')
string linha;

getline(cin, linha);

cout << "Linha: " << linha << endl;
ignore x lê e descarta caracteres
cout << "Tecle ENTER para continuar";
cin.ignore(256, '\n'); // lê e descarta até 256 caracteres, ou até ler '\n'
eof x x Verifica se chegou ao fim da stream
ifstream arq("/etc/hosts");

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

  getline(arq, linha);
  cout << linha << endl;
}
is_open x x Verifica se arquivo foi aberto
ifstream arq("/etc/hosts");

if (not arq.is_open()) {
  cout << "Erro: não abriu o arquivo" << endl;
  return 0;
}

Entrada e saída 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;
}

Sobrecarga de operador

Operadores C++ podem ser redefinidos. Por exemplo, pode-se desejar que objetos de uma classe possam ser escritos em streams diretamente por meio do 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.