Agnor's HQ

Aula 20 - Técnicas Avançadas de C++ - Parte IV

Conceitos-Chave:

- iterador, uma variável que permite percorrer os elementos de um contentor, de forma abstracta e genérica
- list, uma estrutura de dados da STL, que possui uma mecânica idêntica a uma lista duplamente ligada
- pré-processador, um componente do C++ que processa o texto recebido e inclui os seus resultados no próprio ficheiro

Iteradores

Antes de avançar para a classe list da STL é importante saber trabalhar com iteradores. Até agora temos usado o operador de indexação ('[]') para obter os valores das variáveis dentro dos vectores, como fazíamos com os arrays. A STL desencoraja esta forma de aceder aos elementos dos seus contentores (um contentor é um objecto que tem a função de alojar outros objectos, como a classe vector e list da STL). Encoraja sim a utilização dos iteradores, que são muito semelhantes aos apontadores. Na aula 14 analisamos as semelhanças entre um apontador e um array:

#include <iostream>
using namespace std;

int main()
{
    int *array_pointer = new int[5];    //cria um novo array na memória dinâmica
    
    int *array_begin = array_pointer;
    /* Criamos um apontador e depois atribuimos-lhe o endereço do array que criamos anteriormente.
       Não se esqueçam que o array_pointer aponta para o primeiro elemento das 5 variáveis que criamos na memória dinâmica.
       Assim, a variável array_begin está a apontar para o primeiro elemento deste array, ou seja, sinaliza o começo do mesmo.
    */
    
    int *array_end = array_pointer+5; //usando a mesma lógica, o array_end aponta para o 5º elemento do array (último)
    
    int *array_iterator; //este é o nosso iterador, o apontador que irá percorrer o array
    
    /* Esta parte está explicada abaixo (fora do código) */    
    
    for (array_iterator = array_begin; array_iterator != array_end; array_iterator++)
    {
        *array_iterator = 10; //só serve para encher o array com o valor 10
    }
        
    for (array_iterator = array_begin; array_iterator != array_end; array_iterator++)    
    {
        cout << *array_iterator << endl;   //mostra os valores
    }
    
    cin.get();
    
    delete [] array_pointer;       //elimina o array da memória dinâmica
    
    return 0;
}

Exemplo 20.1 - Exemplo primitivo de um iterador

Ufa, admito que este código não seja muito fácil de compreender (para além de haver outras soluções muito mais fáceis de implementar), mas é necessário para poderem perceber como funcionam os iteradores dentro da STL. Toda a mecânica está à volta das operações com o endereço com apontadores, por isso revejam a aula 14. Começarei para explicar o que precisamos para utilizar um iterador:

No exemplo acima usámos esses componentes e implementámo-los através de um for.

A primeira instrução dada ao for (array_iterator = array_begin) corresponde à inicialização, ou seja, estamos a "dizer" ao iterador em que posição na memória deverá começar para percorrer o contentor.

Na segunda instrução, o teste à condição (array_iterator != array_end), pedimos ao computador para, enquanto o array_iterator for igual ao array_end (ou seja, enquanto o iterador não chegar ao final) processar o bloco de instruções (*array_iterator = 10) e para executar a terceira instrução, a instrução de incremento (array_iterator++), que faz com que o iterador passe a apontar para o elemento seguinte.

Foi o melhor que consegui explicar esta mecânica, mas qualquer dúvida e contactem-me

Iterador num vector

Para aplicar os iteradores à "vida real" vou mostrar o mesmo exemplo, mas usando vectores.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector <int> meu_array;
    vector <int>::iterator meu_iterador; //ok, esta notação é estranha, mas faz sentido ;)
    
    meu_array.push_back(10); //vamos aproveitar as facilidades do vector!
    meu_array.push_back(20);
    meu_array.push_back(30);
    meu_array.push_back(20);
    meu_array.push_back(10);
    
    for (meu_iterador = meu_array.begin(); meu_iterador != meu_array.end(); meu_iterador++)
    {
        cout << *meu_iterador << endl;
    }
    
    cin.get();
    
    return 0;
}


Exemplo 20.2 - Exemplo de um iterador na STL (classe vector)

Porque é que os iteradores são importantes?

