Matrizes Dinâmicas

Agosto 25, 2009

Voltando um pouco as bases vamos analisar neste artigo algumas possibilidades para criação de matrizes dinâmicas usando C++. Tem sido bem comum em fóruns e listas de emails o pessoal dar cabeçada com esse problema, neste artigo vou apresentar as soluções mais conhecidas e as características de cada uma.

Alocando cada Linha

A técnica que parece ser a mais popular é alocar um vetor de ponteiros e depois alocar uma linha da matriz para cada ponteiro deste vetor:


int main(int, char **)
{
    int nlinhas = 5;
    int ncol = 5; 

    int **mat = new int*[nlinhas]; 

    for(int i = 0;i < nlinhas; ++i)
        mat[i] = new int[ncol]; 

    //iniciando ela com zero
    for(int i = 0;i < nlinhas; ++i)
        for(int j = 0;j < ncol; ++j)
            mat[i][j] = 0; 

    //liberar memória
    for(int i = 0;i < nlinhas; ++i)
        delete []mat[i]; 

    delete []mat; 

    return 0;
}

O problema dessa técnica é o grande desperdício de espaço e tempo que ela gera. O desempenho desta é o pior, principalmente pelo fato de serem necessárias varias alocações e alocações de memória em C/C++ são operações que costumam ser bem caras.

Mesmo ignorando o tempo de criação da matriz, o uso dela também vai ser comprometido devido a possibilidade de cada linha dela estar numa região de memória diferente e é bem provável que o cache do processador tenha dificuldades em se manter atualizado por causa disso.

Já o desperdício de espaço ocorre devido a necessidade de se realizar varias alocações e é comum cada alocação precisa alocar um pouco mais de espaço do que o requisitado para a estrutura de controle usada internamente para gerenciar a memória.

A única vantagem em utilizar este método é quando precisamos de uma matriz absurdamente grande e alocamos cada linha apenas quando existe necessidade, mas mesmo nesse caso acredito que existam técnicas melhores para se implementar uma matriz esparsa.

Usando Apenas um Vetor

Uma maneira mais simples de se implementar uma matriz é utilizando apenas um vetor e realizar o acesso dos elementos como se fossem uma matriz:


int main(int, char *argv)

int main(int, char **)
{
    int nlinhas = 5;
    int ncol = 3; 

    //alocando a "matriz"
    int *mat = new int[nlinhas * ncol]; 

    //colocando 5 na linha 3, coluna 2
    mat[3 * ncol + 2] = 5; 

    //liberando a memoria
    delete []mat; 

    return 0;
}

Note como o código nesse caso é muito mais simples que o anterior, o único incomodo deste código é a necessidade de armazenar o tamanho da matriz (o número de linhas para ser mais preciso) e ficar passando ele como parâmetro a todo momento na hora de acessar um elemento, mas não é nada complicado criar uma classe para encapsular isso.

Para acessar um elemento usamos a fórmula: elem[linha * ncol + col], onde linha é o numero da linha que se quer acessar, ncol o número de colunas da matriz e col o número da coluna que se deseja acessar.

Como é feita apenas uma alocação é usado o minimo de memória possível, além de deixar o cache feliz na hora de acessar os dados.

Utilizando std::vector

Por fim, podemos utilizar o std::vector e não precisamos assim nos preocupar mais em gerenciar a memória da matriz e deixar o programa mais alinhado com o RAII:


#include <iostream>
#include <vector>

int main(int, char **)
{
    int ncol = 4;
    int nlinha = 3; 

    std::vector mat(ncol * nlinha);    

    //acessando elemento 2, 1 (linha 2, coluna 1)
    mat[2 * nlinha + 1] = -1; 

    std::cout << mat[2 * nlinha + 0] << std::endl;
    std::cout << mat[2 * nlinha + 1] << std::endl; 

    return 0;
}

Esse código é muito semelhante ao anterior, mas as operações de gerenciamento de memória foram encapsuladas com o uso de um std::vector, utilizando-se uma boa implementação de std::vector não deve existir diferença alguma de performance e uma diferença minima de consumo de memória entre este exemplo e o anterior.

Esta é a maneira mais simples que conheço para lidar com matrizes dinâmicas, mas o ideal mesmo seria construir uma template que encapsule estas operações, mas isto fica para um outro post.


Problemas com Memória

Julho 21, 2009

Neste artigo vamos discutir dois problemas comuns e causam um bocado de dores de cabeça e as vezes fazem a gente perder preciosas horas de sono, vamos então conhecer os memory leaks e dangling pointers.

Memory Leaks

Memory leak ou vazamento de memória é um problema um tanto comum em C++, principalmente em casos onde o programador tende alocar memória sem necessidade (o que tem se tornado bem comum com programadores que vem de linguagens onde existe um garbage collector) ou não gerencia esta corretamente esquecendo os princípios básicos do RAII.

O memory leak ocorre quando um bloco de memória alocado fica sem referências, isto pode ocorrer em programas bem simples, vejamos um exemplo:


int main(int , char **)
{
    int *p = new int;
    *p = 3; 

    p = new int;
    delete p; 

    return 0;
}

No exemplo acima, primeiramente é alocado espaço para um int e associado com o ponteiro p, logo após a atribuição do valor “3” é feita uma nova alocação e é neste ponto que o vazamento ocorre.

Note que a memória alocada anteriormente não foi liberada e agora o ponteiro p aponta para outro bloco de memória, a única variável que continha o endereço do primeiro bloco alocado não possui mais, sendo assim, o programa não sabe mais o endereço do bloco e não tem mais como liberar essa memória.

No final da execução desse programa o sistema operacional vai se encarregar de liberar toda a memória do programa, sendo assim este pequeno int que foi perdido não é problema, mas imagine o caso de um programa que tenha que funcionar em um regime 24×7, e a cada segundo ele perde uma centena de ints como aquele, nesse ritmo o programa perde 400 bytes por segundo (não contando aqui o overhead da alocação, que inclui as estruturas de controle do runtime do C++ e do SO).

Em uma hora este programa já vai ter perdido pouco mais de 1 mega de memória, em 24 horas já vão embora 32 mega de memória. Não parece grande coisa, ainda mais hoje em dia que até computador das Casas Bahia vem com 2 gigas de memória, mas lembre-se que esta memória poderia estar sendo útil para outros processos, e no caso de um sistema embarcado este problema é bem grave, um playstation 2, por exemplo, possui apenas 32 mega de ram, então em uma hora já acabamos com boa parte da memória dele.

