Gerenciamento de memória C ++ (1)

Anteontem, eu estava muito interessado no mecanismo de memória subjacente e aconteceu de escovar o mecanismo de gerenciamento de memória C ++ do professor Hou Jie na estação B. Demorou três dias um após o outro e finalmente terminei. Como diz o título do vídeo - de um terreno plano a prédios de dez mil pés de altura, realmente aprendi muito. Escrevi as seguintes notas de visualização para compartilhar um pouco de minha compreensão e experiência.

Primeira conversa sobre primitivos

Insira a descrição da imagem aqui

Esta imagem ilustra as quatro maneiras de um aplicativo C ++ solicitar memória, cada uma das quais pode realizar o aplicativo e a chamada de memória e tem um relacionamento hierárquico. Por exemplo, quando usamos o contêiner STL da biblioteca padrão C ++ para criar um objeto, o contêiner faz um aplicativo de memória para o alocador e, em seguida, o alocador aplica a memória para o próximo nível e assim por diante. Esse processo é encapsulado.A nosso ver, apenas criamos um objeto container, e o sistema nos aloca um pedaço de memória para uso, mas não conhecemos o princípio. Este é também o significado da existência de tal lição, nos contando como essas memórias são alocadas de forma eficiente. Voltando ao tópico, quando o alocador aplica a memória para o próximo nível, ele realmente chama new, new [], :: opeartor new (), etc. Todos nós sabemos que essas primitivas são palavras-chave para aplicar memória na gramática padrão C ++ . Assim, a tarefa de aplicação de memória é iterada para primitivas como new, new [], :: opeartor new () e, em seguida, analisa o código-fonte, descobriremos que em new, malloc é realmente chamado para alocar memória, e mais down é a função de alocação de memória fornecida pelo sistema operacional. Na verdade, não precisamos descer de nível para nível. Podemos solicitar memória a partir de qualquer etapa do meio, então por que não chamamos diretamente a função de aplicativo de memória fornecida pelo sistema operacional no nível mais baixo em o código? Na verdade, a resposta é óbvia: encapsulamos as funções subjacentes camada por camada, e os benefícios do encapsulamento permitem que os programadores as utilizem de maneira eficiente e conveniente para atingir nossos objetivos.

Insira a descrição da imagem aqui
Esta imagem mostra a operação real de chamar a nova função para aplicar para memória. Ele irá primeiro chamar a função new do operador. Esta função pode estar sobrecarregada, ou seja, para sobrecarregar a nova função :: operator (global), por nós mesmos. a função de alocação de memória. No operador new (), podemos ver claramente que a função malloc é realmente chamada para alocar memória. Outra etapa importante é ao chamar if (_callnewh (size) == 0). Esta etapa fornece uma estratégia para o caso de O motivo de malloc falhar , ele não lançará a exceção bad_alloc primeiro, mas chamará uma função _callnewh (tamanho) (chamada regravável de novo manipulador) para tentar liberar memória desnecessária para aplicar com êxito à memória. Após a alocação da memória, o construtor do objeto será executado para concluir a aplicação da memória. Este é o princípio da primeira camada dentro de novo.
Insira a descrição da imagem aqui
Deletar internamente é semelhante.Como podemos ver no código-fonte, o objeto é primeiro destruído e, em seguida, o operador delete é chamado, basicamente usando free para liberar a memória.
Insira a descrição da imagem aqui
Todos nós sabemos que se você deseja alocar dinamicamente um grupo de objetos, você precisa adicionar []. Neste momento, você irá chamar vários construtores para construir os objetos correspondentes. Quando você liberar a memória, também precisará adicionar [ ]. Neste momento, o ponteiro this chamado por dtor aponta para objetos diferentes são desconstruídos separadamente para liberar a memória. Que efeito terá se você não adicionar [] ao liberar memória? O canto inferior esquerdo da figura nos diz que a estrutura de dados do array de objetos que solicitamos tem um cookie. O cookie registra o tamanho da memória do aplicativo dinâmico, portanto, ao liberar, só precisamos saber o endereço de base do psa mais o tamanho do registro do cookie para liberar diretamente o aplicativo., Então, isso mostra que não precisamos adicionar [], de modo que dtor apenas chama o destruidor do objeto no topo da pilha uma vez. Mas se o objeto que solicitamos contém membros de ponteiro, não podemos liberar o espaço de ponteiro de todos os objetos de membro chamando o destruidor apenas uma vez, e isso causará vazamentos de memória. Portanto, é por isso que delete [] não tem efeito na liberação de objetos int char na biblioteca padrão, mas haverá vazamentos de memória na liberação de objetos personalizados.Esta é a razão pela qual o ponteiro está errado. Observe que a ordem é invertida quando o objeto é destruído.
Insira a descrição da imagem aqui
A colocação de novo é muito interessante. O posicionamento novo é uma versão sobrecarregada do operador novo, mas raramente o usamos. Se você quiser criar um objeto na memória alocada, usar novo não funcionará. Em outras palavras, a colocação de novo permite que você construa um novo objeto em uma memória já alocada (pilha ou heap). A segunda frase na figura acima é usar o posicionamento new para criar um objeto ponteiro complexo na memória buf. Podemos ver no código-fonte que ele não chama malloc para alocar memória, mas retorna diretamente buf. Neste momento, o complexo o ponteiro do objeto pc aponta para buf. Primeiro endereço e armazena o objeto, de modo a atingir o propósito do design.
Insira a descrição da imagem aqui
Esta imagem ilustra claramente o método de gerenciamento de memória. Primeiro olhe para a imagem. Como mencionado acima, ao chamar new, inseriremos a função new do operador. Se não reescrevermos a nova função global :: operator neste momento, realizaremos a operação malloc após inserir o :: nova função do operador, e se nós Quando a nova função do operador é reescrita dentro da classe, a prioridade da função que reescrevemos será maior e ela será executada primeiro. Isso mostra que podemos reescrever a nova função do operador dentro da classe para conseguir o gerenciamento de memória.Por exemplo, na próxima aula, vamos escrever uma estrutura de dados _pool_memory para gerenciar a memória e obter uma alocação de memória eficiente.
Como podemos alcançar um gerenciamento de memória eficiente?