Numa classe como a vector, os iteradores não são muito importantes, porque podemos usar o operador '[]'. No entanto, em classes como a classe list (que iremos analisar de seguida), não poderemos aceder aos seus elementos de forma aleatória (com o []), porque se trata de uma lista ligada (ver a aula anterior). A STL apresenta outro tipo de contentores que não consegue aceder aos seus elementos aleatoriamente e, por isso, colocaram a hipótese dos iteradores em todos os seus contentores. Os iteradores são a forma "oficial" da STL para percorrermos os contentores, porque a STL foi feita para ser genérica e abstracta, ou seja, o programador não tem que se preocupar em usar diferentes formas de aceder a cada contentor da STL, porque os iteradores abrangem todas essas formas. Também muitos métodos dentro da STL tiram partido dos iteradores, por exemplo, dentro da classe vector poderemos utilizar o seguinte código para eliminar os 2 primeiros elementos de um array:

meu_array.erase(meu_array.begin(), meu_array.begin()+3);

Outra coisa que é importante assinalar é que os iteradores diferem ligeiramente de contentor para contentor. Por exemplo, enquanto com o vector é possível fazer (teste.begin()+2), com a list isso não é possível (é apenas possível incrementar(++) e decrementar(--)), portanto não podemos reutilizar um iterador de um vector com uma lista.

Classe list

Se já trabalharam com a classe vector e entenderam pelo menos o básico dos iteradores, então vai ser muito fácil trabalhar com as listas. Uma das vantagens da STL é que trabalhar os contentores, em termos de sintaxe e organização, são muito semelhantes, diferendo apenas naquilo em que é mesmo necessário. Desta forma, a escolha de um contentor não se prende com a facilidade de utilização do mesmo, mas sim com a adequação do mesmo ao nosso trabalho. É por isso que a STL é tão boa :D.

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list <int> minha_lista;
    list <int>::iterator meu_iterador;
    
    minha_lista.push_back(20);        //inserimos um elemento no final da lista (isto é igual ao vector)
    minha_lista.push_back(30);
    minha_lista.push_front(0);        //inserimos um elemento no início da lista sem nenhum esforço para o programador e para o computador
    minha_lista.insert(++minha_lista.begin(), 10); //inserimos um elemento na posição 1 da lista (entre o 0 e o 20)
    
    //isto já seria impossível num vector, sem gastar muitos recursos há máquina   
    
    for (meu_iterador = minha_lista.begin(); meu_iterador != minha_lista.end(); meu_iterador++)
    {
        cout << *meu_iterador << endl;
    }
    
    cin.get();
    
    return 0;
}

Exemplo 20.3 - Exemplo simples com a classe list da STL

Penso que o código é bastante simples de perceber e evidencia logo as diferenças entre o vector e a list.

Nota:

De certeza que já me viram utilizar o operador de incrementação (++) antes e depois de cada variável, por exemplo i++ ou ++i. Há uma pequena diferença entre as duas e que por acaso até é importante neste exemplo (na parte da ++minha_lista.begin()).

Fazer i++ faz com que seja executado em primeiro lugar a instrução em que o i++ está inserido e só depois é que incrementa a variável, enquanto fazer ++i faz com que primeiro incremente a variável e só depois execute a instrução em que está inserido o ++i. Com um exemplo é fácil de perceber:

int i = 3;
cout << i++;
 // o valor apresentado vai ser '3'
cout << i;      // o valor apresentado vai ser '4'
cout << ++i;  // o valor apresentado vai ser '5'

Isto vem a propósito da instrução ++minha_lista.begin(), que eu acidentalmente escrevi minha_lista.begin()++ e que, por isso, não fez nenhum efeito (experimentem utilizá-la deste modo)

Aceder aos elementos de uma lista

Como já expliquei, não é possível fazer minha_lista.begin()+2 para aceder ao 3º elemento de uma lista (não se esqueçam que minha_lista.begin() é o primeiro elemento e por isso, ao adicionarmos 2, ele passa para o 3º e não para o 2º!). No entanto é possível fazer incrementos a um iterador até chegar a esse valor. Claro que isto demora mais tempo do que o acesso aleatório dos vectores, mas já expliquei as vantagens e desvantagens das listas na aula anterior. O contentor list apresenta a mecânica de uma lista duplamente ligada, por isso permite que percorramos os seus elementos para a frente (iterador_lista++) ou para trás (iterador_lista--). Fiquem com este exemplo bastante simples:

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list <int> minha_lista;
    list <int>::iterator meu_iterador;
    int e; //usado para a escolha do utilizador
    
    minha_lista.push_back(10); 
    minha_lista.push_back(20); 
    minha_lista.push_back(30);        
    minha_lista.push_back(40);
    minha_lista.push_back(50); 
    minha_lista.push_back(60); 

    meu_iterador = minha_lista.begin();
       
    cout << "Deseja aceder a que elemento da lista? [0-5]: [ ]\b\b"; 
    //o \b é o carácter especial de retrocesso, ou seja, posiciona o cursor 2 caracteres antes de onde é suposto
    //dá um efeito mais agradável :)
    cin >> e;
    
    for (int x = 0; x < e; x++)
    {
        meu_iterador++;
    }
    
    cout << "Valor: " << *meu_iterador << endl;
    
    cin.get();
    cin.get();
    
    return 0;
}

