FAQ Lite
Gerenciamento de Memória

[ 16.1 ] O comando delete p exclui o pointer p ou o dado apontado por *p ?
[ 16.2 ] Eu posso free() pointers alocados com new ? Eu posso delete pointers alocados com malloc() ?
[ 16.3 ] Porque eu devo usar new em lugar do velho e confiável malloc() ?
[ 16.4 ] Eu posso fazer realloc() de pointer alocados via new ?
[ 16.5 ] Eu preciso verificar se NULL após p = new Fred() ?
[ 16.6 ] Como posso convencer meu compilador mais antigo a verificar aumaticamente new para ver se ela retorna NULL ?
[ 16.7 ] Eu preciso verificar NULL antes de delete p ?
[ 16.8 ] Quais são as duas etapas que ocorrem quando eu executo delete p ?
[ 16.9] Em p = new Fred(), a memória de Fred se perde se o construtor Fred acionar uma condição de exceção?
[ 16.10 ] Como eu aloco/desaloco uma matriz de objetos?
[ 16.11 ] O que acontece se eu esqueço o [] ao executar o delete de uma matriz alocada via new T[n] ?
[ 16.12 ] Posso eliminar o [] quando executo delete de uma matriz de elementos do mesmo tipo intrínseco (char, int, etc.)?
[ 16.13 ] Após p = new Fred[n], como o compilador sabe que há n objetos a serem destruidos durante o delete[] p ?
[ 16.14 ] É legal, e moral, uma função membro comandar delete this ?
[ 16.15 ] Como eu aloco matrizes multidimensionais usando new ?
[ 16.16 ] Mas na FAQ anterior o código é cheio de truques e sujeito a erros. Não há um modo mais simples?
[ 16.17 ] Mas a class Matrix acima é específica para Fred. Não um modo mais genérico de se fazer isso?
[ 16.18 ] C++ admite matrizes cujos comprimentos possam ser especificados em tempo de execução?
[ 16.19 ] Como eu posso obrigar que os objetos de minha classe sejam sempre criados via new, e não como objetos static locais ou globais?
[ 16.20 ] Como eu faço uma contagem de referência simples?
[ 16.21 ] Como eu forneço contagem de referência com a semântica copy-on-write?
[ 16.22 ] Como eu forneço contagem de referência com a semântica copy-on-write para toda uma hierarquia de classes?

[ 16.1 ] O comando delete p exclui o pointer p ou o dado apontado por *p ?

O dado apontado.

A palavra chave deveria realmente ser

delete_the_thing_pointed_to_by

O mesmo "abuso da língua inglesa" ocorre quando free a memória apontada por um pointer em C: free(p) realmente significa

free_the_stuff_pointed_to_by(p)

Topo
[ 16.2 ] Eu posso free() pointers alocados com new ? Eu posso delete pointers alocados com malloc() ?

Não!

É perfeitamente legal, moral e salutar usar malloc() e delete no mesmo programa, ou usar new e free() no mesmo programa. Mas é ilegal, imoral e prejudicial clamar free() com um pointer alocado via new, ou chamar delete para um pointer alocado via malloc().

Cuidado: Eu ocasionalmente recebo e-mail de pessoas dizendo que isso funciona bem na máquina X e no compilador Y. Isso não é suficiente para considerar esse método correto. Algumas pessoas dizem "Mas eu estou trabalhando apenas com uma matriz de char." De modo algum misture malloc() e delete para o mesmo pointer, ou new e free() para o mesmo pointer. Se você alocou via p = new char[n], você deve usar delete[] p; você não deve usar free(p). Ou se você alocou via p = malloc(n), você deve usar free(p); você não deve usar delete[] ou delete p. Misturar esses mecanismos pode causar uma falha catastrófica em tempo de execução, se o código for portado para outra máquina, para um novo compilador, ou mesmo para uma nova versão do mesmo compilador.

Você foi avisado!  

Topo
[ 16.3 ] Porque eu devo usar new em lugar do velho e confiável malloc() ?

Construtores/ destrutores, segurança de tipos de dados, capacidade de "override".
  • Construtores/destrutores: ao contrário de malloc(sizeof(Fred)), new Fred() chama o construtor de Fred. De modo semelhante, delete p chama o destrutor de *p.
  • Segurança de tipos de dados: malloc() retorna um void*, o qual não é seguro quanto ao tipo de dados. new Fred() retorna um pointer do tipo correto (um Fred*).
  • Capacidade de override: new é um operator que pode ser sobrepujado por uma classe, enquanto malloc() não é sobrepujável por uma classe.
Topo
[ 16.4 ]   Eu posso fazer realloc() de pointer alocados via new ?

Não!

Quando realloc() tem que copiar a alocação, ele usa uma operação de cópia bitwise, a qual vai dividir alguns objetos C++ em partículas. Objetos C++ devem ser capazes de copiar a si mesmos. Eles usam seus próprios construtores de cópia, ou operadores de atribuição.

