O código a seguir causa um vazamento de memória:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
Não fazia muito sentido para mim, pois a classe derivada não aloca memória dinâmica bruta e o unique_ptr se desaloca. Entendo que o destruidor implícito dessa base de classe está sendo chamado em vez de derivado, mas não entendo por que isso é um problema aqui. Se eu escrevesse um destruidor explícito para derivado, não escreveria nada para vec.
c++
inheritance
memory
allocation
Ignorância inercial
fonte
fonte
~derived()
que delega ao destruidor do vec. Como alternativa, você está assumindo queunique_ptr<base> pt
conheceria o destruidor derivado. Sem um método virtual, este não pode ser o caso. Enquanto um unique_ptr pode receber uma função de exclusão que é um parâmetro de modelo sem nenhuma representação de tempo de execução, e esse recurso não serve para esse código.Respostas:
Quando o compilador executa o implícito
delete _ptr;
dentro dounique_ptr
destruidor do (onde_ptr
está o ponteiro armazenado nounique_ptr
), ele sabe exatamente duas coisas:_ptr
é. Como o ponteiro está dentrounique_ptr<base>
, isso significa_ptr
é do tipobase*
.Isso é tudo o que o compilador sabe. Portanto, como está excluindo um objeto do tipo
base
, ele será chamado~base()
.Então ... onde está a parte em que destrói o
dervied
objeto para o qual realmente aponta? Porque se o compilador não sabe que está destruindo aderived
, ele não sabe quederived::vec
existe , e muito menos que deve ser destruído. Então você quebrou o objeto deixando metade dele sem ser destruído.O compilador não pode assumir que qualquer
base*
ser destruído é realmente umderived*
; afinal, pode haver qualquer número de classes derivadasbase
. Como ele saberia para qual tipo esse particularbase*
realmente aponta?O que o compilador precisa fazer é descobrir o destruidor correto a ser chamado (sim,
derived
tem um destruidor. A menos que você seja= delete
um destruidor, toda classe tem um destruidor, independentemente de você escrever um ou não). Para fazer isso, ele precisará usar algumas informações armazenadasbase
para obter o endereço correto do código destruidor a ser invocado, informações definidas pelo construtor da classe real. Em seguida, é necessário usar essas informações para converter obase*
ponteiro em um endereço daderived
classe correspondente (que pode ou não estar em um endereço diferente. Sim, na verdade). E então pode invocar esse destruidor.Esse mecanismo que acabei de descrever? É comumente chamado de "despacho virtual": ou seja, aquilo que acontece sempre que você chama uma função marcada
virtual
quando você tem um ponteiro / referência a uma classe base.Se você deseja chamar uma função de classe derivada quando tudo que você tem é um ponteiro / referência de classe base, essa função deve ser declarada
virtual
. Destruidores não são fundamentalmente diferentes a esse respeito.fonte
Herança
O ponto principal da herança é compartilhar uma interface e protocolo comuns entre muitas implementações diferentes, de modo que uma instância de uma classe derivada possa ser tratada de forma idêntica a qualquer outra instância de qualquer outro tipo derivado.
No C ++, a herança também traz detalhes de implementação, marcando (ou não marcando) o destruidor como virtual é um desses detalhes de implementação.
Função Vinculação
Agora, quando uma função, ou qualquer um de seus casos especiais, como um construtor ou destruidor, é chamada, o compilador deve escolher qual implementação de função se destina. Em seguida, ele deve gerar código de máquina que siga essa intenção.
A maneira mais simples de trabalhar isso seria selecionar a função em tempo de compilação e emitir código de máquina suficiente para que, independentemente de quaisquer valores, quando esse trecho de código for executado, ele sempre execute o código da função. Isso funciona muito bem, exceto pela herança.
Se tivermos uma classe base com uma função (poderia ser qualquer função, incluindo o construtor ou destruidor) e seu código chamar uma função, o que isso significa?
Tomando como exemplo, se você chamou
initialize_vector()
o compilador, deve decidir se realmente deseja chamar a implementação encontrada emBase
, ou a implementação encontrada emDerived
. Existem duas maneiras de decidir isso:Base
tipo, você quis dizer a implementaçãoBase
.Base
valor digitado pode serBase
, ouDerived
que a decisão sobre qual chamada será feita, deve ser tomada no tempo de execução quando chamada (sempre que é chamada).O compilador neste momento está confuso, ambas as opções são igualmente válidas. É quando
virtual
entra em cena. Quando essa palavra-chave está presente, o compilador escolhe a opção 2, atrasando a decisão entre todas as implementações possíveis até que o código seja executado com um valor real. Quando essa palavra-chave está ausente, o compilador escolhe a opção 1 porque esse é o comportamento normal.O compilador ainda pode escolher a opção 1 no caso de uma chamada de função virtual. Mas somente se puder provar que esse é sempre o caso.
Construtores e Destrutores
Então, por que não especificamos um construtor virtual?
Mais intuitivamente, como o compilador escolheria entre implementações idênticas do construtor para
Derived
eDerived2
? Isso é bem simples, não pode. Não há valor preexistente a partir do qual o compilador possa aprender o que realmente era pretendido. Não há valor pré-existente porque esse é o trabalho do construtor.Então, por que precisamos especificar um destruidor virtual?
Mais intuitivamente, como o compilador escolheria entre implementações para
Base
eDerived
? São apenas chamadas de função, portanto, o comportamento da chamada de função acontece. Sem um destruidor virtual declarado, o compilador decidirá ligar diretamente aoBase
destruidor, independentemente do tipo de tempo de execução dos valores.Em muitos compiladores, se o derivado não declarar nenhum membro de dados nem herdar de outros tipos, o comportamento no
~Base()
será adequado, mas não é garantido. Funcionaria puramente por acaso, como estar diante de um lança-chamas que ainda não havia sido aceso. Você está bem por um tempo.A única maneira correta de declarar qualquer tipo de base ou interface em C ++ é declarar um destruidor virtual, para que o destruidor correto seja chamado para qualquer instância específica da hierarquia de tipos desse tipo. Isso permite que a função com mais conhecimento da instância limpe essa instância corretamente.
fonte