1. No novo array, chamamos repetidamente malloc para solicitar a alocação de memória para alocar pouca memória. Quando a quantidade de dados é grande o suficiente, como milhões de vezes, é sempre ruim chamar malloc repetidamente. Em outras palavras, é sempre bom reduzir o número de chamadas malloc. Portanto, podemos primeiro alocar um grande bloco de memória como um pool de memória de reserva e cortar a pequena memória diretamente do pool de memória para uso quando ela precisar ser alocada posteriormente. Conforme mostrado no canto superior esquerdo. (Velocidade)
2. Para a memória que solicitamos, suponha que aplicamos 10 bytes de memória e conseguimos, mas na verdade o sistema operacional nos deu mais de 10 bytes (a próxima aula terá um diagrama de estrutura de memória), isso ocorre porque Em cada malloc, a memória que solicitamos contém dois cookies (de acordo com a versão do compilador), e um cookie ocupa 4 bytes (sistema de 32 bits), então se malloc é 1 milhão de vezes, então haverá 8 milhões de palavras Cookie da seção desperdício. Então, podemos reduzir o uso de cookies sem afetar a função dos cookies por meio de uma estrutura de dados específica e engenhosa e conseguir uma alocação de memória eficiente? (Espacialmente)

Insira a descrição da imagem aqui
O mesmo é verdade no contêiner, uma camada adicional de alocador desce para aplicar para a memória, mas o princípio permanece o mesmo da figura acima.

Bem, agora sabemos que o método de gerenciamento de memória é as duas idéias acima, o método é reescrever a nova função do operador para assumir a implementação da alocação de memória. Comece o combate real sem dizer uma palavra .

1.1 Gerenciamento de memória
Insira a descrição da imagem aqui
no C ++ Primer Na classe Screen, primeiro olhamos para os principais membros de dados. Há uma variável do tipo int, um próximo ponteiro para ela mesma e dois membros de dados estáticos. Entre eles, freeStroe aponta para o cabeçalho de um grande bloco de memória obtido pela aplicação. Pointer, screenChunk é uma constante estática 24. Neste momento sizeof (Screen) = sizeof (obj), ou seja, o tamanho de um objeto é maior ou igual à soma dos tamanhos de todos os membros não estáticos, portanto é de 8 bytes. Em seguida, observe as funções de membro da classe, o operador reescrito novo e exclua o gerenciamento de memória do implemento. Na função new do operador, definimos um chunk como o número de bytes de memória grandes que queremos obter e, em seguida, usamos o novo aplicativo para obter a memória grande necessária e atribuí-la ao ponteiro freeStore e, em seguida, usar o loop for para dividir a memória grande para obter a memória pequena para cada alocação de Malloc. No operador delete, usamos o método de inserção head da lista vinculada para inserir o espaço do objeto recuperado de volta na lista vinculada e mover o ponteiro freeStore.
Insira a descrição da imagem aqui
Na instância de teste, podemos ver que a classe Screen após a reescrita do operador new tem dois cookies faltando ao criar objetos, e cada objeto tem 8 bytes a menos, o que está em linha com a alocação de memória no espaço. Este modelo também tem deficiências. Ao projetar, projetamos um ponteiro extra e pensamos apenas em nós mesmos, de modo que o tamanho do seziof original foi duplicado. Neste caso, embora seja um membro de dados interno, pode haver várias variáveis ​​na prática. 100 % da taxa de expansão. Como resultado, embora tenhamos reduzido o cookie, mas aumentado o tamanho dos próprios dados, estendemos a próxima versão.

