| 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) |
|
|
|
| [ 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! |
|
|
|
| [ 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.
|
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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;
delete p;
| [ 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) |
|
|
|
| [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() |
|
|
|
| [ 16.10 ]
Como eu aloco/desaloco uma matriz de objetos? |
|
| Use p = new
T[n] e 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 ...) |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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:
|
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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) );
}
}
}
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|
| [ 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. |
|
|
|