Dangling Pointers

Os ponteiros “quebrados” costumam ser bem desastrosos e ocorrem quando um ponteiro aponta para uma região de memória “invalida”. Na verdade o ponteiro apontar para algo invalido não é problema, o problema ocorre quando é feita uma tentativa de acesso nesse endereço invalido, vamos a um exemplo básico:


#include <string> 

void func()
{
    std::string *str = new std::string("abc");
    delete str; 

    str->assign("bla bla");
} 

std::string *func2()
{
    std::string str; 

    return &str;
} 

int main(int , char **)
{
    func(); 

    std::string *str = func2();
    str->assign("ops");

    return 0;

}

A primeira função (a func) primeiramente aloca uma std::string, libera ela da memória e depois disso tenta acessa-la. Este é o caso mais comum de um dangling pointer, onde um ponteiro armazena um endereço para um bloco de memória que já foi liberado. Nesse exemplo é muito provável que nada demais aconteça pois o uso é feito logo após a desalocação, mas não podemos esquecer que após um delete estamos dizendo ao sistema que não vamos mais usar aquela memória e ele pode fazer o que bem entender com ela, isso significa que esta memória pode inclusive ser devolvida ao sistema operacional, que pode passar ela a outro processo, sendo que tudo isso pode acontecer antes mesmo da chamada de assign, e quando assign for executado ele vai tentar acessar a memória de outro processo e teremos um belo “ding, este processo executou uma …”.

Em programas mais complexos pode ser que ocorram novas alocações de memória antes que o dangling pointer seja usado, nesse caso aquele bloco que foi liberado já pode estar sendo usado por outro objeto e na chamada de assign estaremos acessando uma memória que pode conter qualquer valor, onde o resultado da operação é como diz a especificação “indefinido”.

A função func2 mostra um outro erro bem comum onde retornamos o endereço de uma variável local, nesse caso após a função retornar o destrutor da string vai ser chamado e sem falar que o endereço que ela “reside” já vai estar disponível para outros usos, novamente é bem provável que o sistema operacional se zangue com seu processo e mate ele.

Um detalhe sobre a segunda função é que muitos compiladores geram um warning quando o programador faz algo desse tipo, sendo assim este problema é fácil de ser evitado.

Evitando Esses Problemas

O jeito mais simples de evitar estes problemas é utilizar alguma técnica de smart pointer, com isso já é possível evitar os memory leaks (tem que apenas tomar cuidado com as referencias circulares).

Utilizando smart pointers é certo que um dangling pointer nunca vai ocorrer pois o smart pointer vai cuidar para que os ponteiros sejam sempre válidos (claro que pode ocorrer uma invasão de memória ou outro evento grotesco).

Infelizmente os memory leaks ainda podem ocorrer devido as referencias circulares e para tentarmos detectar estes precisamos usar outras técnicas, mas isto fica para um próximo post.


Como Utilizar o Visual Studio C++ – Parte 3

Abril 22, 2009

No artigo anterior vimos de maneira mais detalhada como realizar um build no Visual e diversas configurações, agora vamos expandir um pouco mais nosso ambiente instalando a Windows SDK, que consiste num kit de desenvolvimento para aplicações Windows.

Pode ser que seu projeto não precise utilizar a API do Windows diretamente, mas outras libs podem precisar (Boost por exemplo).

Baixando a SDK

Para baixar a SDK basta acessar o link de download: Windows SDK for Windows Server 2008 and .NET Framework 3.5, para baixar uma imagem de DVD ISO com toda a SDK, use esse link.

No meu caso optei por instalar a versão web, pois como não utilizo todos os componentes não preciso fazer todo o download.

Após o donwload do programa setup, basta rodar ele. Surge então a primeira tela do setup, clicando em next ele pergunta sobre os diretórios de destino (no meu caso, uso o valor padrão).

Em seguida o instalador pergunta sobre os componentes a instalar, no meu caso dispenso qualquer coisa relacionada a desenvolvimento 64 bits (minhas maquinas são todas 32 bits), dispenso também os samples (exemplos) e documentação (que pode ser vista online), minha configuração ficou:

visual_p3_setup_sdk_02

As seleções aqui são um tanto pessoais e dependem do que você pretende fazer, mas o principal a instalar são os componentes do Windows Headers and Libraries, incluindo no minimo os “Header Files” e alguma das “Libraries”. Lembre-se que se for incluído apenas as bibliotecas para CPU 64 bits, será possível apenas compilar código para essa arquitetura.

Clicando em “next”, o instalador vai iniciar o download e a instalação logo após a conclusão deste.

A instalação é bem básica, e apos a conclusão basta clicar em “Finish”.

Testando a Instalação

Se tudo correu bem, agora basta pegar o programa “Hello World” do primeiro artigo da série e modifica-lo:

#include <windows.h>

int main(int argc, char **argv)
{
    MessageBox(NULL, L"Ola Windows!", L"Teste", MB_OK);
    return 0;
}

Agora é compilar e executar o programa, durante a execução deve ser surgir uma dialogo com a mensagem definida acima.

No próximo post vou aproveitar e atualizar o artigo de compilação da Boost, que esta incompleto e desatualizado.


Como Utilizar o Visual Studio C++ – Parte 2

Abril 1, 2009

No artigo anterior vimos como instalar e como criar um programa de teste no visual, agora vamos nos aprofundar um pouco mais no sistema de build do visual.

Configurações de Build

O visual por padrão possui duas configurações de build, a debug e a release. Cada versão permite que o usuário configure o compilador de maneiras totalmente diferentes, além de ser possível criar quantas configurações forem necessárias.

Estas configurações são uteis para permitir, por exemplo, desabilitar qualquer otimização do compilador e facilitar a depuração de código (fato que já ocorre na configuração Debug gerada quando um projeto é criado), sendo assim, a versão debug é geralmente usada apenas pelos desenvolvedores.

Já a versão release liga as otimizações do compilador e (geralmente) desliga a geração de informações de debug, sendo esta usada para se realizar um build quando queremos enviar o software ao usuário final.

A configuração atual pode ser visualizada no topo da janela do visual, próximo as opções de menu:

Caixa de seleção de configurações.

Na figura acima vemos que a configuração ativa é a Debug, clicando na caixa de seleção é possível alterar a configuração a ser usada no próximo build.

Além das configurações de build, é possível especificar uma plataforma para cada configuração, como por exemplo Win32 e Win64, como nunca trabalhei com desenvolvimento multi-plataforma no visual (nos projetos multi-plataforma que trabalhei usávamos uma IDE para cada ambiente) não vou me aprofundar nesse item.

