Antes de escrever uma estratégia em C++, é necessário ter conhecimentos básicos, não é preciso ter conhecimentos de Confucius, pelo menos conheça essas regras. Aqui está a transcrição:
Se uma pessoa se diz especialista em programação e não sabe nada sobre memória, então posso dizer-lhe que ela deve estar se gabando. Escrever programas em C ou C++ requer mais atenção à memória, não apenas porque a distribuição racional da memória afeta diretamente a eficiência e o desempenho do programa, mas principalmente porque há problemas que surgem quando operamos a memória de forma descuidada, e muitas vezes, esses problemas não são fáceis de detectar, como a fuga de memória, como o apontador pendurado.
Sabemos que o C++ divide a memória em três áreas lógicas: a pilha, a coluna e a área de armazenamento estática. Como tal, eu chamo os objetos entre eles de objetos de pilha, objetos de coluna e objetos estáticos. Então, qual é a diferença entre esses diferentes objetos de memória? Quais são as vantagens e desvantagens de objetos de pilha e de coluna? Como é proibido criar objetos de pilha ou de coluna?
1 Conceitos básicos
Para começar, vejamos , que é usado para armazenar variáveis ou objetos locais, como os objetos que declaramos em uma definição de função com uma declaração semelhante à seguinte:
Type stack_object ;
O stack_object é um objeto empilhado, cuja vida começa no ponto de definição e termina quando a função em que ele está é retornada.
Além disso, quase todos os objetos temporários são objetos embutidos. Por exemplo, a seguinte definição de função:
Type fun(Type object);
Esta função produz pelo menos dois objetos temporários, em primeiro lugar, os argumentos são passados por valor, então a função de construção de cópia é chamada para gerar um objeto temporário object_copy1, que é usado dentro da função não é objeto, mas object_copy1, naturalmente, object_copy1 é um objeto de armazém, que é liberado quando a função retorna; e esta função é um retorno de valor, quando a função retorna, se não considerarmos a otimização do valor de retorno ((NRV), então também será gerado um objeto temporário object_copy2, que será liberado dentro de um período de tempo após a função retornar. Por exemplo, uma função tem o seguinte código:
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
A implementação da segunda sentença acima é a seguinte: primeiro, a função fun retorna e gera um objeto temporário object_copy2 e depois chama o operador de atribuição para executar
tt = object_copy2 ; //调用赋值运算符
Vês? O compilador gera tantos objetos temporários para nós sem que nós saibamos, e o gasto de tempo e espaço para gerar esses objetos temporários pode ser muito grande, então, você pode entender por que é melhor passar os parâmetros da função com referência constante em vez de passar os parâmetros da função de acordo com o valor.
Em seguida, veja o empilhamento. O empilhamento, também conhecido como espaço de armazenamento livre, é distribuído dinamicamente durante a execução do programa, então sua maior característica é a dinâmica. Em C ++, a criação e destruição de todos os objetos do empilhamento são de responsabilidade do programador, então, se for mal tratado, ocorrerão problemas de memória. Se um objeto do empilhamento for distribuído e esquecer de liberá-lo, ocorrerá um vazamento de memória.
Então, como é que os objetos de uma pilha são distribuídos em C++? A única maneira de fazer isso é usando o new ((é claro que a instrução tipo malloc também pode ser usada para obter memória de pilha em C), e quando você usa o new, você distribui um pedaço de memória na pilha e retorna o ponteiro que aponta para o objeto da pilha.
De fato, 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 (), que executa a construção e inicialização de todos os objetos globais, é chamada antes do final da função main (), a função exit gerada pelo compilador, para liberar todos os objetos globais. Por exemplo, o código a seguir:
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Então, sabendo disso, podemos extrair algumas dicas, como, por exemplo, assumindo que vamos fazer alguns preparativos antes da execução da função main (), então podemos escrever esses preparativos para uma função de construção de um objeto global personalizado, de modo que, antes de executar o código gráfico da função main (), a função de construção desse objeto global seja chamada e execute o movimento esperado, atingindo assim nosso objetivo. Se falamos de um objeto global no espaço de armazenamento estático, então, o objeto estático local é um objeto? O objeto estático local também é geralmente definido na função, assim como o objeto, apenas que ele tem mais de uma palavra-chave estática na frente. O objeto estático local é o período de vida em que a função é chamada pela primeira vez, ou mais precisamente, a partir do momento em que o objeto estático é declarado pela primeira vez na execução do código estático do objeto, até que o objeto estático é destruído e o programa termina.
Há também um objeto estático, que é um membro estático de uma classe. Considerando essa situação, há problemas mais complexos envolvidos.
O primeiro problema é o tempo de vida dos objetos de membros estáticos de uma classe, que são criados com a criação do primeiro objeto de classe e que morrem ao final do programa. Por exemplo, existe uma situação em que definimos uma classe com um objeto estático como membro, mas, durante a execução do programa, se não criarmos nenhum objeto da classe, não será criado o objeto estático que a classe contém.
O segundo problema é quando:
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 as três sentenças acima marcadas como blackbody, e veja se o s_object que eles acessam é o mesmo objeto. A resposta é que sim, eles realmente apontam para o mesmo objeto, o que não parece ser verdade, não é? Mas é verdade, você pode escrever um simples parágrafo de código para verificar por si mesmo. O que eu vou fazer é explicar por que isso acontece.
Vamos pensar, quando passamos um objeto do tipo Derived1 para uma função que recebe um parâmetro do tipo Base que não é de referência, como é que o corte ocorre? Creio que agora você já sabe, é apenas tirar o subobjeto do objeto do tipo Derived1 e ignorar todos os outros membros de dados que o Derived1 define, e passar esse subobjeto para a função (na verdade, a função usa uma cópia deste subobjeto).
Todos os objetos de classes derivadas da classe Base herdada contêm um subobject do tipo Base (onde a chave de um objeto Derived1 pode ser apontada com o ponteiro do tipo Base, o que é naturalmente um polinomial), e todos os subobject e todos os objetos do tipo Base compartilham o mesmo objeto s_object, naturalmente, todas as instâncias de classes em todo o sistema de herança derivado da classe Base compartilham o mesmo objeto s_object. A disposição de objetos do exemplo 1, exemplo 2 mencionado acima é mostrada na figura a seguir:
2 Comparação de três tipos de objetos de memória
A vantagem dos objetos embutidos é que são gerados automaticamente quando apropriado e destruídos automaticamente quando apropriado, sem a preocupação do programador; e a velocidade de criação dos objetos embutidos é geralmente mais rápida do que a dos objetos empilhados, pois quando os objetos embutidos são distribuídos, a operação new do operador é chamada, o novo operador usa algum tipo de algoritmo de pesquisa de espaço de memória, e a pesquisa pode ser muito demorada, mas os objetos embutidos não são tão difíceis, basta mover o ponteiro do topo do embutido. No entanto, é importante notar que o espaço embutido geralmente é menor, geralmente 1MB para 2MB, então os objetos de tamanho maior não são adequados para serem distribuídos em embutidos.
Objetos de pilha, seus momentos de criação e destruição devem ser definidos pelo programador, ou seja, o programador tem controle total sobre a vida do objeto de pilha. Muitas vezes, precisamos de um objeto 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 criar um objeto de pilha é uma boa opção, e depois transmitir o ponteiro desse objeto de pilha entre as funções, para que o objeto possa ser compartilhado. Além disso, em comparação com o espaço livre, a capacidade do pilha é muito maior.
A seguir, vejamos objetos estáticos.
Em primeiro lugar, os objetos globais. Os objetos globais fornecem uma maneira mais simples de comunicação entre classes e entre funções, embora não seja elegante. Em geral, não existem objetos globais em linguagens totalmente orientadas a objetos, como C#, porque objetos globais significam insegurança e alta integração. O uso excessivo de objetos globais em programas reduz muito a robustez, estabilidade, manutenção e versatilidade do programa.
Em seguida, é o membro estático da classe, já mencionado acima, todos os objetos da classe base e suas classes derivadas compartilham esse objeto de membro estático, então esse membro estático é uma ótima opção quando é necessário compartilhar dados ou comunicar entre essas classes ou entre esses objetos de classe.
Um dos exemplos mais notáveis são as funções recursivas, e todos sabemos que as funções recursivas são funções que se chamam a si mesmas. Se se define um objeto local não-estático em uma função recursiva, o custo é grande quando o número de repetências é grande. Isso ocorre porque os objetos locais não-estáticos são objetos de silício.
Na concepção de funções recursivas, os objetos estáticos podem ser usados em vez dos objetos locais não-estáticos (ou seja, os objetos de coluna), o que não apenas reduz o custo de geração e liberação de objetos não-estáticos em cada chamada e retorno recursivo, mas também permite que os objetos estáticos preservem o estado intermediário da chamada recursiva e sejam acessíveis para cada camada de chamada.
3 Colheitas inesperadas de objetos usados para cozinhar
Já foi dito anteriormente que os objetos de silício são criados no momento apropriado e liberados automaticamente no momento apropriado, ou seja, os objetos de silício têm função de gerenciamento automático. Então, em que os objetos de silício são liberados automaticamente? Primeiro, no final de sua vida; segundo, quando ocorrem anomalias na função em que estão.
Quando um objeto de silício é liberado automaticamente, ele invoca sua própria função de decomposição. Se nós encapsularmos um recurso no objeto de silício e executarmos a ação de liberar um recurso na função de decomposição do objeto de silício, a probabilidade de vazamento de recursos será muito reduzida, porque o objeto de silício pode liberar recursos automaticamente, mesmo quando ocorrem anomalias na função em que ele está localizado. O processo real é o seguinte: quando a função lança uma anomalia, ocorre o que é chamado de stack_unwinding (rolling back of the stack), ou seja, a pilha se expande, e como o objeto de silício está presente no silício natural, a função de decomposição do objeto de silício é executada durante o rolamento da pilha, liberando o recurso emcapsulado.
4 Objetos em pilha proibidos
Como mencionado acima, se você decidir proibir a geração de um tipo de objeto de pilha, você pode criar uma classe de encapsulamento de recursos que só pode ser gerada em uma pilha, o que liberará automaticamente o encapsulamento de recursos em casos excepcionais.
Então, como proibir a criação de objetos de pilha? Nós já sabemos que a única maneira de criar objetos de pilha é usar a operação new, e se proibirmos o uso de new não funciona. Mais adiante, a operação new será executada chamando o operador new, e o operador new será recarregado.
#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 ; //释放封装的资源
}
};
NoHashObject é agora uma classe que proíbe objetos de pilha, se você escrever o seguinte código:
NoHashObject* fp = new NoHashObject (() ; // Compilação errada!
delete fp ;
O código acima pode gerar erros de compilação. Ok, agora que você já sabe como criar uma classe que proíbe objetos de pilha, você pode ter a mesma dúvida que eu, não seria possível gerar esse tipo de objeto de pilha se a definição da classe NoHashObject não pudesse ser alterada? Não, há uma maneira de fazer isso, que eu chamo de piratear a violência do silício.
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 raramente é usada na prática, mas eu escrevo o caminho, porque entender isso é bom para entendermos os objetos de memória em C++. O que é fundamental para tantas transformações de tipo forçado acima? Podemos entender assim:
Os dados em um bloco de memória são constantes, e o tipo é o óculos que usamos, e quando usamos um tipo de óculos, interpretamos os dados na memória com o tipo correspondente, de modo que diferentes interpretações recebem informações diferentes.
A chamada conversão de tipo forçada é, na verdade, trocar de óculos e voltar a ver o mesmo pedaço de memória.
Também é importante ressaltar que a disposição dos membros de dados de objetos pode ser diferente em diferentes compiladores, por exemplo, a maioria dos compiladores coloca os membros do ponteiro ptr do NoHashObject nos primeiros 4 bytes do espaço do objeto, garantindo que a conversão da seguinte frase seja executada como esperamos:
Resource* rp = (Resource*)obj_ptr ;
No entanto, nem todos os compiladores são necessariamente assim.
Como podemos proibir a criação de um tipo de objeto de pilha, podemos criar uma classe que não pode criar um objeto de barra? Claro que podemos.
5 Proibição de criar objetos de bronze
Como mencionado anteriormente, a criação de um objeto de coluna é feita movendo o ponteiro para extrair um espaço do tamanho apropriado do coluna, e depois, em um espaço chamado diretamente a função de construção correspondente para formar um objeto de coluna, e quando a função retorna, a função de sua composição é chamada para liberar o objeto, e depois ajustar o ponteiro para recuperar o bloco de memória do coluna. O processo não requer a operação new/delete, portanto, configurar o operador new/delete como privado não atinge o objetivo.
Isso é possível, e eu também pretendo usar esse tipo de solução. Mas antes disso, há uma coisa que precisa ser pensada: se definirmos a função de construção como privada, não poderemos usar o new para gerar objetos de pilha diretamente, porque o new também chamará sua função de construção depois de distribuir espaço para o objeto. Então, eu pretendo apenas configurar a função de decomposição como privada.
Se uma classe não pretende ser uma classe base, a solução comum é declarar sua função de decomposição como privada.
Para restringir o objeto de hash, mas não restringir a herança, podemos declarar a função de decomposição como protected, e assim tudo fica bem. O seguinte código mostra:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Em seguida, você pode usar a classe NoStackObject assim:
NoStackObject* hash_ptr = new NoStackObject() ;
… … // operar o objeto direcionado para hash_ptr
hash_ptr->destroy() ; Bem, não é um pouco estranho que nós criamos um objeto com o new e não o eliminamos com o delete, mas sim com o destruy. Obviamente, os usuários não estão acostumados com esse tipo de uso estranho. Então, eu decidi que a função de construção também deve ser definida 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() ;
… … // operar o objeto direcionado para hash_ptr
hash_ptr->destroy() ;
hash_ptr = NULL ; // impede o uso do ponteiro suspenso
Agora que a sensação é melhor, a operação de geração de objetos e liberação de objetos está em harmonia.
Muitos programadores de C ou C++ têm aversão à reciclagem de lixo, acreditando que a reciclagem de lixo é definitivamente menos eficiente do que a gestão da memória dinâmica, e que a recuperação necessariamente faz com que o programa pare lá, e que, se você controlar a gestão da memória, o tempo de distribuição e liberação é estável e não causa paralisação do programa. Finalmente, muitos programadores de C / C ++ acreditam firmemente que não há mecanismo de reciclagem de lixo em C / C ++.
Na verdade, o mecanismo de reciclagem de lixo não é 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 é suficiente para obter nova memória do monte, movendo o ponteiro do topo do monte; e o processo de liberação é omitido e, naturalmente, acelera a velocidade. Os algoritmos de reciclagem de lixo modernos já desenvolveram muito, os algoritmos de coleta incremental já podem fazer com que o processo de reciclagem de lixo seja dividido, evitando a interrupção do funcionamento do programa.
O conceito de que não é possível realizar a recuperação de lixo em C/C++ baseia-se na incapacidade de digitalizar corretamente todos os blocos de memória que ainda podem ser usados, mas o que parece impossível não é realmente complexo. Em primeiro lugar, é fácil identificar os ponteiros que apontam para a memória distribuída dinamicamente por cima da pilha, e, se houver um erro de identificação, apenas alguns dos ponteiros podem ser dados que não são ponteiros, e não os ponteiros podem ser dados que não são ponteiros. Assim, o processo de recuperação do lixo só perde a recuperação e não há erros que não devem ser recuperados.
A recuperação de lixo, basta escanear o segmento de bss, o segmento de dados e o espaço de cadência atualmente em uso, para encontrar a quantidade de indicadores de memória dinâmica que podem ser referenciados. A varredura recursiva da memória referenciada pode obter toda a memória dinâmica atualmente em uso.
É possível implementar um bom reciclador de lixo para o seu projeto, melhorar a velocidade de 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.
Traduzido de:HK Zhang
#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. Os endereços das variáveis locais estão na própria pilha do programa. Após o término da variável local, 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 for modificado, é mais perigoso, pois o endereço de memória pode ter sido dado a outras variáveis do programa, podendo causar o colapso do programa se forçado a modificar o programa através do ponteiro.