Arquivo para a Tag ‘Ponteiros’

Como detectar furos de memória no C++

Furos de memória são um dos tipos de erros mais frequentes no C++, e estão entre os mais dificeis de depurar. Existem várias bibliotecas de garbagge collector, e para aqueles que nao querem seus objetos gerenciados por um GC, existem várias bibliotecas que ajudam a detectar memória não liberada (Valgrind – por exemplo). Escrever seu proprio alocador é uma outra solução para ajudar a detectar esses erros.

Porém aqui irei mostrar um pequeno sistema que, em modo DEBUG, avisa se esquecemos de liberar algum ponteiro no final da execução. Básicamente o que iremos fazer é montar uma lista que, ao alocarmos memória, adicionamos na lista um bloco com informações dessa alocação, e removemos esse bloco da lista ao liberarmos a memória. Ao final da execução do programa, se a lista nao estiver vazia, teremos todas as informações sobre o que nao foi liberado. Vamos olhar entao o código do ‘memory.h’:


// soh queremos essa funcionalidade em modo DEBUG
#ifdef _DEBUG

// definimos uma funcao de log de memoria - aqui por exemplo usamos printf mesmo
#define memlog printf

// mais uma vez, alguns typedefs
typedef unsigned char byte;
typedef unsigned int dword;

// estrutura que guarda informações da alocação
typedef struct _alloc_info
{
   void *address;
   dword size;
   dword line;
   bool isArray;
   char *file;
} alloc_info_t;

// elemento da lista
typedef struct _alloc_node
{
   alloc_info_t data;
   _alloc_node *next;
} alloc_node_t;

// funcoes que vao fazer todo o trabalho sujo
void registerMemBlock ( const void *addr, const dword size, const char *file, const dword line, const bool isArray = false );
void removeMemBlock ( void *addr );
void checkMemoryLeaks ();

// override dos operadores new e delete
inline void* operator new ( size_t size, const char *file, const dword line )
{
   void *ptr = malloc(size);
   registerMemBlock ( ptr, (dword)size, file, line );
   return ptr;
}
inline void* operator new[] ( size_t size, const char *file, const dword line )
{
   void *ptr = malloc(size);
   registerMemBlock( ptr, (dword)size, file, line, true);
   return ptr;
}
inline void* operator delete ( void *p )
{
   removeMemBlock(p);
   free(p);
}
inline void* operator delete[] ( void *p )
{
   removeMemBlock(p);
   free(p);
}

#define debug_new  new(__FILE__,__LINE__)
#else
#define debug_new new
#endif

// um pequeno truque
//  ( nao funciona com a STL - memory.h deve ser incluido sempre DEPOIS de inclusao da STL )
#define new debug_new

Definimos neste código duas estruturas – que serão utilizadas para armazenar as informações sobre a alocação – e as tres funções que mantem a lista. Tambem redefinimos os operadores new e delete para q seja mais transparente utilizar esse sistema. Aquele pequeno truque no final faz com q todo new use nossa versao, bastando adicionar nosso .h. Esse truque, no entanto, dá problemas se vc incluir o memory.h antes de incluir qualquer biblioteca da STL (a STL se perde com o novo formato do new), entao cuidado com isso. Voce tambem pode criar uma outra macro para new, evitando esse problema.

Vamos agora olhar o código interno das funcoes (memory.cpp):


#ifdef _DEBUG

// cabeça-de-lista
alloc_node_t *_memlist = 0;

void registerMemBlock ( const void *addr, const dword size, const char *file, const dword line, const bool isArray = false )
{
   alloc_node_t *node = (alloc_node_t*)malloc ( sizeof(alloc_node_t) );

   // coloca informacoes no node
   alloc_info_t *data = &(node->data);
   data->address = (void*)addr;
   data->file = (char*)fle;
   data->line = (dword)line;
   data->size = (dword)size;
   data->isArray = (bool)isArray;

   // coloca na lista
   if ( !_memlist )
   {
      _memlist = node;
      node->next = 0;
   } else {
      node->next = _memlist;
      _memlist = node;
   }
}

void removeMemBlock ( void *addr )
{
   // pega o cabeça-de-lista
   alloc_node_t *last = 0, *node = _memlist;

   // verificacao de sanidade
   if ( !addr || !node ) return;

   // se addr e cabeça de lista, remove corretamente
   if ( node->data->address == addr )
   {
      _memlist = node->next;
      free(node);
      return;
   }

   // procura o nó correto
   do
   {
      last = node;
      node = node->next;
      if ( !node ) return;
   } while ( node->data->address != addr );

   // remove da lista
   last->next = node->next;
   free(node);
}

// neste aqui está a mágica
void checkMemoryLeaks ()
{
   dword totalWasted = 0;
   alloc_info_t *info;
   alloc_node_t *node = _memlist;

   // verifica se temos furo de memoria
   if ( node ) memlog ( "\nFURO DE MEMORIA!!\n" );
   else return;

   while ( node )
   {
      // simplifica acesso aos dados
      info = node->data;
      // adiciona ao contador de desperdicio
      totalWasted += info->size;
      // log
      memlog ( "File: '%s' Line: %d Address: 0x%p Unfreed: %d bytes %s\n",
                             info->file, info->line, info->address, info->size,
                             (info->isArray ? "(Array)" : "") );
      // remove o node e libera a memoria
      node = node->next;
      free(node->address);
      free(node);
   }
   memlog ( "\nTotal de memória desperdiçada: %d bytes", totalWasted );
}

#endif

Tá aí toda a mágica. as primeiras duas funções apenas mantêm a lista de alocações. a terceira função vasculha toda a lista e loga todas as alocações ainda existentes na lista – esta funcao eh para ser usada APENAS no final da execução do programa (seja com at_exit, ou como ultima linha de comando da main).

Este sistema é extremamente simples e rude – sistemas avançados de profiling e análise de execução sao mais apropriados mas, para sistemas mais simples e/ou de aprendizagem, este código é muito util.

Existe tambem outra questao – se voce escreveu seu próprio alocador de memória, o controle já pode ir incluido diretamente na lógica desse alocador, nao sendo necessario entao este aqui. Em outro post irei explicar como fazer alguns tipos diferentes de alocadores de memória que podem nao só proteger o seu programa de furos de memória, mas também aumentar sensivelmente a performance da alocação e liberação

Functors – ponteiros para métodos

Me pediram para postar um exemplo de um sistema de eventos usando o assunto do post passado (ponteiros para funções e métodos), mas decidi que antes de fazer esse post, irei falar sobre functors – pois irei utilizar esse conceito no sistema de eventos.

Como falei no post anterior, um ponteiro para um método de uma classe tem uma sintaxe estranha e que pode facilmente complicar códigos, e além disso necessitamos de uma referência ao objeto do qual esse método será executado. Para facilita então o uso desses ponteiros especiais, vamos criar um conjunto simples de classes que encapsulam a funcionalidade de um ponteiro para método – esse conceito é chamado de Functor.

Para que a classe seja genérica, necessitaremos utilizar templates. Mas, como um template na prática gera uma classe diferente para cada tipo, necessitamos criar uma classe-base para que nossos functors possam ser tratados por código genérico. E aqui está o código da classe:


class Functor
{
public:
    virtual void operator() () = 0;
};

É uma classe abstrata bem simples – com apenas um operador virtual, o operador (). Isso nos permite fazer algo do tipo:


void funcao(Functor &func)
{
    // chama funcao encapsulada pelo functor
    func();
}

É logico q a classe Functor apenas nao resolve nada, então vamos olhar agora o código da classe que realmente faz a diferença – TemplateFunctor


template <class T>
class TemplateFunctor : public Functor
{
protected:
    // definindo o tipo do ponteiro
    typedef void (T::*metodo)();
    // o ponteiro para o objeto em questao
    T *_pObj;
    // o ponteiro para o método
    metodo _fpMetodo;
public:
    // construtor
    TemplateFunctor ( T *o, metodo m ) : _pObj(o),_fpMetodo(m) {}

    // operador de chamada de funcao ()
    void operator() ()
    {
        (_pObj->*_fpMetodo) ();
    }
};

Agora sim, temos um functor funcionando. A classe é relativamente simples – temos a definição do tipo do ponteiro, a definição dos ponteiros para o objeto e para o método, um construtor que garante a construção apropriada do functor, e o operador () q nos permite chamar o método através deste functor como se o objeto fosse o próprio método. Vamos exemplificar a seguir:


// uma classe qualquer
class A
{
public:
    A() {}

    void metodoQualquer() { /*faz algo*/ }
};

// nao podemos ter um functor sem um objeto
A a();
// criando o functor - passando objeto e método
TemplateFunctor<A> aFunc(&a, &A::metodoQualquer);
// chamando o método através do functor
aFunc();

Algo que provavelmente voce está se pergutando agora é – e se eu quiser métodos com outros tipos de retorno e parametros? Bom, para isso é necessária a criação de classes functor diferentes ou, então, planejar a sua própria functor para trabalhar com parameterização genérica (receber uma union, ou um ponteiro void). Porém, para um sistema de eventos, por exemplo, onde todos os métodos chamados terão a mesma assinatura, este tipo de functor facilita bastante a codificação.

Como sempre, postem suas dúvidas e sugestoes, estarei mais que disponivel para responde-las.

Ponteiros de função

Uma função nada mais é, em sua essência, do que apenas um conjunto de instruções binárias em alguma posição de memória. Sendo assim, então, o C/C++ permite voce criar um ponteiro para uma função, que irá guardar o endereço de memória da função que poderá ser executada. Esse tipo de funcionalidade permite técnicas como de bom exemplo sistema de eventos.

Antes de começar a demonstrar como usar ponteiro para funções, vou deixar avisado que a sintaxe para os definir é um tanto quanto esdruxula, então nao se assustem muito.

Como C (e C++) é uma linguagem fortemente tipada, nenhum dado pode ser de tipo desconhecido para ser utilizado, e funções nao sao exceçao. Alem disso, funções tem o agravante de não apenas terem um tipo de dado de retorno, mas também a possibilidade de receber n parametros. Então, ao declarar um ponteiro para função, necessitamos declarar  seu tipo de retorno e os tipos de todos os parametros. Voce consegue o endereço de uma função usando o nome da função, sem o operador (). O uso do operador & antes do nome da função é, neste caso, opcional. Porém, mais além iremos ver um caso onde o seu uso é obrigatório. Vamos ver alguns exemplos:


// exemplo de função 01
void vazia() {}
// seu ponteiro e atribuiçao
void (*fpVazia)(void) = vazia;

// exemplo de função 02
int numero() { return 2; }
// seu ponteiro e atribuição
int (*fpNumero)(void) = numero;

// exemplo de função 03
int soma(int a, int b) { return a+b; }
// seu ponteiro e atribuição
int (*fpSoma)(int,int) = soma;

Como disse anteriormente, a sintaxe é ligeiramente estranha, mas funciona. Temos aqui tres ponteiros para funções. Agora para executar cada uma dessas, usamos a seguinte sintaxe:


// executa cada uma das funções anteriores
fpVazia();
int num = fpNumero(); // num recebe 2
int res = fpSoma(2,2); // res recebe 4

// outras formas de executar:
(fpVazia)();
(*fpVazia)();

Repare que eu coloquei algumas outras formas de executar no final do código. Embora todas elas funcionem na maioria dos compiladores, alguns compiladores mais antigos podem dar problema com um ou outro formato. Além disso, para executar ponteiros para métodos (funções membros de classe), a sintaxe para chamadas de função vai ficar menos flexivel.

Para facilitar um pouco a sintaxe de uso de ponteiros para função, podemos utilizar o operador typedef, como exemplifico a seguir:


// exemplo de declaração sem typedef
void (*funcExemplo)(int,int) fpExemplo;

// agora usando typedef para criar
// um tipo de dado novo que é um
// ponteiro para função
typedef void (*funcExemplo)(int,int);
// declarando o ponteiro
funcExemplo fpExemplo;

Muito mais simples, não? se vc pensar na questão de passar ponteiros para função como parametro de outra função, esta facilidade de criar um tipo de dado deixa o código muito mais limpo e facil de entender.

Ponteiros para funções são muito úteis, mas voce deve estar pensando “posso usar isso para criar ponteiros de métodos de classes?”. A resposta carrega junto uma explicação:

Um método estático de uma classe é exatamente igual a uma função comum – apenas que o compilador mantém controle sobre as permissões de acesso. Um método não-estático, porém, tem uma diferença bem especial. O formato de chamada ( chamada _thiscall), envia internamente um parametro extra – um ponteiro para o objeto no qual o método está sendo chamado. Disso podemos tirar que, além de sabermos o endereço dessa função, necessitados do ponteiro (ou referência) para o objeto também. Aqui está o exemplo de um ponteiro para um método:


// vamos ver uma classe
class A
{
private:
   int _a;

public:
   A(int a) : _a(a) {}
   ~A() {}

   // método a ser executado
   int soma(int b) { return _a + b; }
};

// criação do tipo ponteiro 'funcSoma'
typedef int (A::*funcSoma)(int);

// criando ponteiro
funcSoma fpSoma = &A::soma;
// nao podemos chamar a função sem um
// objeto do tipo A
A a(2);
A p = new A(3);

// chamando função pelo ponteiro
int res;
res = (a.*fpSoma)(2); // res recebe 4
res = (p->*fpSoma)(2); // res recebe 5

Repare em algumas peculariedades. Primeiramente, inicializamos o ponteiro passando o caminho do método ( &A::soma – diz q soma pertence à classe A. Neste caso, o uso do operador & passa a ser obrigatório). Como disse anteriormente, chama o método sem uma referência a um objeto da classe é impossível, então criamos os objetos ‘a’ e ‘p’ do tipo A. Finalmente, vemos os métodos sendo executados. Repare bem na sintaxe: voce usa o próprio objeto para chamar o método. No caso, dependendo do tipo (se é uma referência ou um ponteiro), usamos um operador diferente. ‘ .* ‘ ou ‘ ->* ‘.

Num outro post, irei descrever a implementação de uma classe genérica (templated) que facilita a manipulação de ponteiro para métodos.