Opções do Menu Build

Na figura abaixo podemos ver as opções do menu build do visual, que são descritas a seguir:

Menu de build do visual

Vemos que o menu de build é divido em quatro seções, sendo estas:

  1. Solução (solution): comandos que afetam toda a solução.
  2. Projeto: os comandos aqui afetam apenas o projeto e suas dependências.
  3. Lote (Batch): comandos que afetam múltiplas configurações.
  4. Compilar: este compila apenas o arquivo sendo editado.

Note que os comandos de compilação são basicamente:

  • Build: este comando compila apenas os arquivos alterados e as dependências afetadas por estes.
  • Rebuild: este comando apaga todos os arquivos gerados, forçando uma rê-compilação completa.
  • Clean: apenas apaga os arquivos gerados.

Detalhe que os comandos relacionados ao projeto não afetam unicamente o projeto sendo editado, podem afetar as suas dependências, se existirem múltiplos projetos dentro de uma solução, os comandos da seção projeto vão afetar as dependências também, para aplicar o comando apenas ao projeto selecionado, escolha as opções do item “Project Only”. Veremos o uso de dependências em mais detalhes no próximo post.

Opções de Runtime

O runtime (no caso do visual) é a forma como seu projeto é associado a biblioteca padrão do C ou C++ (a libc), que no caso do visual pode ser uma dll, as famosas MSVC???.lib, onde ??? varia de acordo com a versão e tipo de build, ou então, uma lib que é linkada diretamente com seu projeto.

Mas para que especificar uma biblioteca C/C++, não deveriam ser iguais? Sim, deveriam, mas a Microsoft disponibiliza dois tipos básicos, uma versão debug e outra versão release.

A versão debug da libc gera informações extras de depuração que veremos em detalhes no post sobre como usar o depurador do visual, já a versão release não possui essas informações e é construída com todas as otimizações possíveis.

Além da versão debug e release, existem para cada uma delas a versão DLL e não DLL. A diferença entre elas é que na versão DLL, seu código é linkado com a MSVC???.dll, na versão não dll (estática), o seu código é linkado diretamente com o arquivo lib não precisando da dll para ser executado.

Para configurar o runtime sendo usado basta clicar com o botão direito do mouse sobre um projeto, e escolher “Properties”:

menu de um projeto

Na janela que abrir, basta expandir a “Configuration Properties”, depois o item “C/C++”, e clicar em “Code Generation”, do lado deve surgir então o item “Runtime Library”, como na figura abaixo:

Propriedades de um projeto

As opções são:

  • Multi-threaded (/MT): versão release multi thread, essa é a versão para linkagem estática.
  • Multi-threaded Debug (/MTd): mesma anterior, mas versão debug.
  • Multi-threaded DLL (/MD): versão release multi thread, mas para linkagem dinâmica.
  • Multi-threaded Debug DLL (/MDd): mesma da anterior, mas versão debug.
  • inherit from project defaults: este simplesmente usa a configuração de um projeto pai (nesse caso a solução) ou configuração padrão.

Detalhe que todas as libs são especificadas como multi-threaded, isso indica que elas podem ser usadas com código multi-thread (antigamente existia também a opção single thread).

Um detalhe que pode passar despercebido sobre a janela de propriedades é que existe uma versão para cada configuração, a configuração sendo utilizada fica no canto superior esquerdo da janela de configurações.

Escolhendo o Runtime

A escolha do runtime depende muito do tipo de projeto, e a decisão se baseia entre versão DLL ou não DLL, sendo debug e release escolhidos de acordo com o tipo de build.

A versão DLL é recomendada se seu projeto utiliza dlls, isso é necessário porque se o seu projeto usar a versão não dll da libc, cada dll e o exe do seu programa vão ter sua própria heap. Como cada módulo possui sua própria heap, a memória alocada em um módulo (dll ou exe), utilizando new ou malloc, tem quer ser liberada apenas no mesmo módulo, a estrutura do programa fica como no exemplo abaixo:

suposto programa usando libc com linkagem estatica

Já o mesmo programa utilizando a versão da libc para dlls, fica com a estrutura como na figura abaixo:

o mesmo programa usando libc com linkagem dinâmica (dll)

Sendo assim, se seu projeto utiliza dlls, é recomendável linkar ele apenas com a versão dll da libc.

Arquivos Gerados no Build

Na configuração padrão de projetos do visual, ele cria duas sub-pastas uma para os builds de debug, e outra para release:

Estrutura de diretórios de uma solução

Nestas duas pastas são colocados os arquivos gerados durante o build, no caso do build de release do meu programa de testes:

Arquivos gerados pelo build

Exceto por hello.exe, todos os outros arquivos são utilizados apenas pelo visual durante um build. Isso significa que para enviar esse programa para alguem basta enviar o arquivo exe gerado, que é suficiente para este programa funcionar.

Outro detalhe que como o conteúdo destas pastas é todo gerado pelo visual, removendo qualquer um ou todos os arquivos não causa problema algum, basta executar o build que o visual os gera novamente.

O visual permite modificar a pasta usada para armazenar os arquivos gerados e a maneira mais simples consiste em acessar as propriedades do projeto (clicando com o botão direito sobre o projeto no Solution Explorer, e clicando em “Properties”), na janela que abrir, selecione a opção General, que fica dentro de “Configuration Properties”, deve surgir então a opção “Output Directory”, que indica o diretório usado:

Propriedades do projeto, alterando diretórios de saída

O caminho padrão é um pouco estranho, pois consiste de: $(SolutionDir)$(ConfigurationName). Isso na verdade são duas variáveis de ambiente criadas pelo visual, sendo que $(SolutionDir) e $(ConfigurationName) contém respectivamente o diretório da solução e o diretório da configuração sendo usada. Pode-se por exemplo modificar para: $(SolutionDir)\bin, com esta alteração o arquivo exe vai ser ser gerados no diretório bin.

Alterando o valor da opção “Intermediate Directory” modifica o diretório de destino dos arquivos temporários (como arquivos obj).

No próximo post, vamos ver como instalar a Windows SDK no visual.


Como utilizar o Visual C++ – Parte 1

Março 6, 2009

O Visual Studio é um pacote de programas da Microsoft para desenvolvimento de software, suportando diversas linguagens como C#, C++, C, Java, Visual Basic, etc. Nesta série de artigos vou focar apenas no Visual C++ Express 2008, mas a maioria da dicas / comandos devem funcionar em outras versões do Visual C++.

