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;
}
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:
- um apontador para o início do "contentor" (neste caso o array)
- um apontador para o final do "contentor"
- um apontador que percorra o "contentor" do início ao fim (o 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;
}
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;
}
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;
}
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
- Programação em C++ - Conceitos Básicos e Algoritmos, vários autores - Livro em Português que serve de introdução a C++ (FCA, FNAC, Bertrand)
- Effective C++, Scott Meyers - 55 dicas para melhorar a vossa forma de programar em C++ (Amazon.co.uk
, Amazon.com
)
- More Effective C++, Scott Meyers - A continuação do anterior, com mais 35 dicas (Amazon.co.uk
, Amazon.com
)
- Effective STL, Scott Meyers - 50 dicas para melhorar a vossa forma de programar com a STL (Amazon.co.uk
, Amazon.com
)
- C++ for Game Programmers, Mike Dickheiser - Livro que mostra ao programador com experiência em C++ como utilizá-la para programar jogos (Amazon.co.uk
, Amazon.com
)
- Beginning C++ Game Programming, Michael Dawson - Ensina C++ ao iniciante, recorrendo a exemplos de jogos (Amazon.co.uk
, Amazon.com
)
- Design Patterns: Elements of Reusable Object-Oriented Software, vários autores - Este livro ensina-nos como implementar as melhores formas de abordar certos problemas, através da POO (Amazon.co.uk
, Amazon.com
)
Websites
- Tutorial de C++ do Pedro Santos
- C++ Language Tutorial
- CProgramming.com
- SGI - STL Programmer's Guide
- C++ Reference
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):
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
Fazer o download da aula em PDF (Brevemente)
Ir para o topo da página