1.2 O gerenciamento de memória no C ++ Primer
Insira a descrição da imagem aqui
ainda é o mesmo. Primeiro olhe para os membros dos dados. Uma estrutura e uma união são definidas, e duas variáveis ​​estáticas são definidas. Tenho muitas dúvidas sobre o tamanho da estrutura da classe. Se, como o design a caminho, o tamanho da união da variável de estrutura indefinida e a variável de união indefinida for 0, a saída de seziof deve ser 1 .Isto é uma dúvida. Suponha que definamos variáveis ​​para a variável de estrutura e a variável de união. De acordo com o princípio de alinhamento da memória, a primeira estrutura ocupa 8 bytes. O deslocamento inicial de todas as variáveis ​​na união é o mesmo, então o tamanho é a união O elemento com o maior tamanho no corpo também ocupa 8 bytes, portanto, o tamanho total de deve produzir 16. Essa é a segunda dúvida. No entanto, o tamanho do vídeo de Hou Jie e dos casos de teste subsequentes é de 8 bytes, o que é realmente confuso. Os leitores podem comentar e explicar . Por conveniência, presumimos que seja de 8 bytes. Existem também duas variáveis ​​de membro estático na classe, semelhantes à versão 1.1, uma é uma constante para obter um grande bloco de memória e um ponteiro é usado para apontar para a lista vinculada de memória desenvolvida. Olhando para as funções-membro, o principal é reescrever o operador novo e excluí-lo. No operador new, o princípio de funcionamento é semelhante ao da versão 1.1, mas o global :: operator new é usado para aplicar para memória, e operator delete também é semelhante, então não vou explicar muito.

Comparando as versões 1.1 e 1.2, descobrimos que a maior diferença é o uso de união, que toma emprestados os primeiros quatro bytes de união para definir o próximo ponteiro. Como a memória é compartilhada na união, usamos o conceito de ponteiros incorporados para use os quatro primeiros quando a memória não estiver alocada. Os bytes são convertidos nos próximos ponteiros. Após a alocação, grave os dados neste bloco de memória para substituir o conteúdo do ponteiro para torná-lo inválido. Desta forma, a otimização pode ser alcançada , o que não só reduz o número de cookies a dois blocos no início e no final, mas também economiza ponteiros. Ponteiro próprio para alcançar grande otimização no espaço.

Insira a descrição da imagem aqui
Verificar a amostra de teste é de fato o esperado.

Como é muito problemático reescrever o operador novo e excluí-lo dessa forma em cada função, o código é redundante, portanto, podemos projetar uma classe especial para fazer a alocação de memória, de modo que o alocador apareça.
Insira a descrição da imagem aqui
O código foi ligeiramente alterado, mas o princípio é semelhante, então não vou falar sobre isso novamente. Neste momento, para alocar memória na classe, você só precisa definir um alocador estático para alocar memória para cada objeto.

Por fim, existe uma função que considero muito útil, que vale a pena tomar notas, ou seja, = delete e = default em C ++ 11. Adicione o especificador = default ao final da declaração da função para declarar a função como o construtor do display default. Usar o especificador = delete para desabilitar qualquer função de membro que ele usa também é válido para nosso operador reescrito new e delete, e será útil em algumas ocasiões especiais.

Nesse ponto, a primeira palestra acabou. Desde o terreno plano até o prédio de dez mil pés de altura, ele pode ser considerado o primeiro andar.

Acho que você gosta

Origin blog.csdn.net/GGGGG1233/article/details/114989004
Recomendado
Clasificación