Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação
Algoritmos e Estruturas de
Dados I
“Containers” e iteradores
Considere o problema de programação onde (i) precisamos guardar
os nomes de alunos e sua notas, (ii) precisamos guardar o nome de pessoas e seu
cargo na empresa, o numero de um empregado e seus dados pessoais. Estes são
problemas recorrentes em Programação e podem ser resolvidos utilizando
elementos denominados “containers”. Resolvido o problema de como guardar ou
armazenar uma informação temos um outro problema que é como percorrer os
elementos que estão armazenados em um “container”. O elemento que exibe
comportamentos sobre como percorrer os elementos de um “container” é denominado
“iterador”. O ambiente da linguagem C++ provê ao programador vários tipo de
“containers” e também vários tipos de iteradores.
O trecho de programa abaixo mostra um exemplo de
“container” denominado “vector”. Estamos usando este container para guardar 3
valores inteiros:
#include <vector>
...
vector<int> c;
c.push_back(10);
c.push_back(20);
c.push_back(30);
Observe a especialização do container vector para guardar
valores do tipo int. O container vector sobrecarrega o operador indexação
(define a função operator[]) e permite um tipo de “iteração” especial via
indexação:
for(int i=0; i<c.size(); i++) cout<<c[i]<<endl;
Mas podemos utilizar um iterador “de verdade” através da
construção:
for(vector<int>::iterator it=c.begin(); it!=c.end(); ++it)
cout<<*it<<endl;
Apesar de parecer “complicado” a vantagem do “iterador” é
que ele é geral e pode ser utilizado de forma ortogonal a vários tipos de
containers. A interpretação do trecho acima é a seguinte: it é uma variável do
tipo iterador de container de ints(vector). O valor inicial de it é dado por
uma função de instância de vector que retorna o valor do “primeiro” elemento
armazenado. A iteração é feita enquanto o valor de it não é igual ao valor
correspondente ao “ultimo” elemento de vector (atente para este último). Os
projetistas da biblioteca STL da linguagem C++ definiram que os iteradores
deveriam funcionar como ponteiros! observe a expressão ++it e *it, fazem
lembrar ponteiros (o conceito neste caso é sobrecarga de opearadores!). Em
nosso curso não vamos ver todos os tipo de containers nem todos os tipo de
iteradores. O material abaixo mostra de maneira bem grosseira e tosca como é
implementado um container denominado list. Nosso objetivo principal é a
discussão da estrutura de lista que pode ser criada dinamicamente. Observe o
seu uso no trecho de programa abaixo:
#include <list>
...
list<int> lista;
lista.push_back(10);
lista.push_back(25);
lista.push_back(30);
for(list<int>::iterator
it=lista.begin(); it!=lista.end(); ++it) cout<<*it<<endl;
Este estilo de “iterador” está presente em várias das
classes definidas para C++, observe seu uso com a classe string:
string s("abcde");
for(string::iterator
it=s.begin(); it!=s.end(); ++it) cout<<*it<<endl;
O container list implementa uma estrutura de <“nodos” duplamente
encadeados> e o significado disso é explicado no material a seguir. Deverá
então ficar claro a razão de não ser possível indexar os componentes de uma
“lista duplamente encadeada” (de forma similar à indexação dos componentes de
um “arranjo/vetor”).
Encadeamento
O programa abaixo ilustra um série de técnicas relacionadas
ao encadeamento de elementos que fazem o envelope de informação a ser
colecionada. Para simplificar a apresentação colecionamos valores inteiros que
devem ser armazenados no campo info da classe Nodo (não vamos usar “programação
genérica”). A classe Nodo define os campos ant e prox que devem ser usados para
apontar para instâncias, respectivamente, anteriores e posteriores à instância
considerada. A classe Lista utiliza um nodo postiço para poder servir como
ponto de origem da coleção. Uma lista vazia tem uma instância de Nodo, postico,
com campos ant e prox apontando para este mesmo elemento.
#include <cstdlib>
#include <iostream>
using namespace std;
class ErroDeRemocao{};
class Nodo{private: int info;
public:
Nodo(){info=0;}
Nodo(int i){info=i;}
Nodo *ant; Nodo *prox;
void ajustaInfo(int
i){info=i;}
int obtemInfo(){return info;}
};
class Lista{private:
Nodo postico;
public:
Lista(){
postico.prox=&postico;
postico.ant=&postico;
}
void adicione(int i){
Nodo *p=new Nodo(i);
p->prox=postico.prox;
p->ant=&postico;
postico.prox->ant=p;
postico.prox=p;
}
void
removeUltimoInserido(){
if(postico.prox==&postico){
throw new
ErroDeRemocao();
}
Nodo *p=postico.prox;
postico.prox=p->prox;
p->prox->ant=&postico;
delete p;
}
void escreve(){
Nodo *n;
for(n=postico.prox;
n!=&postico; n=n->prox){
printf("%d\n", n->obtemInfo());
}
}
};
int main(int argc, char *argv[]){
Lista l1;
l1.adicione(10);
l1.adicione(20);
l1.adicione(30);
l1.escreve();
printf("remove ultimo
inserido\n");
l1.removeUltimoInserido();
l1.escreve();
system("PAUSE");
return EXIT_SUCCESS;
}
Na definição da estrutura
da classe Lista é utilizada a variável postico (postiço) do tipo Nodo. Podemos
sintetizar isto da seguinte forma:
class A{...};
class B{public: A a;…};
…
B b;
Na forma sintética acima A corresponde a Nodo, a corresponde a postico, B corresponde a Lista e b corresponde a l1.
Após a passagem do fluxo de
controle pela declaração
Lista l1;
Temos a seguinte estrutura
em memória:

As linhas em azul representam a relação “é instância de” entre objetos e
classes. As linhas pretas com um circulo cheio na origem e uma seta no final
representam endereços.
Após a passagem do fluxo de
controle pela sentença
l1.adicione(10);
A estrutura é alterada
para:

Observe que esta nova
instância da classe Nodo é criada de forma dinâmica usando o operador new. Esta
nova instância não tem nome (ao ser criada seu endereço é guardado no ponteiro
p). Ao final da execução de l1.adicione(10) o ponteiro p sai de escopo e esta
intância de Nodo só pode ser acessada via navegação a partir dos ponteiros da
instância de Nodo de nome “postico”.
Exercício
Faça o desenho da estrutura
de dados criada a partir da variável l1 depois que forem inseridas as intâncias
da classe Nodo que “empacotam” os valores 10, 20 e 30.
Transforme o programa acima
para ser “parcialmente” genérico, ou seja reescreva a classe Nodo de forma a
poder colecionar qualquer tipo. Não precisa ser “muito” genérico.
Escolha alguns operadores
da linguagem C/C++ para serem sobrecarregados de forma “intuitiva”.