Exemplo 20.4 - Aceder "aleatoriamente" aos valores de uma lista

O pré-processador em C++

O pré-processador em C++ faz com que um determinado texto seja processado e inserido no documento. É algo bastante simples de utilizar e que era usado frequentemente em C, mas agora já se encontra algo desactualizado. No entanto, bastantes programadores ainda o usam e é importante conhecê-lo. Todos os comandos do pré-processador incluem antes um '#'.

#include

Este é o comando mais usado em C++ e faz coisas incríveis. Basicamente copia todo o código de um determinado ficheiro para o ficheiro em que esta instrução está presente, o que faz com que o código fique muito mais lindo e organizado. Pode ser utilizado de duas formas:

#include <ficheiro.h>

para ficheiros do sistema, como iostream, etc.

#include "ficheiro.h"

para ficheiros criados por vocês.

#define

O #define substitui "palavras por números", actuando como uma constante. Por exemplo:

#define PI 3.14159

Substitui todas as "palavras" PI dentro do código (menos as que estão dentro das strings) pelo valor 3.14159. Ainda é bastante utilizado, mas desaconselhado, porque em C++ existe o tipo const, que funciona muito melhor.

Também é possível utilizar os defines como uma função (chama-se a isso uma macro), por exemplo:

#define soma(a,b) a+b

Novamente, declarar as funções como inline tem o mesmo efeito, e é muito melhor ( inline int soma (int a, int b) { return a+b; } )

Dica:

Declarar uma classe como inline faz com que todas as chamadas a essa função sejam substituídas pelo código da mesma. Por exemplo, com a função soma acima, o código soma(3,7) seria substituído por 3+7. Com isto há um ganho na velocidade de processamento, mas uma perda no tamanho do programa. Funções pequenas (como a função soma) são aconselhadas para serem declaradas como inline, mas funções mais complexas não!

#ifdef, #ifndef, #endif

Estas são úteis em headers e em casos específicos (como para debug e assim). O #include não é perfeito, uma das suas limitações é não verificar se um determinado ficheiro já foi incluido no programa, de modo a evitar que este seja incluído mais que uma vez. O #ifndef permite-nos artificialmente verificar isso:

#ifndef FICHEIRO_JA_FOI_INCLUIDO //se ainda não foi definido isto (ou seja, se o ficheiro ainda não foi incluído)
#define FICHEIRO_JA_FOI_INCLUIDO
// código aqui
#endif

Outra coisa interessante é por exemplo uma verificação do sistema operativo:

#ifdef WINDOWS
#include "headersparawindows
#endif
#ifdef LINUX

#include "headersparalinux
#endif

Isto poderia ser utilizado para outros casos, como um modo de debug, onde nos bastaría escrever #define DEBUG_MODE e o programa recompilava para nos oferecer instruções detalhadas de como cada elemento do jogo se está a comportar.

E agora?

As aulas teóricas de C++ deste site já terminaram! Para os acompanhantes do website, podem ficar descansados, ainda estão previstas mais 5 aulas práticas, que irão mostrar como construir (bem) um jogo. O jogo continuará a ser em modo de texto, mas penso que irão ficar contentes com o resultado!

No entanto, ainda é preciso muito para ser um "mestre" em C++ (o que nem eu, de longe, sou). Há bastante informação na Internet e em livros de boas práticas em C++, assim como de mais algoritmos, estruturas de dados e de muito C++ avançado. No entanto, penso que as bases já estão dadas, e o resto virá como "acréscimos" à vossa capacidade. Deixo-vos alguns websites e livros para material de "self-study" (em Inglês a sua maioria, infelizmente):

Livros

Websites

E por último, mas não menos importante, websites com bastantes recursos (incluíndo fórums) que vos vão ajudar bastante (ajudaram-me muito e tenho uma dívida especial com eles):

Final da aula 20 de C++:

Próxima aula -> C++ P1 - Fazendo um jogo do princípio ao fim - Parte 1

Fazer o download do código fonte dos exemplos da aula WinZip
Fazer o download da aula em PDF (Brevemente)

Ir para o topo da página

Aulas de C++

Anúncios