Além de disso, o heap que new usa pode não ser o mesmo heap que malloc() e realloc() usam.

Topo
[ 16. 5] Eu preciso verificar se NULL após p = new Fred() ?

Não! Mas se você usa um compilador antigo, você pode ter que obrigar o compilador a fazer essa verificação.

É realmente muito penoso sempre codificar a verificação explícita de NULL, após todas as alocações new. Códigos como o exemplo a seguir são muito tediosos.

Fred* p = new Fred();
    if (p == NULL)
      throw bad_alloc(); 
Se o seu compilador não suporta tratamento de exceções, ou se você se recusa a usá-los, seu código terá que ser bem mais tedioso.
Fred* p = new Fred();
    if (p == NULL) {
      cerr << "Couldn't allocate memory for a Fred" << endl;
      abort();
    } 
Sossegue o seu coração. Em C++, se o sistema de execução não consegue alocar a quantidade de bytes de memória para sizeof(Fred) durante p = new Fred(), uma exceção bad_alloc será ativada. Ao contrário de malloc(), new nunca retorna NULL.

Portanto, você deve escrever simplesmente

Fred* p = new Fred();   // No need to check if p is NULL 
Entretanto, se o seu compilador é antigo, é possível que ele ainda não suporte isso. Descubra verificando a documentação de seu compilador no que se refere a new. Se você tem um compilador antigo, você pode forçar o compilador a ter esse comportamento, ou seja, fazer essa verificação.
Topo
[ 16.6 ] Como posso convencer meu compilador mais antigo a verificar aumaticamente new para ver se ela retorna NULL ?

Eventualmente seu compilador poderá ser convencido.

Se você tem um compilador antigo, que não faz automagicamente o teste de NULL, você pode forçar o sistema de execução a fazer o teste instalando uma função new handler. Sua função new handler pode fazer qualquer coisa que você queira, como imprimir uma mensagem e abort() o programa, delete algum objeto e retornar (nesse caso operator new  vai tentar novamente - retry - a alocação), throw uma exceção, etc.

A seguir uma amostra de new handler que imprime uma mensagem e chama abort(). O handler é instalado usando set_new_handler()

#include <new.h>        // To get set_new_handler
    #include <stdlib.h>     // To get abort()
    #include <iostream.h>   // To get cerr
    
    void myNewHandler()
    {
      // This is your own handler.  It can do anything you want.
      cerr << "Attempt to allocate memory failed!" << endl;
      abort();
    }
    
    main()
    {
      set_new_handler(myNewHandler);   // Install your "new handler"
      // ...
    } 
Após a linha set_new_handler ser executada, operator new vai chamar sua myNewHandler() se e quando não conseguir realizar a alocação da memória. Isso significa que new nunca vai retornar um NULL
Fred* p = new Fred();   // No need to check if p is NULL 
Nota: Por favor, use a abordagem de abort() como último recurso. Se o seu compilador suporta o tratamento de exceções, por favor considere throw uma exceção ao invés de chamar abort()

Nota: Se o construtor de algum objeto estático/global usa new, ele não vai usar a função myNewHandler() já que o construtor será chamado antes de main(). Infelizmente não há um meio conveniente de se garantir que set_new_handler será chamado antes do primeiro uso de new. Por exemplo, mesmo que você ponha a chamada para set_new_handler no construtor de um objeto global, você ainda não sabe se o módulo (unidade de compilação) que contém tal objeto global será implementado primeiro lugar, em último lugar, ou entre outros módulos quaisquer. Portanto, não há garantia de que sua chamada de set_new_handler vai acontecer antes que qualquer outro construtor global ser invocado.

Topo
[ 16.7 ]   Eu preciso verificar NULL antes de delete p ?

Não!

A linguagem C++ garante que delete p não resultará em qualquer ação se p for igual a NULL. Já que você deve ter feito o teste em etapas anteriores do programa, e já que a maioria das metodologias de teste o forçam a testar explicitamente cada ponto de desvio, você não deve colocar um if redundante para fazer esse teste.

Errado:

if (p != NULL)
      delete p; 
Certo:
delete p;
Topo
[ 16.8 ] Quais são as duas etapas que ocorrem quando eu executo delete p ?

delete p é um processo de duas etapas: ele chama o destrutor, e então libera a memória. O código gerado por delete p se parece, em linhas gerais, com o seguinte exemplo (assumindo que p é do tipo Fred*)
// Original code: delete p;
    if (p != NULL) {
      p->~Fred();
      operator delete(p);
    } 
O comando p->~Fred() chama o destrutor para o objeto Fred apontado por p.

O comando operator delete p(void* p) chama a primitiva de desalocação de memória,

void operator delete (void* p).

Essa primitiva é essencialmente similar a

free(void* p)

(Note, entretanto, que elas não são intercambiáveis, ou seja, não há garantia de que as duas primitivas de desalocação de memória usem o mesmo heap)