As versões express do Visual Studio são versões grátis que a microsoft disponibiliza e obviamente elas não possuem todas as funcionalidades das versões pagas, mas na minha opinião o Visual C++ Express é a melhor ferramenta “grátis” para se trabalhar com C++ no Windows.

Instalando

O primeiro passo é instalar a ferramenta, e o jeito mais simples é indo ao site oficial: Visual C++ Express. Logo ali no lado direito tem um botão “Download Now”, escolha a linguagem (Inglês no meu caso) e clique em Download.

Após concluído o download, execute o programa para inicializar a instalação que a principio é como qualquer outro programa, aceitar licença, instalar algum atalho extra, etc.

A versão atual do instalador pergunta se você quer instalar o Microsoft Silverlight Runtime e o Microsoft SQL Server, nenhum dos dois é necessário, a menos que você queira usar o SQL Server como banco de dados para suas aplicações ou utilizar o Silverlight

Opcionais do Visual C++ Express

Após escolher os adicionais, vem a escolha do diretório de instalação, de novo, é a gosto do fregues. Clicando em “Next” é iniciado o download. Existe no site da Microsoft imagens de CD para quem quiser instalar em um computador que não tenha acesso a rede.

Rodando

Após concluída a instalação, o instalador cria um atalho no menu iniciar, basta então acessar ele e clicar no “Microsoft Visual C++ Express Edition 2008″. O Visual vai carregar e você deve ver uma tela parecida com a abaixo:

Tela inicial do Visual

A versão express costuma solicitar registro, basta seguir o link do dialogo que surgir, entrar com os dados e depois inserir a chave de registro no visual.

Criando a primeira Solução (projeto)

O Visual Studio gerencia o software criado através de soluções, cada solução possui um ou mais projetos, que formam o software ou o conjunto de software sendo criado.

Para criarmos nosso “Hello Visual”, clique em “File” -> “New” -> “Project”. Surge então um dialogo parecido com o abaixo:

Criando novo projeto

Clique em Win32, depois selecione “Win32 Console Application”. Entre com o nome do projeto, diretório onde ele vai ser criado, e nome da solução (que é opcional), em seguida clique em “Ok”.

Configurando o novo projeto

Surge então a primeira tela do Application Wizard, clique em “Next”, na segunda tela selecione “Console Application” e marque a caixa “Empty Project “. Dessa forma é criada uma aplicação vazia, e sem o código de “Hello World” do visual. Depois de criado, o projeto pode ser modificado para aplicação com janela, dll ou biblioteca.

Selecionando o tipo de projeto a ser criado

Agora clique em “Finish” e o projeto vai ser criado.

Após criada a aplicação, deve surgir então o “Solution Explorer”, que é uma janela (que costuma ficar do lado esquerdo da tela) com a visão de todos os projetos e arquivos da sua solução.

Visão do Solution Explorer

No nosso caso ela vai estar vazia, agora criaremos o primeiro arquivo de código: clique com o botão direito do mouse no nome do projeto (meuPrimeiroProjeto, no exemplo), selecione “Add” -> “New Item”. Na janela que aparecer, selecione “C++ File (.cpp)”, entre com o nome do arquivo e clique em “Add”.

Criando novo arquivo para o projeto

Vai surgir então um lindo arquivo em branco, repare no “Solution Explorer” que o arquivo foi adicionado ao seu projeto, agora basta entrar com o código do “Hello World”:

#include <stdio.h>
int main(int argc, char **argv)
{
    printf("Hello Visual");
    return 0;
}

Após entrar com o código, clique na opção “Build” (menu principal) e selecione “Build”, ou então pressione F7. Na parte de baixo da tela vai surgir a tela de output, que mostra o que o compilador esta fazendo, e no final do processo ela indica se houve algum erro ou não. Se aconteceu algum erro, dando um clique duplo sobre a mensagem de erro foca o mesmo na tela.

Programa de teste após compilação

Rodando o “Hello World”

Agora que o projeto já foi compilado, basta executar ele. No menu principal, selecione “Debug” -> “Start Without Debugging”, ou pressione “CTRL + F5″. Pronto, vai surgir uma janela de console com a saída do seu programa.

No proximo post, vamos aprender um pouco mais sobre o build do visual.


Strings e Números

Janeiro 23, 2009

No ultimo post mostrei as operações básicas com strings como contar caracteres e comparações, agora vamos nos aprofundar um pouco mais.

Primeiramente, como funciona um char? O char em C é um int pequeno (geralmente 8 bits) sinalizado (o char pode ser sinalizado ou não, dependendo da plataforma), a questão é que o char armazena números, e os números representam caracteres. Mas qual número é qual? O Visual (e acho que quase todos compiladores) usam como base a tabela ASCII que define o valor numérico de cada caracter.

Com base nisso, podemos então escrever:


#include <stdio.h>

int main(int, char **)
{
    char ola[4];

    ola[0] = 111;
    ola[1] = 108;
    ola[2] = 97;
    ola[3] = 0;

    printf(ola);

    return 0;
}

No código acima inicializamos a string “ola” usando os valores numéricos dela, mas qual a vantagem disso? Nenhuma, na verdade apenas facilitamos a vida do compilador, mas o que pode ser feito de útil com esse conhecimento?

A primeira coisa que pode ser feita é traduzir o outdoor da EA:

ea_canada_tbwa_vancouver_01

 

Agora vamos supor que seja necessário criar um programa para verificar se uma string possui apenas caracteres de A a Z (caixa alta):

#include <iostream>

bool ehAZ(const char *str)
{
    for(int i = 0;str[i]; ++i)
    {
        if((str[i] < 'A') || (str[i] > 'Z'))
            return false;
    }

    return true;
}

int main(int argc, char **argv)
{
    using namespace std;

    cout << ehAZ("ABC") << endl;
    cout << ehAZ("ABaC") << endl;
    cout << ehAZ("!@#$%") << endl;

    return 0;
}

Note que na tabela ASCII letras e números estão em sequência, por isso não precisamos nos preocupar em testar todas as letras, podemos testar apenas sequências.

Convertendo para Letras Maiúsculas

Agora vamos ver como fica uma rotina para mudar todas as letras de uma string para maiúsculas:

#include <iostream>

char *toUp(char *str)
{
    for(int i = 0;str[i]; ++i)
    {
        if((str[i] >= 'a') && (str[i] <= 'z'))
            str[i] = 'A' + (str[i] - 'a');
    }

    return str;    
}

int main(int argc, char **argv)
{
    char str[] = "abCd!@#$456z";

    using namespace std;

    cout << toUp(str) << endl;

    return 0;
}

A função toUp é bem simples, ela percorre a string procurando caracteres que estejam no intervalo de ‘a’ a ‘z’, e quando encontra converte o caractere para sua versão maiúscula. A formula de conversão  primeiramente calcula qual o índice da letra sendo convertida (que seria o número da letra no alfabeto, mas começando de zero) e depois soma esse índice ao índice inicial das letras maiúsculas.

Vamos pegar como exemplo a conversão da letra ‘c’, primeiramente é feito str[i] – ‘a’, o que resulta em ‘c’ – ‘a’, chegando em 99 – 97, resultando 2. Na sequência é feito ‘A’ + 2, que significa: 65 + 2 = 67. Consultando a tabela temos: 67 = C.

Apenas para lembrar, já existe uma função na biblioteca padrão para conversão de letras minusculas para maiúsculas chamada toupper, que é declarada em ctype.h. Neste header também existem outras funções do mesmo genero, como: tolower, islower, isupper, etc.

Convertendo para Números

Por fim, vamos ver como converter uma string que contêm um número inteiro para uma variável to tipo int:

#include <iostream>

int myAtoi(const char *str)
{
    int len = (int) (strlen(str)-1);
    int num = 0;
    int dec = 1;
 
    for(;len >= 0; --len)
    {
        if(str[len] >= '0' && str[len] <= '9')
        {
            num += dec * (str[len] - '0');
            dec *= 10;
        }        
        else if((len == 0) && (str[len] == '-' || str[len] == '+'))
            num = (str[0] == '-') ? -num : num;
        else
        {
            //numero invalido, entao 0
            return 0;
        }
    }    

    return num;
}

int main(int argc, char **argv)
{
    using namespace std;

    int num = myAtoi("23");

    cout << num << endl;
    cout << myAtoi("-10") + myAtoi("5") << endl;

    return 0;
}

A função myAtoi processa a string e retorna o int que ela representa, note que a função processa a string do final para o começo para facilitar os cálculos. No loop principal primeiramente é verificado se o caracter atual é um número (ou seja, tem que estar entre ‘0′ e ‘9′). Se for um número, a variável num é atualizada com a formula: dec * (str[len] – ‘0′). O código str[len] – ‘0′ calcula qual o valor do número atual (da mesma forma que foi feita na conversão de letras para caixa alta). Achado o número, multiplicamos ele por dec, que armazena qual a casa decimal que ele se encontra (unidade, dezena, centena, etc), e por fim o valor é somado com o número atual. Depois disso a casa decimal é atualizada (em dec *= 10).

Caso o caracter não seja um número, é verificado se a string contêm o sinal + ou – na primeira posição, caso sim, o sinal do número é atualizado de maneira adequada. Caso o caractere em questão não seja número ou sinal, a função retorna 0. O correto seria retornar algum erro, mas para não complicar o exemplo ficamos com o zero.

Na biblioteca padrão existem as funções atoi e strtol que fazem conversões de strings para números.

A conversão de int para string fica como lição de casa, uma boa leitura para se aprender mais é o artigo sobre tipos básicos do Caloni: Básico do básico: tipos.


Strings em C

Dezembro 11, 2008

Um item que vejo muitos novatos na linguagem tendo dificuldades são as strings em C, por isso resolvi escrever alguns posts e tentar resolver muitas das duvidas que vejo por ai.

Para começo de conversa strings em C praticamente não existem, o compilador tem apenas uma vaga noção do que é uma string, pois elas são uma convenção onde um vetor de caracteres terminado com 0 (zero) representa uma string. Sendo assim, podemos criar uma string usando:


#include <stdio.h>

int main(int, char **)
{
    char ola[4];

    ola[0] = 'o';
    ola[1] = 'l';
    ola[2] = 'a';
    ola[3] = 0;

    printf(ola);

    return 0;
}

Este código simplesmente imprime “ola” na tela, mas note que ao inicializar ola colocamos o caracter ‘\ 0′ (que nada mais é que o numero 0) no final do array, ele é necessário pois é a única forma do printf saber onde termina a string. Agora escrever esse código toda vez que precisarmos criar uma string é bem chato, por isso, uma das poucas coisas que o compilador C sabe sobre strings é:

#include <stdio.h>
 
int main(int, char **)
{
    char ola[] = "ola";

    printf(ola);

    return 0;
}

Este código é idêntico ao anterior, mas escrito de uma maneira bem mais simples. Note que o compilador reconhece a string “ola” e declara um array de 4 chars e já inicializa ela com a string “ola” (incluindo o ). Também podemos escrever:

#include <stdio.h>

int main(int, char **)
{
    const char *ola = "ola";

    printf(ola);

    return 0;

}

Este código já é um pouco diferente do anterior, aqui não criamos um vetor de caracteres, e sim um ponteiro para uma região de memória constante do tipo char, trocando por miúdos, criamos um ponteiro que aponta para string. Mas onde diabos foi parar a string? O compilador alocou um trecho de memória constante na seção de dados do código (uma variável global). Por isso usamos const, pois esta string não deve ser modificada pelo código.

Contando Caracteres

Agora que já sabemos como uma string funciona em C, vamos fazer uma função para contar quantos caracteres uma string tem, assim podemos ver como o 0 no final dela é usado:

#include <stdio.h>

int contaChar(const char *str)
{
    int i = 0;

    for(;str[i] != 0; ++i);

    return i;
}

int main(int, char **)
{
    char ola[] = "ola";

    printf("A string %s possui %d caracteres\n", ola, contaChar(ola));

    return 0;
}

 
Note que sempre que precisamos saber quantos caracteres uma string possui, precisamos percorrer toda a string, isso pode trazer problemas de performance em algumas aplicações, e isto ocorre com quase todas as operações com string.

 

Outro detalhe, não é preciso criar a função contaChar, quando precisar saber o tamanho de uma string basta usar a strlen, que é declarada no string.h:

#include <stdio.h>
#include <string.h>

int main(int, char **)
{
    char ola[] = "ola";

    printf("A string %s possui %d caracteres\n", ola, strlen(ola));

    return 0;
}

Neste mesmo arquivo existem varias outras funções para se trabalhar com strings, é recomendável dar uma olhada na documentação antes de escrever a sua própria função para verificar se ela já não existe (alias, isso é recomendável para qualquer coisa, não apenas strings).
 

Comparando Strings

Agora chegamos ao ponto onde a maioria tem dificuldades, como saber se uma string é igual a outra? A idéia inicial é escrever:

#include <stdio.h>

int main(int, char **)
{
    char ola[] = "ola";
    char ola2[] = "ola";

    if(ola == ola2)
        printf("Iguais");
    else
        printf("Nao sao iguais");

    return 0;
}

Se você entendeu tudo até aqui já deve imaginar qual vai ser a saída do programa acima, se não, tente ler novamente e execute o programa. Muitos ficam surpresos ao ver o programa imprimir “Não são iguais”. Isto ocorre porque o if esta na verdade comparando dois ponteiros (lembre-se, arrays sem índice são ponteiros).

Então como fazer para saber se duas strings são iguais? É necessário comparar todos os caracteres das strings:

#include <stdio.h>

bool saoIguais(const char *s1, const char *s2)
{        
    for(int i = 0;s1[i] == s2[i]; ++i)
    {                
        if(s1[i] == 0)
            return true;
    }
    return false;
}

int main(int, char **)
{
    char ola[] = "ola";
    char ola2[] = "ola";

    if(saoIguais(ola, ola2))
        printf("Iguais");
    else
        printf("Nao sao iguais");

    return 0;
}

Agora já sabemos como identificar se duas strings são iguais ou não, um pouco trabalhoso, mas é o único jeito. Para simplificar um pouco a vida, a biblioteca padrão já vem com uma função de comparação de strings chamada strcmp, que funciona de maneira similar a função saoIguais, a diferença é que essa função não verifica apenas se as duas strings são iguais, ela compara as duas, retornando 0 quando são iguais, -1 quando a primeira string vem antes da segunda, ou 1 caso contrário:

#include <stdio.h>
#include <string.h>

int main(int, char **)
{
    char ola[] = "ola";
    char ola2[] = "ola";

    if(strcmp(ola, ola2) == 0)
        printf("Iguais\n");
    else if(strcmp(ola, ola2) != 0)
        printf("Nao sao iguais");

    return 0;
}

Pronto, agora já temos uma maneira simples de verificar se duas strings são iguais ou não. 

No próximo post, vamos dar uma olhada em mais algumas operações com strings em C.


C++ Type Casting – 3ª e ultima parte

Novembro 5, 2008

No post anterior vimos como realizar as operações de casting usando os novos operadores do C++, neste post vamos ver como realizar casting com os smart pointers da Boost , estes operadores podem ser usados com o shared_ptr e o intrusive_ptr.

Mas primeiramente, porque existem operadores de cast específicos da Boost? Pelo fato de que os operadores do C++ não estão preparados para lidar com smart pointers e contagem de referências, dessa forma, caso se utilize um operador padrão do C++ a contagem de referência do objeto pode se tornar invalida, resultando em um objeto sendo destruído mais de uma vez. Exemplo:


enum Eventos_e
{
    TIRO,
    ABRIR_PORTA
};

class Objeto
{
    public:
        virtual void Evento(int tipo, void *param) = 0;
        virtual void Atualizar()=0;
};

class Monstro: public Objeto
{
    public:
        virtual void Evento(int tipo, void *param);
        virtual void Atualizar();
       
    private:
        int m_energia;
};

Com base na hierarquia acima, vamos criar alguns objetos e fazer um cast:


int main(int, char **)
{
    using namespace boost;

    shared_ptr<Objeto> obj(new Monstro());
   
    shared_ptr<Monstro> monstro(static_cast<Monstro *>(obj.get()));   
}

Repare que na linha onde é feito o cast é usado o método get do shared_ptr para se obter uma referência ao objeto que obj armazena, depois é feito o cast e o ponteiro resultante é utilizado para inicializar o ponteiro “monstro”, note que monstro foi inicializado com um ponteiro comum, não com outro shared_ptr, ou seja, para o shared_ptr “monstro” ninguém até o momento gerenciava esse ponteiro, então ele vai simplesmente começar uma nova contagem de referências, dessa forma, quando a função terminar de executar, ambos os smart pointers vão destruir o objeto Monstro, resultando em um comportamento indefinido.

Para evitar esses problemas foram criados os casts a seguir:

Cada um dos casts acima deve ser usado nas mesmas situações dos casts do C++, a diferença é que eles devem ser apenas usados quando os objetos envolvidos são gerenciados por smart pointers.

No caso do exemplo anterior, o código correto é listado abaixo:


int main(int, char **)
{
    using namespace boost;

    shared_ptr<Objeto> obj(new Monstro());
   
    shared_ptr<Monstro> monstro(static_pointer_cast<Monstro>(obj)));   
}

Note que as operações de cast da boost são idênticas as do C++, a diferença é o nome do operador usado.

As operações de cast da Boost devem ser sempre usadas quando é necessário fazer casts entre ponteiros gerenciados por smart pointers da mesma, nos outros casos, deve-se utilizar os operadores do C++.


C++ Type Casting – 2ª Parte

Setembro 18, 2008

No post anterior vimos como funciona o casting que o C++ herdou da linguagem C, agora vamos ver como funcionam os novos operadores de casting do C++

static_cast

O static_cast é o mais simples de todos, ele faz em partes o trabalho do cast do C, mas com algumas restrições que veremos na sequência. Baseando-se no exemplo do post anterior onde tínhamos as classes Objeto, Monstro e Tiro pode-se construir:

void CriaExplosao(int potencia);
void ExplodeTiro(Objeto *obj)
{
Tiro *tiro = static_cast<Tiro *>(obj);

CriaExplosao(tiro->Potencia());
}

Por alguma obra do destino que não vem ao caso (ou por falta de criatividade do autor com exemplos) a função ExplodeTiro recebe como parâmetro uma referência para Objeto, e não tiro. Por se tratar da função ExplodeTiro sabemos e confiamos friamente que os programadores sempre vão usar ela passando como parâmetro uma classe Tiro.

O static_cast não faz verificação nenhuma para checar se o Objeto passado como parâmetro é da classe Tiro ou não, apenas ajusta algum endereço se necessário, nada mais. Agora vamos imaginar que um novo programador começou a trabalhar no projeto e sem saber das consequências escreveu:

void func()
{
Monstro m;

ExplodeTiro(&m);
}

O que vai acontecer? Se da ultima vez o cachorro da vizinha latiu, dessa vez é provável que ele exploda, quem sabe o cãozinho que faleceu segundo relataram alguns leitores não ressuscite com essa técnica? Piadas a parte, o comportamento é totalmente imprevisível nesse caso.

