O recurso está a ser carregado... Carregamento...

Recuperação da memória C++ de pontos de conhecimento de estratégias de programação C++

Autora:Inventor quantificado - sonho pequeno, Criado: 2017-12-29 11:00:02, Atualizado: 2017-12-29 11:25:11

Recuperação da memória C++ de pontos de conhecimento de estratégias de programação C++

Antes de escrever uma estratégia de C++, é necessário ter conhecimentos básicos e, pelo menos, essas regras. A seguir estão as informações para reproduzir:

  • C++ Memory Object Battle

Se alguém se diz programador e não sabe nada sobre memória, então eu posso dizer-lhe que ele deve estar se gabar. Escrever um programa em C ou C++ requer mais atenção à memória, não só porque a distribuição racional da memória afeta diretamente a eficiência e o desempenho do programa, mas também porque, principalmente, quando operamos com a memória, surgem problemas que muitas vezes não são facilmente detectáveis, como fugas de memória, como por exemplo, ponteiros pendurados.

Sabemos que o C++ divide a memória em três áreas lógicas: pilha, hexadecimal e área de armazenamento estático. Assim, os objetos que estão dentro delas são chamados de pilha, hexadecimal e estático. Então, qual é a diferença entre esses diferentes objetos de memória?

  • 1 Conceitos básicos

    Para começar, veja o........................................

    Type stack_object ; 
    

    O objecto stack_object é um objeto em que a vida começa no ponto de definição e termina quando a função em que ele se encontra é devolvida.

    Além disso, quase todos os objetos temporários são objetos de escala. Por exemplo, a definição da função abaixo:

    Type fun(Type object);
    

    Esta função produz pelo menos dois objetos temporários, primeiro, os parâmetros são transmitidos por valor, então a função de construção de cópias é chamada para gerar um objeto temporário object_copy1, que é usado dentro da função não por object_copy1, mas por object_copy1, naturalmente, object_copy1 é um objeto de parede, que é liberado quando a função retorna; e também esta função é um valor de retorno, quando a função retorna, então também produz um objeto temporário object_copy2, que é liberado por algum tempo após o retorno da função. Por exemplo, uma função tem o código:

    Type tt ,result ; //生成两个栈对象
    
    tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
    

    A execução da segunda frase acima é a seguinte: primeiro, quando a função fun retorna, um objeto provisório object_copy2 é gerado e depois o operador de atribuição é executado.

    tt = object_copy2 ; //调用赋值运算符
    

    Você vê? O compilador gera tantos objetos temporários para nós sem a nossa percepção, e o custo de tempo e espaço para gerar esses objetos temporários pode ser grande, então, você pode entender por que é melhor passar os parâmetros da função por referência de const em vez de por valor.

    Em seguida, veja a pilha. A pilha, também chamada de área de armazenamento livre, é dinâmicamente distribuída durante a execução do programa, então sua maior característica é a dinâmica. No C++, todos os objetos da pilha são criados e destruídos pelo programador, então, se não forem bem tratados, problemas de memória ocorrem.

    Então, como é que os objetos de pilha são distribuídos em C++? A única maneira de fazer isso é usar new (obviamente, o comando malloc também pode ser usado para obter memória de pilha de tipo C), o que significa que basta usar new para distribuir um pedaço de memória na pilha e retornar o ponteiro que aponta para esse objeto.

    Volte a olhar para o repositório estático. Todos os objetos estáticos e globais são distribuídos no repositório estático. Em relação aos objetos globais, eles são distribuídos antes da execução da função main. Na verdade, antes de executar o código de exibição na função main, uma função _main))) gerada pelo compilador é chamada, enquanto a função _main))) faz o trabalho de construção e inicialização de todos os objetos globais.

    void main(void)
    {
        ... // 显式代码
    }
    
    // 实际上转化为这样:
    
    void main(void)
    {
        _main(); //隐式代码,由编译器产生,用以构造所有全局对象
        ...      // 显式代码
        ...
        exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
    }
    

    Então, sabendo isso, podemos derivar algumas dicas, como, por exemplo, assumir que vamos fazer algum trabalho de preparação antes da execução da função main(), então podemos escrever esses trabalhos de preparação em uma função de construção de um objeto global definido, de modo que, antes da execução do código expresso da função main(), a função de construção desse objeto global seja chamada e execute as ações esperadas, atingindo nosso objetivo.

    Há também um objeto estático, que é um membro estático de uma classe. Considerando isso, alguns problemas mais complexos surgem.

    O primeiro problema é a vida útil dos objetos membros estáticos da classe, que surgem com a criação do primeiro objeto da classe e morrem no final do programa. Ou seja, existe uma situação em que, no programa, definimos uma classe que tem um objeto estático como membro, mas durante a execução do programa, se não criamos nenhum objeto da classe, não produzimos o objeto estático que a classe contém.

    A segunda questão é: quando ocorrem as seguintes coisas:

    class Base
    {
    public:
        static Type s_object ;
    }
    
    class Derived1 : public Base / / 公共继承
    {
    
        ... // other data 
    
    }
    
    class Derived2 : public Base / / 公共继承
    {
    
        ... // other data 
    
    }
    
    Base example ;
    
    Derivde1 example1 ;
    
    Derivde2 example2 ;
    
    example.s_object = …… ;
    
    example1.s_object = …… ; 
    
    example2.s_object = …… ; 
    

    Observe que as três declarações acima marcadas como negros, são os mesmos objetos que são visitados por s_object? A resposta é sim, eles realmente apontam para o mesmo objeto, o que não parece ser verdade, não é? Mas é verdade, você pode escrever um simples código para verificar por si mesmo.

    Imagine que um corte ocorre quando passamos um objeto do tipo Derived1 para uma função que aceita um parâmetro do tipo Base que não é referenciado, então como é o corte? Acredite que agora você já sabe, é simplesmente tirar o objeto do tipo Derived1, ignorando todos os outros membros de dados que o tipo Derived1 define, e passar esse objeto para a função ((na verdade, a função usa uma cópia do objeto do tipo Derived)).

    Todos os objetos de classes derivadas da classe Base que herdam contêm um subobjeto do tipo Base (que é o ponto onde o ponteiro do tipo Base pode apontar para um objeto Derived1 e é, naturalmente, o ponto polêmico), enquanto todos os subobjetos e todos os objetos do tipo Base compartilham o mesmo objeto s_object, naturalmente, todos os exemplos de classes do sistema de herança completo derivado da classe Base compartilham o mesmo objeto s_object. O layout de objetos do exemplo 1 e do exemplo 2 mencionado acima é o seguinte:

  • 2 Comparação de três objetos de memória

    A vantagem dos objetos de barras é que eles são gerados automaticamente no momento certo e destruídos automaticamente no momento certo, sem que o programador tenha que se preocupar; além disso, os objetos de barras geralmente são criados mais rápido do que os objetos de pilha, porque quando os objetos de pilha são distribuídos, a operação operator new é chamada, e o operador new adota algum algoritmo de busca de espaço em memória, o que pode ser muito demorado. A criação de objetos de barras não é tão problemática, basta mover o ponteiro do topo.

    Objetos de pilha, cujo momento de criação e de destruição são definidos pelo programador, ou seja, o programador tem total controle sobre a vida dos objetos de pilha. Muitas vezes precisamos de objetos como esse, por exemplo, precisamos criar um objeto que possa ser acessado por várias funções, mas não queremos torná-lo global, então é uma boa opção criar um objeto de pilha e depois passar o ponteiro do objeto de pilha entre as funções para que o objeto seja compartilhado. Além disso, o volume da pilha é muito maior em comparação com o espaço de pilha.

    Agora vamos ver objetos estáticos.

    Em geral, não existem objetos globais em uma linguagem totalmente orientada a objetos, como o C#, porque objetos globais significam insegurança e alta aderência, e usar objetos globais em excesso no programa reduzirá drasticamente a robustez, estabilidade, manutenção e reutilização do programa. O C++ também pode eliminar objetos globais completamente, mas finalmente não existe, uma das razões, eu acho, é a compatibilidade com o C.

    Em seguida, o membro estático da classe, já mencionado, é compartilhado por todos os objetos da classe básica e suas classes derivadas, por isso é uma ótima opção quando é necessário compartilhar dados ou comunicar entre essas classes ou objetos de classe.

    Depois, o objeto local estático, usado principalmente para manter o estado intermediário durante o período em que a função em que se encontra é chamada repetidamente. Um dos exemplos mais notáveis são as funções recursivas. Sabemos que as funções recursivas são aquelas que chamam suas próprias funções.

    No design de funções recursivas, objetos estáticos podem ser usados para substituir objetos locais não estáticos (ou seja, objetos de couro), o que não só reduz o custo de gerar e liberar objetos não estáticos em cada chamada recursiva e retorno, mas também permite que os objetos estáticos preservem o estado intermediário das chamadas recursivas e sejam acessíveis para cada camada de chamadas.

  • 3 Colheita acidental com objetos de alumínio

    Já foi apresentado anteriormente que os objetos de couro são criados no momento apropriado e depois liberados automaticamente no momento apropriado, ou seja, os objetos de couro têm funções de gerenciamento automático. Então, em que os objetos de couro são liberados automaticamente?

    O objeto de moldagem, quando liberado automaticamente, invoca sua própria função de desdobramento. Se nós enveloparmos os recursos no objeto de moldagem e executarmos a ação de libertar os recursos no objeto de moldagem, a probabilidade de vazamento de recursos será muito reduzida, porque o objeto de moldagem pode liberar os recursos automaticamente, mesmo quando a função em que ele está ocorre uma anomalia. O processo real é o seguinte: quando a função é lançada, ocorre o que se chama de stack_unwinding (em inglês: stack unwinding, em inglês: stack rollback), ou seja, uma expansão na pilha, ou seja, como os objetos de moldagem existem naturalmente no moldagem, a função de desdobramento do objeto de moldagem será executada no processo de desdobramento, liberando assim os pequenos recursos envelopados.

  • 4 Proibir a criação de objetos de pilha

    Como mencionado acima, se você decidir proibir a criação de um determinado tipo de objeto de pilha, então você pode criar uma classe de envelopes de recursos, que só pode ser gerada em um eixo, para liberar automaticamente os recursos do envelopes em casos de exceção.

    Então, como é que se proíbe a criação de objetos de pilha? Já sabemos que a única forma de criar objetos de pilha é usando a operação new, e se proibirmos a utilização de new não funciona. Mais adiante, a operação new é chamada de operador new quando executada, e o operador new é recarregável. Há uma maneira de fazer o new operador privado, e para simetria, é melhor recarregá-lo também como privado.

    #include <stdlib.h> //需要用到C式内存分配函数
    
    class Resource ; //代表需要被封装的资源类
    
    class NoHashObject
    {
    private: 
        Resource* ptr ;//指向被封装的资源
    
        ... ... //其它数据成员
    
        void* operator new(size_t size) //非严格实现,仅作示意之用
        { 
            return malloc(size) ; 
        } 
    
        void operator delete(void* pp) //非严格实现,仅作示意之用
        { 
            free(pp) ; 
        } 
    
    public: 
        NoHashObject() 
        { 
            //此处可以获得需要封装的资源,并让ptr指针指向该资源
    
            ptr = new Resource() ; 
        } 
        
        ~NoHashObject() 
        { 
    
            delete ptr ; //释放封装的资源
        } 
    }; 
    

    O NoHashObject é agora uma classe que proíbe objetos de pilha, se você escrever o seguinte código:

    NoHashObject* fp = new NoHashObject (()) ; // erro de compilação!

    suprimir fp;

    O código acima produz erros de compilação. Ok, agora que você já sabe como projetar uma classe que proíbe objetos de pilha, você pode ter a mesma dúvida que eu, se é que não pode gerar objetos de pilha desse tipo quando a definição da classe NoHashObject não pode ser alterada?

    void main(void)
    {
        char* temp = new char[sizeof(NoHashObject)] ; 
    
        //强制类型转换,现在ptr是一个指向NoHashObject对象的指针
    
        NoHashObject* obj_ptr = (NoHashObject*)temp ; 
    
        temp = NULL ; //防止通过temp指针修改NoHashObject对象
    
        //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
    
        Resource* rp = (Resource*)obj_ptr ; 
    
        //初始化obj_ptr指向的NoHashObject对象的ptr成员
    
        rp = new Resource() ; 
    
        //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
    
        ... ... 
    
        delete rp ;//释放资源
    
        temp = (char*)obj_ptr ; 
    
        obj_ptr = NULL ;//防止悬挂指针产生
    
        delete [] temp ;//释放NoHashObject对象所占的堆空间。
    
        } 
    

    A implementação acima é problemática e essa implementação raramente é usada na prática, mas eu escrevi o caminho, porque entendê-la é benéfico para a nossa compreensão dos objetos de memória C++.

    Os dados em um pedaço de memória são imutáveis, e o tipo são os óculos que usamos, e quando usamos um dos óculos, usamos o tipo correspondente para interpretar os dados na memória, de modo que diferentes interpretações dão diferentes informações.

    A chamada conversão de tipo forçada é, na verdade, trocar outro par de óculos e ver o mesmo pedaço de dados na memória novamente.

    Também é importante lembrar que diferentes compiladores podem organizar de forma diferente os dados dos membros do objeto, por exemplo, a maioria dos compiladores organiza os membros do ponteiro ptr do NoHashObject nos primeiros 4 bytes do espaço do objeto para garantir que a operação de conversão da seguinte frase seja executada como esperado:

    Recursos* rp = (Recursos*) obj_ptr ;

    No entanto, nem todos os compiladores são necessariamente assim.

    Uma vez que podemos proibir a produção de objetos de um determinado tipo de pilha, podemos projetar uma classe para que ela não produza objetos de cadeia? Claro que podemos.

  • 5 Proibição de produzir objetos de alumínio

    Como mencionado anteriormente, a criação de um objeto de casca movimenta o ponteiro de casca para retirar o espaço do tamanho apropriado do casco e, em seguida, chama diretamente a função de construção correspondente para formar um objeto de casca nesse espaço, e quando a função retorna, chama sua função de construção analítica para liberar o objeto e, em seguida, ajusta o ponteiro de casca para recuperar a memória do casco. O operador new/delete não é necessário neste processo, portanto, não é possível definir o operador new/delete como privado.

    Isso é possível, e eu também pretendo usar esse método. Mas antes disso, um ponto a ser considerado é que, se definirmos a função de construção como privada, também não podemos usar new para gerar diretamente objetos de pilha, porque new também invoca sua função de construção depois de alocar o espaço para o objeto. Então, eu só pretendo definir a função de construção de diagrama como privada.

    Se uma classe não é pretendida como uma classe base, o método mais comum é declarar sua função de análise como privada.

    Para restringir os objetos da cadeia sem restringir a herança, podemos declarar a função de análise como protegida, o que é bom para ambos.

    class NoStackObject
    {
    protected: 
    
        ~NoStackObject() { } 
    
    public: 
    
        void destroy() 
    
        { 
    
            delete this ;//调用保护析构函数
    
        } 
    }; 
    

    Em seguida, você pode usar uma classe NoStackObject como esta:

    NoStackObject* hash_ptr = novo NoStackObject() ;

    ...... // executar uma operação em um objeto hash_ptr

    hash_ptr->destroy (); O que é isso? Oh, não é estranho que nós criamos um objeto com new, mas não usamos delete para removê-lo, mas usamos o método de destruição. Obviamente, os usuários não estão acostumados com esse uso estranho. Então, eu decidi definir a função de construção também como privada ou protegida.

    class NoStackObject
    {
    protected: 
    
        NoStackObject() { } 
    
        ~NoStackObject() { } 
    
    public: 
    
        static NoStackObject* creatInstance() 
        { 
            return new NoStackObject() ;//调用保护的构造函数
        } 
    
        void destroy() 
        { 
    
            delete this ;//调用保护的析构函数
    
        } 
    };
    

    Agora você pode usar a classe NoStackObject assim:

    NoStackObject* hash_ptr = NoStackObject::creatInstance() ;

    ...... // executar uma operação em um objeto hash_ptr

    hash_ptr->destroy() ;

    hash_ptr = NULL; // evitar o uso de ponteiros pendurados

    Agora, parece que não é melhor, a operação de gerar objetos e liberar objetos é a mesma.

  • Métodos de reciclagem de lixo em C++

    Muitos programadores de C ou C++ não gostam da reciclagem de lixo, pensando que a reciclagem de lixo é definitivamente menos eficiente do que eles mesmos para gerenciar a memória dinâmica, e que, no momento da reciclagem, o programa deve parar lá, e se eles controlam o gerenciamento de memória, o tempo de alocação e liberação é estável e não causa a paralisação do programa. Finalmente, muitos programadores de C/C++ acreditam que não há mecanismos de reciclagem de lixo em C/C++. Essas ideias errôneas são especuladas por não entenderem os algoritmos de reciclagem de lixo.

    Na verdade, o mecanismo de reciclagem de lixo não é mais lento e até mais eficiente do que a distribuição de memória dinâmica. Como podemos apenas distribuir sem liberar, o tempo de distribuição de memória só precisa de obter uma nova memória da pilha, o ponteiro da pilha móvel é suficiente; e o processo de liberação é omitido e, naturalmente, acelerado. Os algoritmos modernos de reciclagem de lixo evoluíram muito, e os algoritmos de coleta incremental já permitem que o processo de reciclagem de lixo ocorra por etapas, evitando a interrupção do processo.

    A base dos algoritmos de reciclagem de lixo geralmente é baseada em escanear e marcar todos os blocos de memória que podem estar atualmente em uso, e recuperar a memória não marcada de todos os blocos de memória que já foram atribuídos. A ideia de que não se consegue fazer o reciclagem de lixo em C/C++ geralmente é baseada em não conseguir escanear corretamente todos os blocos de memória que podem ainda estar em uso, mas o que parece impossível não é realmente complicado. Primeiro, por escanear dados de memória, é fácil identificar pontos dinâmicos distribuídos em uma pilha de memória, e, se houver erros de identificação, é possível apenas pegar alguns dados que não são pontos como pontos, e não pegar pontos como dados não-pontos. Assim, o processo de reciclagem de lixo só recua e limpa erros que não devem ser recuperados.

    Quando o lixo é recuperado, basta digitalizar o segmento bss, o segmento de dados e o espaço de cache que está sendo usado para encontrar o número de pontos de memória dinâmica que podem ser, e o escaneamento recorrente da memória referida pode obter toda a memória dinâmica que está sendo usada.

    Se você quiser implementar um bom reciclador de lixo para o seu projeto, é possível melhorar a velocidade do gerenciamento de memória e até mesmo reduzir o consumo total de memória. Se você estiver interessado, pesquise os artigos e bibliotecas existentes na internet sobre reciclagem de lixo, o que é especialmente importante para um programador.

    Traduzido porHK Zhang

  • Por que, quando um indicador é endereçado a uma variável local, seu ciclo de vida pode ser prolongado até o final do programa?

    #include<stdio.h>
    int*fun(){
        int k = 12;
        return &k;
    }
    int main(){
        int *p = fun();    
        printf("%d\n", *p);
    
        getchar();
        return 0;
    }
    

    Não só pode ser acessado, mas também modificado, mas esse acesso é incerto. O endereço da variável local está no próprio programa, e após o término da variável de autoridade, seu valor permanece, desde que o endereço de memória da variável local não seja dado a outra variável. Mas se modificado, é mais perigoso, pois esse endereço de memória pode ser dado a outras variáveis do programa, e se forçado por um modificador do ponteiro, pode causar um crash do programa.

    CSDN bbs


Mais.