Topo
[16.9] Em p = new Fred(), a memória de Fred se perde se o construtor Fred acionar uma condição de exceção?

Não.

Se uma condição de exceção ocorre durante o construtor Fred de   p = new Fred(), a linguagem C++ garante que a a quantidade sizeof(Fred) de bytes de memória, que foi alocada, seja  automagicamente liberada e devolvida ao heap.

Aqui estão os detalhes: new Fred() é um processo de duas etapas:

1 - Aloca sizeof(Fred) bytes de memória usando a primitiva

void* operator new (size_t nbytes)

Essa primitiva é, essencialmente, semelhante a

malloc (size_t nbytes)

Note, entretanto que elas não são intercambiáveis, ou seja, não há garantias de qua essa suas primitivas de alocação de memória usem o mesmo heap)

2 - Constrói um objeto na memória alocada, chamando o construtor Fred. O pointer que retorna da primeira etapa é passado como o parâmetro this para o construtor. Essa etapa é contida dentro de um bloco try ... catch, para manejar o caso de uma condição de exceção ser acionada durante essa etapa.

Assim, o código gerado se parece, em linhas gerais, com o seguinte exemplo:

// Original code: Fred* p = new Fred();
    Fred* p = (Fred*) operator new(sizeof(Fred));
    try {
      new(p) Fred();       // Placement new
    } catch (...) {
      operator delete(p);  // Deallocate the memory
      throw;               // Re-throw the exception
    } 
O comando que contém o comentário // Placement new chama o construtor Fred. O pointer p torna-se o this pointer dentro do construtor Fred::Fred()
Topo
[ 16.10 ] Como eu aloco/desaloco uma matriz de objetos?

Use p = new T[n] delete[] p:
Fred* p = new Fred[100];
    // ...
    delete[] p; 
Todas as vezes que você alocar uma matriz de objetos via new (usualmente com o [n] na expressão new) você deve usar [] no comando delete. Essa sintaxe é necessária porque não há diferença sintática entre um pointer para um objeto, e um pointer para uma matriz de objetos (coisas herdadas do C ...)
Topo
[ 16.11 ] O que acontece se eu esqueço o [] ao executar o delete de uma matriz alocada via new T[n] ?

Um final catastrófico.

É responsabilidade do programador - não do compilador - fazer a conexão correta entre new T[n] e delete[] p. Se você fizer essa conexão erradamente, nenhuma mensagem será gerada pelo compilador em tempo de compilação, nem em tempo de execução. O resultado mais provável será corrupção do heap, ou pior, a morte de seu programa.

Topo
[ 16.12 ] Posso eliminar o [] quando executo delete de uma matriz de elementos de um mesmo tipo intrínseco (char, int, etc.)?

Não!

Há vezes em que alguns programadores pensam que o []no delete[] é mera formalidade, e que o compilador vai chamar o destrutor apropriado para todos os elementos da matriz. Por essa razão eles assumem que uma matriz de dados de tipos intrínsecos, como char ou int pode ser delete sem o [], ou seja, eles assumem o seguinte código incorreto:

void userCode(int n)
    {
      char* p = new char[n];
      // ...
      delete p;     // <— ERROR! Should be delete[] p !
    } 
O código acima está errado, e pode causar um desastre em tempo de execução. Em particular, o código que é chamado por

delete p é operator delete(void*),

mas o código que é chamado por

delete[] é operator delete[] (void*).

O comportamento normal para esse último é chamar o padrão, mas é permitido aos usuários substituir esse comportamento por um outro diferente (nesse caso eles deveriam também substituir o correspondente código new em operator new[](size_t)). Se você substituiu o código delete[] e o fez incompatível com código delete, então você chamou o código errado. Isto é, você usou delete p ao invés de delete[] p, e você pode acabar tendo um desastre.

Topo
[ 16.13 ] Após p = new Fred[n], como o compilador sabe que há n objetos a serem destruidos durante o delete[] p ?

Resposta curta: Mágica.

Resposta longa: O sistema de execução armazena o número de objetos, n, em um lugar de onde possa resgatá-lo, conhecendo apenas o pointer p. Há duas técnicas populares para se fazer isso. Ambas as técnicas estão em uso por grande número dos compiladores à venda no mercado, ambas têm prós e contras, e nenhuma delas é perfeita. Essa duas técnicas são:

Topo
[ 16.14 ] É legal, e moral, uma função membro comandar delete this ?

Se você é cuidadoso. esse procedimento é válido para que um objeto cometa suicídio (delete this).