A vantagem de se usar o static_cast é que o compilador faz algumas verificações antes de usá-lo:

Tiro tiro;
Monstro *m = static_cast<Monstro *>(&tiro);

O código acima vai dar erros, porque Tiro e Monstro são classes que não podem ser convertidas entre si. Se fosse utilizado um cast do C o compilador aceitaria tudo sem problemas.

Outra vantagem do static_cast é que ele somente permite seu uso com tipos definidos:

class X;
class Y;
void proc(X *x)
{
Y *p = static_cast<Y *>(x);
}

Esse é o mesmo exemplo do artigo do Bjarne listado nas referências abaixo, o interessante desse código é que ele não compila, pois o compilador ainda não conhece a estrutura das classes X e Y, evitando assim a geração de código que pode fazer alguem perder a noite com um bug misterioso.

O static_cast também evita erros como remoção de const por acidente, acesso a membros privados, etc. Dessa forma ele impõe uma série de restrições ao código, minimizando erros que poderiam passar despercebidos.

dynamic_cast

Este cast é utilizado quando é preciso fazer um cast de uma classe base para uma classe derivada. No caso da função ExplodeTiro acima, podemos melhora-la trocando o static_cast pelo dynamic_cast:

void ExplodeTiro(Objeto &obj)
{
Tiro *tiro = dynamic_cast<Tiro *>(&obj);
if(tiro == NULL)
return;

CriaExplosao(tiro->Potencia());
}

Note que caso a conversão não seja possível, o dynamic_cast retorna NULL, dando oportunidade ao programador de verificar se o tipo é valido ou não. No caso da função acima, decidimos simplesmente parar a execução da função quando não é possível fazer a conversão.

O dynamic_cast é o cast mais complexo em termos computacionais do C++, pois ele precisa realizar algumas buscas pela hierarquia de objetos e isso algumas vezes pode levar um tempo precioso, sendo assim, deve-se usá-lo com moderação.

Agora no dia a dia, se o programador tiver certeza absoluta de que o tipo a fazer cast é da classe derivada, não existe problema algum em usar o static_cast ao invés do dynamic_cast (a não ser o risco de fazer coisa errada). Existem códigos onde ao invés dos casts, existe uma template onde na versão debug do código é usado um dynamic_cast com assert, na versão release é usado apenas um static_cast.

Outro detalhe do dynamic_cast é que para ele funcionar é necessário ligar a geração de RTTI (Run Time Type Information – Informação de Tipos em Tempo de Execução), que é opcional em alguns compiladores (Visual Studio por exemplo). Pode ser que algum compilador esse item não seja opcional, mas para o dynamic_cast funcionar ele tem que existir. No caso do visual, se alguem tenta usar o dynamic_cast sem RTTI ele vai gera um erro em tempo de compilação.

reinterpret_cast

Este cast é utilizado quando queremos converter um tipo para um outro tipo não relacionado, como por exemplo de char* para int*. Ele é apenas uma indicação ao compilador de que você sabe o que esta fazendo. Note que ele também pode ser usado para tipos que ainda não foram definidos, mas atenção, o reinterpret_cast diferentemente do dynamic_cast não navega pela hierarquia de classes. Exemplos:

void ExplodeTiro(Objeto *obj)
{
Tiro *tiro = reinterpret_cast<Tiro *>(obj);

CriaExplosao(tiro->Potencia());
}

No exemplo acima o código compila, mas a não ser que a intenção seja realmente essa, esse código somente vai dar problemas. No primeiro cast da classe Objeto para Tiro quando o reinterpret_cast é usado os ponteiros não são ajustados. O ajuste de ponteiros é feito quando trocamos ponteiros de uma classe filha para uma classe pai ou vice versa, isso depende da implementação, mas baseado no exemplo, o ponteiro para Tiro pode não possuir exatamente o mesmo endereço do ponteiro para Objeto (mesmo se tratando do mesmo objeto). Geralmente em hierarquias simples (sem herança múltipla ou herança virtual) o endereço é sempre o mesmo.

O reinterpret_cast é recomendado apenas para operações onde se deseja converter um tipo básico para ponteiro e vice-versa:

void func()
{
int i = 5;
char *p = reinterpret_cast<char *>(i);

++p;

i = reinterpret_cast<int>(p);
}

Outro detalhe é que o reinterpret_cast também preserva constness de tipos assim como o static_cast.

const_cast

O ultimo tipo e o mais simples de todos é o const_cast, este cast simplesmente tira o const de um tipo:

void ExplodeTiro(const Objeto *obj)
{
Tiro *tiro = const_cast<Tiro *>(static_cast<const Tiro *>(obj));

CriaExplosao(tiro->Potencia());

//um cast tipico apenas para remover o cont
Objeto *o = const_cast<Objeto *>(obj);
}

Note que foi usado um static_cast que converte para “const Tiro *”, pois se fosse usado dentro do static_cast apenas “Tiro *” o compilador iria reclamar, então com o resultado dele é aplicado o const_cast, que remove o const do tipo. As pessoas reclamam que é muito código para pouca coisa, mas em troca disso temos um código mais seguro, além de ficar bem explicito quais são as intenções do programador e para terminar, essas operações não implicam custos extras em relação a um cast tradicional do C, então o uníco motivo para não usar é preguiça na hora de digitar.

Sobre o exemplo acima, o ideal é mudar o método potência e transforma-lo em um método const, mas vamos supor que isso não fosse possível por alguma outra obra do acaso :) .

Conclusões

Os casts do C++ trazem mais segurança ao programador e ajudam a evitar erros de conversões invalidas. Apesar de eles serem muito mais longos que um cast normal (em termos de tamanho de código a se digitar) é vantajoso utiliza-los em vista das vantagens que eles trazem.

Caso esteja em duvida sobre qual cast utilizar, utilize o static_cast, se o compilador reclamar:

  • Se for um const sendo removido, estude o caso e veja se é realmente necessário, caso sim, utilize o const_cast.
  • Se for algum tipo incompleto, adicione o include apropriado para definir o tipo ou veja se é necessário trocar por um reinterpret_cast.

Referência

New Casts Revisited – Bjarne Stroustrup: a base de todo esse post, contém vários exemplos de como utilizar e de como bagunçar tudo com os casts do C++.


C++ Type Casting – 1ª Parte

Agosto 27, 2008

A linguagem C++ (assim como a linguagem C) possui operações de casting de tipos (Type Casting) que é uma operação em que se converte uma expressão de um determinado tipo para outro tipo, essas conversões podem ser feitas de duas maneiras: implícita e explicita.

Conversão Implícita

Nesse caso, o programador não precisa fazer nada, pois a conversão de dados é feita automaticamente sem que ele precise usar qualquer instrução extra:


int main(int, char **)
{
	char a = 5;
	int i = a;
}

No código acima não existe nada demais, certo? O único detalhe é que quando fazemos i = a, essa não é uma atribuição comum, o compilador precisa gerar código extra para converter o char para int. Esta conversão é feita de maneira implícita e silenciosa pelo compilador porque qualquer valor de char pode ser armazena em um int. Já um int, não pode ter todos os seus valores armazenados em um char. Se pegarmos como exemplo um compilador 32 bits, onde o char geralmente tem 8 bits, os seus valores vão de -128 a 127, já um int (32 bits) pode possuir valores de -2147483648 a 2147483647.

Se fizermos ao contrário:


int main(int, char **)
{
	int i = 2147483647;
	char a = i;

}

Alguns compiladores vão gerar avisos com o código acima (no caso do Visual, tem que mudar o Warning level de 3 para 4). O problema do código acima é que quando atribuímos um valor de i para a, a variável char não tem como armazenar todos os valores de um inteiro, nesse caso, pode haver uma perda de dados.

Conversão Explicita

Se você estiver usando o compilador com warnings no máximo, é possível evitar o aviso usando o código abaixo:


int main(int, char **)
{
	int i = 2147483647;
	char a = (char)i;

}

O código gerado acima vai ser idêntico ao anterior, o cast nesse caso não faz nada em relação ao código gerado, você apenas esta sinalizando: “Cale a boca compilador que eu sei o que estou fazendo”. O compilador entende o seu recado e não lhe da mais aviso nenhum.

Agora vamos provocar a ira do compilador:


#include <string>
#include <vector>

int main(int, char **)
{
	using namespace std;

	vector<int> v;
	string *str = &v;

	*str = "abc";

    return 0;
}

O código acima deu um belo de um erro…, isso ocorre porque o C++ tenta evitar que o programador faça coisas erradas, vamos tentar consertar a situação:


#include <string>
#include <vector>

int main(int, char **)
{
	using namespace std;

	vector<int> v;
	string *str = (string*)&v;

    *str = "abc";

    return 0;
}

Agora novamente o compilador ficou quieto, mas o que aconteceu? Quase nada, quando colocamos o (string*) estamos dizendo ao compilador: “Ei, o valor da expressão &v é o endereço de uma string”. O compilador simplesmente aceita (afinal nós sabemos o que estamos fazendo) e copia o valor para o ponteiro str. Atenção ao detalhe “copia o endereço”, pois lembre-se que ponteiros no fundo são apenas um unsigned int com propriedades quase magicas. Note também que não foi feito nada quanto a conversão de tipo.

O que vai acontecer quando esse código for executado? Pode acontecer qualquer coisa:

  • O código pode simplesmente funcionar por alguma obra do acaso;
  • Dar uma falha de segmentação;
  • Seu computador explodir;
  • O cachorro do vizinho passar mal

No meu caso, deu um baita erro de assertion failed. Segunda a especificação, esse tipo de operação tem como resultado “undefined” ou seja, indefinido. Mas note que o problema não esta no casting, ele é totalmente valido, o problema ocorre quando tentamos jogar “abc” dentro de str. Nesse caso o operador de atribuição da string vai ser chamado, mas o ponteiro this dele ao invés de apontar para um objeto string, vai estar apontando para o vector!

Exemplo: Sistema de Eventos

Se isso causa apenas problemas, porque existe então? Bom, isso permite fazer algumas coisas interessantes… vamos imaginar que você esta criando um jogo, e nesse jogo você possui uma lista de objetos, que podem ser qualquer coisa: um monstro, um tiro, uma porta, chave, etc.

Todos esses objetos possuem uma classe base em comum, chamada Objeto:


enum Eventos_e
{
	TIRO,
	ABRIR_PORTA
};

class Objeto
{
	public:
		virtual void Evento(int tipo, void *param) = 0;
		virtual void Atualizar()=0;
};

class Monstro: public Objeto
{
	public:
		virtual void Evento(int tipo, void *param);
		virtual void Atualizar();

	private:
		int m_energia;
};

class Tiro: public Objeto
{
	public:
		virutal void Evento(int tipo, void *param);
		virtual void Atualizar();

		int Potencia();

	private:
		Objeto *MovimentarEColidir();
};

Agora, vamos imaginar que o monstro estava andando e levou um tiro de laser. Quando qualquer coisa acontece no nosso jogo, chamamos o método Evento e passamos as informações necessárias. O método atualizar é chamado o tempo todo para que os objetos se atualizem (como por exemplo movimentar o tiro, andar com o monstro, etc).

Vamos agora implementar o método Atualizar do tiro:


void Tiro::Atualizar()
{
	Objeto *obj = MovimentarEColidir();
	if(obj)
	{
		obj->Evento(TIRO, this);
	}
}

O método é bem simples, vamos assumir que o método MovimentarEColidir atualiza a posição do tiro e verifica se ele bateu com algo, caso isso ocorra, o objeto com o qual ele colidiu é retornado. Quando isso ocorre, o tiro envia um evento ao objeto avisando-o do ocorrido, dessa forma não precisamos poluir a interface da classe Objeto com todo o tipo de evento que pode acontecer (tiro, abrir, usar item, etc). Por outro lado, esse tipo de código é complicado de debugar e não é muito intuitivo, além de permitir que o programador faça muitas burradas sem aviso algum do compilador.

Agora vamos ver como fica o código do Monstro:


void Monstro::Evento(int tipo, void *param)
{
	if(tipo == TIRO)
	{
		Tiro *tiro = (Tiro *)param;

		energia -= tiro->potencia();
	}
}

Note que quando recebemos um evento do TIRO, simplesmente fazemos um cast do ponteiro e diminuímos a energia do monstro com base na potencia do tiro, extremamente simples.

Esse tipo de código funciona muito bem em jogos (apesar de existirem outras técnicas para se conseguir o mesmo feito, algumas melhores, outras nem tanto). Esta técnica serve para outros sistemas também, um exemplo é a API do Windows que usa essa técnica para fazer a comunicação entre os componentes da GUI.

No próximo post, vamos ver como usar os operadores de casting do C++.