Aqui está como eu defino cuidadoso:

  • Você deve estar absolutamente 100% seguro de que this objeto foi alocado via new (não new[], não via placement new, não é um objeto local na pilha, não é um objeto global, não é um membro de outro objeto; foi alocado puramente com new.
  • Você deve estar absolutamente 100% seguro de que a sua função membro [que comanda delete this] seja a última função invocada nesse objeto.
  • Você deve estar absolutamente 100% seguro de que o restante de sua função membro não toca qualquer parte desse objeto, inclusive não chama outras funções membro, nem toca em qualquer dado membro.
  • Você deve estar absolutamente 100% seguro de que nenhuma outra função sequer toca o pointer this após a linha delete this. Em outras palavras, você não pode examinar esse pointer, compará-lo como outro pointer, verificar se seu conteúdo é NULL, imprimir o conteúdo do pointer, etc., ou seja, você não pode fazer mais nada com ele.

Naturalmente as advertências se aplicam nos casos em que o seu pointer this é um pointer para uma classe base, onde você não tem um destrutor virtual.

Topo
[ 16.15 ] Como eu aloco matrizes multidimensionais usando new ?

Há várias maneiras de se fazer isso, dependendo de quanta flexibilidade você queira na determinação do tamanho da matriz. Em um dos extremos, se você sabe todas as dimensões da matriz em tempo de compilação, você pode alocar matrizes multimensionais estaticamente (como no C):
class Fred { /*...*/ };
    void someFunction(Fred& fred);
    
    void manipulateArray()
    {
      const unsigned nrows = 10;  // Num rows is a compile-time constant
      const unsigned ncols = 20;  // Num columns is a compile-time constant
      Fred matrix[nrows][ncols];
    
      for (unsigned i = 0; i < nrows; ++i) {
        for (unsigned j = 0; j < ncols; ++j) {
          // Here's the way you access the (i,j) element:
          someFunction( matrix[i][j] );
    
          // You can safely "return" without any special delete code:
          if (today == "Tuesday" && moon.isFull())
            return;     // Quit early on Tuesdays when the moon is full
        }
      }
          // No explicit delete code at the end of the function either
    } 
Mais comumente, o tamanho da matriz é determinado em tempo de execução, mas você sabe, em tempo de compilação, que ela será retangular. Nesse caso você precisa usar o heap (memória livre), mas poderá alocar todos os elementos em uma mesma parte da memória livre.
void manipulateArray(unsigned nrows, unsigned ncols)
    {
      Fred* matrix = new Fred[nrows * ncols];
    
      // Since we used a simple pointer above, we need to be VERY
      // careful to avoid skipping over the delete code.
      // That's why we catch all exceptions:
      try {
    
        // Here's how to access the (i,j) element:
        for (unsigned i = 0; i < nrows; ++i) {
          for (unsigned j = 0; j < ncols; ++j) {
            someFunction( matrix[i*ncols + j] );
          }
        }
    
        // If you want to quit early on Tuesdays when the moon is full,
        // make sure to do the delete along ALL return paths:
        if (today == "Tuesday" && moon.isFull()) {
          delete[] matrix;
          return;
        }
    
        // ...
      }
      catch (...) {
        // Make sure to do the delete when an exception is thrown:
        delete[] matrix;
        throw;    // Re-throw the current exception
      }
          // Make sure to do the delete at the end of the function too:
      delete[] matrix;
    } 
Finalmente, no outro extremo, você pode nem mesmo saber, garantidamente, que a matriz será retangular. Por exemplo, se cada linha puder ter um tamanho diferente, você vai precisar alocar cada linha individualmente. Na função seguinte, ncols[i] é o número de colunas na linha i, onde i varia entre 0 e nrows-1, inclusive
void manipulateArray(unsigned nrows, unsigned ncols[])
    {
      Fred** matrix = new Fred*[nrows];
      for (unsigned i = 0; i < nrows; ++i)
        matrix[i] = new Fred[ ncols[i] ];
    
      // Since we used a simple pointer above, we need to be VERY
      // careful to avoid skipping over the delete code.
      // That's why we catch all exceptions:
      try {
    
        // Here's how to access the (i,j) element:
        for (unsigned i = 0; i < nrows; ++i) {
          for (unsigned j = 0; j < ncols[i]; ++j) {
            someFunction( matrix[i][j] );
          }
        }
    
        // If you want to quit early on Tuesdays when the moon is full,
        // make sure to do the delete along ALL return paths:
        if (today == "Tuesday" && moon.isFull()) {
          for (unsigned i = nrows; i > 0; --i)
            delete[] matrix[i-1];
          delete[] matrix;
          return;
        }
    
        // ...
      }
      catch (...) {
        // Make sure to do the delete when an exception is thrown:
        for (unsigned i = nrows; i > 0; --i)
          delete[] matrix[i-1];
        delete[] matrix;
        throw;    // Re-throw the current exception
      }
    
      // Make sure to do the delete at the end of the function too.
      // Note that deletion is the opposite order of allocation:
      for (i = nrows; i > 0; --i)
        delete[] matrix[i-1];
      delete[] matrix;
    } 
Note o uso interessante de matrix[i-1] no processo de exclusão da matriz. Isso previne o wrap-around de valores unsigned quando i assume um valor menor que zero.

Finalmente, note que pointers e matrizes são miseráveis. Normalmente é preferível encapsular seus pointers em uma classe que tenha uma interface simples e segura. A FAQ seguinte mostra como se faz isso.

Topo
[ 16.16 ] Mas na FAQ anterior o código é cheio de truques e sujeito a erros. Não há um modo mais simples?

Sim.

O que leva o código da FAQ anterior a usar tantos truques, e ser sujeito a erros, é que ele usa pointers, e nós sabemos que pointers e matrizes são miseráveis. A solução é encapsular seus pointers em uma classe que tenha uma interface simples e segura. Por exemplo, nós podemos definir a classe Matrix que controla uma matriz retangular, e aí o nosso código será grandemente simplificado, se comparado ao código da matriz retangular da FAQ anterior.

// The code for class Matrix is shown below...
    void someFunction(Fred& fred);
    
    void manipulateArray(unsigned nrows, unsigned ncols)
    {
      Matrix matrix(nrows, ncols);   // Construct a Matrix called matrix
    
      for (unsigned i = 0; i < nrows; ++i) {
        for (unsigned j = 0; j < ncols; ++j) {
          // Here's the way you access the (i,j) element:
          someFunction( matrix(i,j) );
    
          // You can safely "return" without any special delete code:
          if (today == "Tuesday" && moon.isFull())
            return;     // Quit early on Tuesdays when the moon is full
        }
      }
      // No explicit delete code at the end of the function either
    } 
O principal aspecto a notar é a ausência da codificação para limpeza (clean up). Por exemplo, não há qualquer comando delete no código acima, mais ainda, não há perda de memória alocada, assumindo que o destrutor de Matrix fará esse trabalho corretamente.

Aqui está o código para Matrix que torna possível o que se mencionou acima:

class Matrix {
    public:
      Matrix(unsigned nrows, unsigned ncols);
      // Throws a BadSize object if either size is zero
      class BadSize { };
    
      // Based on the Law Of The Big Three:
     ~Matrix();
      Matrix(const Matrix& m);
      Matrix& operator= (const Matrix& m);
    
      // Access methods to get the (i,j) element:
      Fred&       operator() (unsigned i, unsigned j);
      const Fred& operator() (unsigned i, unsigned j) const;
      // These throw a BoundsViolation object if i or j is too big
      class BoundsViolation { };
    
    private:
      Fred* data_;
      unsigned nrows_, ncols_;
    };
    
    inline Fred& Matrix::operator() (unsigned row, unsigned col)
    {
      if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
      return data_[row*ncols_ + col];
    }
    
    inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
    {
      if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
      return data_[row*ncols_ + col];
    }
    
    Matrix::Matrix(unsigned nrows, unsigned ncols)
      : data_  (new Fred[nrows * ncols]),
        nrows_ (nrows),
        ncols_ (ncols)
    {
      if (nrows == 0 || ncols == 0)
        throw BadSize();
    }
    
    Matrix::~Matrix()
    {
      delete[] data_;
    } 
Note que o a classe Matrix acima realiza duas coisas: move alguns códigos trucados de gerenciamento de memória do código do usuário (ou seja, de main()) para a própria classe, e isso reduz o tamanho global do programa (por exemplo, assumindo que Matrix é facilmente reusavel, mover a complexidade dos usuários de Matrix para dentro de Matrix é equivalente a mover a complexidade de muitos para poucos.
Topo
[ 16.17 ] Mas a class Matrix acima é específica para Fred. Não há  um modo mais genérico de se fazer isso?

Sim. Use templates:
template<class T>  // See section on templates for more
    class Matrix {
    public:
      Matrix(unsigned nrows, unsigned ncols);
      // Throws a BadSize object if either size is zero
      class BadSize { };
    
      // Based on the Law Of The Big Three:
     ~Matrix();
      Matrix(const Matrix<T>& m);
      Matrix<T>& operator= (const Matrix<T>& m);
    
      // Access methods to get the (i,j) element:
      T&       operator() (unsigned i, unsigned j);
      const T& operator() (unsigned i, unsigned j) const;
      // These throw a BoundsViolation object if i or j is too big
      class BoundsViolation { };
    
    private:
      T* data_;
      unsigned nrows_, ncols_;
    };
    
    template<class T>
    inline T& Matrix<T>::operator() (unsigned row, unsigned col)
    {
      if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
      return data_[row*ncols_ + col];
    }
    
template<class T>
    inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
    {
      if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
      return data_[row*ncols_ + col];
    }
    
    template<class T>
    inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
      : data_  (new T[nrows * ncols]),
        nrows_ (nrows),
        ncols_ (ncols)
    {
      if (nrows == 0 || ncols == 0)
        throw BadSize();
    }

    template<class T>
    inline Matrix<T>::~Matrix()
    {
      delete[] data_;
    }         
Aqui está uma maneira de se usar essa template:
#include "Fred.hpp"     // To get the definition for class Fred
    
    void doSomethingWith(Fred& fred);
    
    void sample(unsigned nrows, unsigned ncols)
    {
      Matrix<Fred> matrix(nrows, ncols);   // Construct a Matrix<Fred> called matrix
    
      for (unsigned i = 0; i < nrows; ++i) {
        for (unsigned j = 0; j < ncols; ++j) {
          doSomethingWith( matrix(i,j) );
        }
      }
    } 
Topo
[ 16.18 ] C++ admite matrizes cujos comprimentos possam ser especificados em tempo de execução?

Sim, considerando que STL tem uma template vector que provê essa facilidade.

Não, considerando que matrizes de tipos intrínsecos precisam ter seu tamanho especificado em tempo de compilação.

Sim, considerando que mesmo matrizes de tipos intrínsecos podem especificar o primeiro índice de limite em tempo de execução. Por exemplo, comparando com o que se apresenta na FAQ anterior, se você precisa que a primeira dimensão da matriz varie, você pode solicitar new para uma matriz de matrizes, ao invés de uma matriz de pointers.

const unsigned ncols = 100;           // ncols = number of columns in the array
    
    class Fred { /*...*/ };
    
    void manipulateArray(unsigned nrows)  // nrows = number of rows in the array
    {
      Fred (*matrix)[ncols] = new Fred[nrows][ncols];
      // ...
      delete[] matrix;
    } 
Você não pode fazer isso se precisa que qualquer outra coisa, que não a primeira dimensão da matriz, seja alterada em tempo de execução

Mas por favor, não use matrizes a menos que você seja obrigado. Matrizes são miseráveis. Use um objeto de uma classe, se você puder. Use matrizes apenas quando não tiver outra opção.

Topo
[ 16.19 ] Como eu posso obrigar que os objetos de minha classe sejam sempre criados via new, e não como objetos static locais ou globais?

Use Named Constructor Idiom.

Como é usual com o Named Constructor Idiom, os construtores são todos private: ou protected:, e há um ou mais métodos públicos public static create() (os denominados Named Constructors), um por construtor. Nesse caso o método create() aloca o objeto via new. Uma vez que os próprios construtores não são públicos, não resta outra forma de criar objetos da classe.

    class Fred {
    public:
      // The create() methods are the "named constructors":
      static Fred* create()                 { return new Fred();     }
      static Fred* create(int i)            { return new Fred(i);    }
      static Fred* create(const Fred& fred) { return new Fred(fred); }
      // ...
    
    private:
      // The constructors themselves are private or protected:
      Fred();
      Fred(int i);
      Fred(const Fred& fred);
      // ...
    }; 
Agora o único meio de criar objetos Fred é via Fred::create():

    main()
    {
      Fred* p = Fred::create(5);
      
// ...
      delete p;
    }

Certifique-se de que o seus construtores estejam na seção protected: se você admite que Fred tenha classes derivadas.

Note também que você pode fazer uma outra classe Vilma uma friend de Fred, se você deseja permitir que Vilma tenha objetos membro da classe Fred. Mas é claro que isso já seria um abrandamento do objetivo original, explicitamente forçar que objetos Fred sejam alocados via new.

Topo
[ 16.20 ] Como eu faço uma contagem de referência simples?

Se tudo o que você quer é habilidade de passar por um punhado de pointers para o mesmo objeto, com a facilidade de que o o objeto seja automagicamente delete quando o último pointer para ele desaparecer, você pode usar algo como a seguinte classe smart pointer:
    // Fred.h
    
    class FredPtr;
    
    class Fred {
    public:
      Fred() : count_(0) /*...*/ { }  // All ctors set count_ to 0 !
      // ...
    private:
      friend FredPtr;     // A friend class
      unsigned count_;
      // count_ must be initialized to 0 by all constructors
      // count_ is the number of FredPtr objects that point at this
    };
    
    class FredPtr {
    public:
      Fred* operator-> () { return p_; }
      Fred& operator* ()  { return *p_; }
      FredPtr(Fred* p)    : p_(p) { ++p_->count_; }  // p must not be NULL
     ~FredPtr()           { if (--p_->count_ == 0) delete p_; }
      FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
      FredPtr& operator= (const FredPtr& p)
            { // DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
              // (This order properly handles self-assignment)
              ++p.p_->count_;
              if (--p_->count_ == 0) delete p_;
              p_ = p.p_;
              return *this;
            }
    private:
      Fred* p_;    // p_ is never NULL
    }; 
Naturalmente você pode usar classes aninhadas para renomear FredPtr para Fred::Ptr.

Note que você pode abrandar a regra nunca NULL acima com um pouco mais de verificação no construtor, construtor de cópia, operador de atribuição e no destrutor. Se você desejar isso, você poderá colocar uma verificação p_ != NULL dentro dos operadores "*" e "->" (no mínimo como um assert()). Eu gostaria de não recomendar o método operator Fred*(), uma vez que ele levar as pessoas a acidentalmente obter o Fred*.

Uma das limitantes implícitas de FredPtr é que ele pode apontar unicamente objetos Fred que tenham sido alocados via new. Se você quiser estar realmente seguro, você pode reforçar essa limitante fazendo todos os construtores de Fred private, e para cada construtor ter um método público

public (static) create()

que aloca o objeto Fred via new e retorna um FredPtr e não Fred*. Dessa forma, a única maneira de se criar objetos Fred é obter um FredPtr ("Fred* p = new Fred()" seria substituido por  "FredPtr p = Fred::create()"). Assim ninguém poderia subverter acidentalmente o mecanismo de contagem de referência.

Por exemplo, se Fred tem um Fred::Fred() e um Fred::Fred(int i, int j), as alterações para class Fred poderiam ser

    class Fred {
    public:
      static FredPtr create()             { return new Fred(); }
      static FredPtr create(int i, int j) { return new Fred(i,j); }
      // ...
    private:
      Fred();
      Fred(int i, int j);
      // ...
    }; 
O resultado final é que você agora tem um modo de usar contagem de referência simples para prover pointer semantics para um dado objeto. Usuários de sua class Fred usam explicitamente FredPtr, o qual atua mais ou menos como pointers Fred*. O benefício é que os usuários podem fazer várias cópias de seus objetos FredPtr smart pointer, e o objeto Fred apontado será automagicamente delete quando o último objeto FredPtr desaparecer.

Se você preferiu dar aos seus usuários reference semantics ao invés de pointers semantics, você pode usar contagem de referência para prover copy on write.

Topo
[ 16.21 ] Como eu forneço contagem de referência com a semântica copy-on-write?

A FAQ anterior apresentou um esquema simples de contagem de referência que fornece ao usuário a semântica de pointers. Esse FAQ descreve uma abordagem que fornece ao usuário a semântica de referência.

A idéia básica é permitir ao usuário pensar que está copiando seus objetos Fred, mas na realidade essa implementação não faz qualquer cópia do objeto atual, a menos e até que o usuário tente fazer alguma modificação no objeto Fred atual.

Fred::Data aloja todos os dados que normalmente iriam para dentro de Fred class. Fred::Data tem também um dado membro extra, count_ , para gerenciar a contagem de referência. A classe Fred é então, em última análise, um smart reference que (internamente) aponta para Fred::Data

    class Fred {
    public:
    
      Fred();                               // A default constructor
      Fred(int i, int j);                   // A normal constructor
    
      Fred(const Fred& f);
      Fred& operator= (const Fred& f);
     ~Fred();
    
      void sampleInspectorMethod() const;   // No changes to this object
      void sampleMutatorMethod();           // Change this object
    
      // ...
    
    private:
    
      class Data {
      public:
        Data();
        Data(int i, int j);
        Data(const Data& d);
    
        // Since only Fred can access a Fred::Data object,
        // you can make Fred::Data's data public if you want.
        // But if that makes you uncomfortable, make the data private
        // and make Fred a friend class via friend Fred;
        // ...
    
        unsigned count_;
        // count_ is the number of Fred objects that point at this
        // count_ must be initialized to 1 by all constructors
        // (it starts as 1 since it is pointed to by the Fred object that created it)
      };
    
      Data* data_;
    };
    
    Fred::Data::Data()              : count_(1) /*init other data*/ { }
    Fred::Data::Data(int i, int j)  : count_(1) /*init other data*/ { }
    Fred::Data::Data(const Data& d) : count_(1) /*init other data*/ { }
    
    Fred::Fred()             : data_(new Data()) { }
    Fred::Fred(int i, int j) : data_(new Data(i, j)) { }
    
    Fred::Fred(const Fred& f)
      : data_(f.data_)
    {
      ++ data_->count_;
    }
    
    Fred& Fred::operator= (const Fred& f)
    {
      // DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
      // (This order properly handles self-assignment)
      ++ f.data_->count_;
      if (--data_->count_ == 0) delete data_;
      data_ = f.data_;
      return *this;
    }
    
    Fred::~Fred()
    {
      if (--data_->count_ == 0) delete data_;
    }
    
    void Fred::sampleInspectorMethod() const
    {
      // This method promises ("const") not to change anything in *data_
      // Other than that, any data access would simply use "data_->..."
    }
    
    void Fred::sampleMutatorMethod()
    {
      // This method might need to change things in *data_
      // Thus it first checks if this is the only pointer to *data_
      if (data_->count_ > 1) {
        Data* d = new Data(*data_);    // Invoke Fred::Data's copy ctor
        -- data_->count_;
        data_ = d;
      }
      assert(data_->count_ == 1);
    
      // Now the method proceeds to access "data_->..." as normal
    } 
Ao usar essa maneira de chamar o construtor default de Fred, você pode evitar todas aquelas chamadas de new, por compartilhar um objeto Fred::Data comum a todos os objetos Fred que são construídos via Fred::Fred(). Para evitar os problemas de ordenação de inicialização static, esse objeto Fred::Data compartilhado é criado dentro de uma função on first use. Aqui estão as alterações que deveriam ser feitas no código acima (note que o destrutor do objeto compartilhado Fred::Data nunca é invocado.
    class Fred {
    public:
      // ...
    private:
      // ...
      static Data* defaultData();
    };
    
    Fred::Fred()
    : data_(defaultData())
    {
      ++ data_->count_;
    }
    
    Fred::Data* Fred::defaultData()
    {
      static Data* p = NULL;
      if (p == NULL) {
        p = new Data();
        ++ p->count_;    // Make sure it never goes to zero
      }
      return p;
    } 
Nota: Voce pode ainda fornecer contagem de referência para uma hierarquia de classes, se sua classe Fred puder ser uma classe base.
Topo
[ 16.22 ] Como eu forneço contagem de referência com a semântica copy-on-write para toda uma hierarquia de classes?

A FAQ anterior apresentou um esquema de contagem de referência que fornece ao usuário a semântica de referências, aplicável a uma única classe, ao invés de a uma hierarquia de classes. Esta FAQ extende essa técnica para permitir aplicá-la a uma hierarquia de classes. A diferença básica é que agora Fred::Data é a raiz de uma hierarquia de classes, o que o leva a ter algumas funções virtual. Note que a própria classe Fred não tem qualquer função virtual.

O Virtual Constructor Idiom é usado para fazer copias dos objetos Fred:Data. Para selecionar a classe derivada a criar, o código de exemplo abaixo usa o Named Constructor Idiom, Mas há também outras técnicas aplicáveis (um comando switch no construtor, etc). O código de exemplo assume duas classes derivadas: Der1 e Der2. Os métodos nas classes derivadas desconhecem a contagem de referência.

    class Fred {
    public:
    
      static Fred create1(String s, int i);
      static Fred create2(float x, float y);
    
      Fred(const Fred& f);
      Fred& operator= (const Fred& f);
     ~Fred();
    
      void sampleInspectorMethod() const;   // No changes to this object
      void sampleMutatorMethod();           // Change this object
    
      // ...
    
    private:
    
      class Data {
      public:
        Data() : count_(1) { }
        Data(const Data& d) : count_(1) { }              // Do NOT copy the 'count_' member!
        Data& operator= (const Data&) { return *this; }  // Do NOT copy the 'count_' member!
        virtual ~Data() { assert(count_ == 0); }         // A virtual destructor
        virtual Data* clone() const = 0;                 // A virtual constructor
        virtual void sampleInspectorMethod() const = 0;  // A pure virtual function
        virtual void sampleMutatorMethod() = 0;
      private:
        unsigned count_;   // count_ doesn't need to be protected
      };
    
      class Der1 : public Data {
      public:
        Der1(String s, int i);
        virtual void sampleInspectorMethod() const;
        virtual void sampleMutatorMethod();
        virtual Data* clone() const;
        // ...
      };
    
      class Der2 : public Data {
      public:
        Der2(float x, float y);
        virtual void sampleInspectorMethod() const;
        virtual void sampleMutatorMethod();
        virtual Data* clone() const;
        // ...
      };
    
      Fred(Data* data);
      // Creates a Fred smart-reference that owns *data
      // It is private to force users to use a createXXX() method
      // Requirement: data must not be NULL
    
      Data* data_;   // Invariant: data_ is never NULL
    };
    
    Fred::Fred(Data* data) : data_(data)  { assert(data != NULL); }
    
    Fred Fred::create1(String s, int i)   { return Fred(new Der1(s, i)); }
    Fred Fred::create2(float x, float y)  { return Fred(new Der2(x, y)); }
    
    Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
    Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }
    
    Fred::Fred(const Fred& f)
      : data_(f.data_)
    {
      ++ data_->count_;
    }
    
    Fred& Fred::operator= (const Fred& f)
    {
      // DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
      // (This order properly handles self-assignment)
      ++ f.data_->count_;
      if (--data_->count_ == 0) delete data_;
      data_ = f.data_;
      return *this;
    }
    
    Fred::~Fred()
    {
      if (--data_->count_ == 0) delete data_;
    }
    
    void Fred::sampleInspectorMethod() const
    {
      // This method promises ("const") not to change anything in *data_
      // Therefore we simply "pass the method through" to *data_:
      data_->sampleInspectorMethod();
    }
    
    void Fred::sampleMutatorMethod()
    {
      // This method might need to change things in *data_
      // Thus it first checks if this is the only pointer to *data_
      if (data_->count_ > 1) {
        Data* d = data_->clone();   // The Virtual Constructor Idiom
        -- data_->count_;
        data_ = d;
      }
      assert(data_->count_ == 1);
    
      // Now we "pass the method through" to *data_:
      data_->sampleInspectorMethod();
    } 
Naturalmente os construtores e os métodos sampleXXX para Fred::Der1 e Fred::Der2 terão que ser implementados da maneira apropriada.
Topo Anterior Próximo Índice
C++ FAQ Lite
Copyright © 1991-98 by Marshall Cline Ph.D., cline@parashift.com
Tradução: Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |