Alocação dinâmica de memória em C para um array. Alocação dinâmica de memória em C. Funções padrão de alocação de memória dinâmica

Trabalhar com memória dinâmica costuma ser um gargalo em muitos algoritmos, a menos que truques especiais sejam usados.

Neste artigo, examinarei algumas dessas técnicas. Os exemplos do artigo diferem (por exemplo, deste) porque é utilizada a sobrecarga dos operadores new e delete, e por isso as estruturas sintáticas serão minimalistas e o retrabalho do programa será simples. As armadilhas encontradas no processo também são descritas (é claro, os gurus que lerem o padrão de capa a capa não ficarão surpresos).

0. Precisamos de trabalho manual com memória?

Em primeiro lugar, vamos verificar o quanto um alocador inteligente pode acelerar o trabalho da memória.

Vamos escrever testes simples para C++ e C# (C# é conhecido por seu excelente gerenciador de memória, que divide objetos em gerações, usa pools diferentes para objetos de tamanhos diferentes, etc.).

Nó de classe ( público: Nó* próximo; ); // ... for (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Nó de classe (nó público next;) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

Apesar de toda a natureza de “vácuo esférico” do exemplo, a diferença de tempo foi de 10 vezes (62 ms versus 650 ms). Além disso, o exemplo C# está finalizado e de acordo com as regras de boas maneiras em C++, os objetos selecionados devem ser excluídos, o que aumentará ainda mais o intervalo (até 2580 ms).

1. Conjunto de objetos

A solução óbvia é pegar um grande bloco de memória do sistema operacional e dividi-lo em blocos iguais de tamanho sizeof(Node), ao alocar memória, retirar o bloco do pool e, ao liberá-lo, devolvê-lo ao pool. A maneira mais fácil de organizar um pool é usando uma lista vinculada individualmente (pilha).

Como o objetivo é uma intervenção mínima no programa, tudo o que pode ser feito é adicionar o mixin BlockAlloc à classe Node:
Nó de classe: BlockAlloc público

Primeiro de tudo, precisamos de um conjunto de grandes blocos (páginas), que retiramos do sistema operacional ou do tempo de execução C. Ele pode ser organizado sobre as funções malloc e free, mas para maior eficiência (para pular o nível extra de abstração), usamos VirtualAlloc/VirtualFree. Essas funções alocam memória em múltiplos de blocos de 4K e também reservam espaço de endereço do processo em múltiplos de blocos de 64K. Ao especificar simultaneamente as opções commit e reserve, saltamos para outro nível de abstração, reservando espaço de endereço e alocando páginas de memória em uma única chamada.

Classe PagePool

inline size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) modelo class PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); return page; ) ~PagePool() ( for (vetor ::iterador i = pages.begin(); eu != páginas.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) privado: vetor Páginas; );

Então organizamos um conjunto de blocos de um determinado tamanho

Classe BlockPool

modelo classe BlockPool: PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; head = tmp; ) privado: void* head; tamanho_t BlockSize; void FormatNewPage();< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

Comente // todo: bloquear(isto) Os locais que exigem sincronização entre threads são marcados (por exemplo, use EnterCriticalSection ou boost::mutex).

Explicarei porque ao “formatar” uma página, a abstração FreeBlock não é usada para adicionar um bloco ao pool. Se algo assim fosse escrito

Para (tamanho_t i = 0; i< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

Então a página, usando o princípio FIFO, seria marcada “ao contrário”:

Vários blocos solicitados do pool consecutivos teriam endereços decrescentes. Mas o processador não gosta de retroceder, isso quebra a pré-busca ( Atualização: Não relevante para processadores modernos). Se você fizer marcação em loop
for (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
então o ciclo de marcação voltaria para os endereços.

Agora que os preparativos estão feitos, podemos descrever a classe mixin.
modelo classe BlockAlloc ( público: static void* operador new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) operador static void delete(void * m, size_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( pool.FreeBlock(m); ) ) // todo: implementar sobrecargas nothrow_t, de acordo com o comentário de borisko" // http://habrahabr.ru/post/148657/#comment_5020297 // Evite ocultar o posicionamento new que é necessário para os contêineres stl... static void* operador new(size_t, void * m) ( return m; ) // ...e o aviso sobre posicionamento ausente delete... static void operator delete(void*, void*) ( ) private: static BlockPool piscina; ); modelo BlockPool BlockAlloc ::piscina;

Vou explicar por que as verificações são necessárias se (s! = tamanho de (T))
Quando eles funcionam? Então, quando uma classe herdada da base T é criada/excluída.
Os herdeiros usarão o new/delete usual, mas o BlockAlloc também pode ser misturado a eles. Dessa forma, podemos determinar com facilidade e segurança quais classes devem utilizar os pools sem medo de quebrar algo no programa. A herança múltipla também funciona muito bem com este mixin.

Preparar. Herdamos o Node do BlockAlloc e executamos novamente o teste.
O tempo de teste agora é de 120 ms. 5 vezes mais rápido. Mas em C# o alocador ainda é melhor. Provavelmente não é apenas uma lista vinculada. (Se imediatamente após new chamarmos delete, e assim não desperdiçarmos muita memória, colocando os dados no cache, obtemos 62 ms. Estranho. Exatamente como o .NET CLR, como se retornasse variáveis ​​locais liberadas imediatamente para o pool correspondente, sem esperar pelo GC)

2. Recipiente e seu conteúdo colorido

Você costuma encontrar classes que armazenam muitos objetos filhos diferentes, de modo que o tempo de vida destes últimos não é maior que o tempo de vida do pai?

Por exemplo, pode ser uma classe XmlDocument preenchida com classes Node e Attribute, bem como strings C (char*) extraídas do texto dentro dos nós. Ou uma lista de arquivos e diretórios no gerenciador de arquivos que são carregados uma vez quando o diretório é relido e nunca mais são alterados.

Como foi mostrado na introdução, apagar é mais caro que novo. A ideia da segunda parte do artigo é alocar memória para objetos filhos em um grande bloco associado ao objeto Pai. Quando um objeto pai é excluído, os destruidores filhos serão chamados, como de costume, mas a memória não precisará ser devolvida - ela será liberada em um bloco grande.

Vamos criar uma classe PointerBumpAllocator que pode arrancar pedaços de tamanhos diferentes de um bloco grande e alocar um novo bloco grande quando o antigo estiver esgotado.

Classe PointerBumpAllocator

modelo class PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (bloco, PageSize); head = GetPage(free); void* tmp = head = (char*)head + block; ::iterador i = pages.begin(); eu != páginas.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) private: void* GetPage(size_t size) ( void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page) ; página de retorno; Páginas;<>cabeça vazia*; tamanho_t grátis; ); typedef PointerBumpAllocator

Alocador padrão;

Finalmente, vamos descrever um mixin ChildObject com new e delete sobrecarregados acessando um determinado alocador: Modelo

struct ChildObject ( static void* operador new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* operador new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static operador void delete(void*, size_t) ( ) // *1 operador estático void delete(void*, A*) ( ) operador estático void delete(void*, A&) ( ) private: static void* operador new(size_t s );

Neste caso, além de adicionar um mixin à classe filha, você também precisará corrigir todas as chamadas para new (ou usar o padrão “fábrica”). A sintaxe do novo operador será a seguinte:

Novo (...parâmetros para o operador...) ChildObject (...parâmetros para o construtor...)
Por conveniência, especifiquei dois novos operadores que aceitam A& ou A*.
Se o alocador for adicionado à classe pai como membro, a primeira opção será mais conveniente:
nó = novo (alocador) XmlNode (nome do nó);
Se o alocador for adicionado como ancestral (mixin), o segundo será mais conveniente:

nó = novo (este) XmlNode (nome do nó);
Não há sintaxe especial para chamar delete; o compilador chamará delete padrão (marcado como *1), independentemente de qual novo operador foi usado para criar o objeto. Ou seja, a sintaxe de exclusão é normal:

excluir nó;

Colocar o novo operador na seção privada protege contra chamadas new sem especificar um alocador.

Aqui está um exemplo completo de uso do par Allocator-ChildObject:

Exemplo

class XmlDocument: public DefaultAllocator ( public: ~XmlDocument() ( for (vetor ::iterador i = nós.begin(); eu != nós.end(); ++i) ( delete (*i); ​​​​) ) void AddNode(char* conteúdo, char* nome) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content ); char* n = (char*)AllocBlock(strlen(nome)+1); nós.push_back(new(this) XmlNode(c, n) ) classe XmlNode: public ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* name; ); privado: vetor nós; );

Conclusão. O artigo foi escrito há 1,5 anos para o sandbox, mas, infelizmente, o moderador não gostou.

Última atualização: 28/05/2017

Ao criar um array com tamanho fixo, uma certa quantidade de memória é alocada para ele. Por exemplo, digamos que temos um array com cinco elementos:

Números duplos = (1,0, 2,0, 3,0, 4,0, 5,0);

Para tal matriz, a memória alocada é 5 * 8 (tamanho duplo) = 40 bytes. Dessa forma sabemos exatamente quantos elementos estão no array e quanta memória ele ocupa. Contudo, isto nem sempre é conveniente. Às vezes é necessário que o número de elementos e, consequentemente, o tamanho da memória alocada para um array sejam determinados dinamicamente dependendo de certas condições. Por exemplo, o próprio usuário pode inserir o tamanho do array. E neste caso, podemos usar a alocação dinâmica de memória para criar o array.

Para controlar a alocação dinâmica de memória, são usadas várias funções, definidas no arquivo de cabeçalho stdlib.h:

    Malloc(). Tem um protótipo

    Void *malloc(s não assinados);

    Aloca memória de comprimento s bytes e retorna um ponteiro para o início da memória alocada. Se não tiver sucesso, retorna NULL

    calloc(). Tem um protótipo

    Void *calloc(n não assinado, m não assinado);

    Aloca memória para n elementos de m bytes cada e retorna um ponteiro para o início da memória alocada. Retorna NULL se não tiver êxito

    reallocar() . Tem um protótipo

    Void *realloc(void *bl, ns não assinado);

    Redimensiona o bloco de memória alocado anteriormente apontado pelo ponteiro bl para ns bytes de tamanho. Se o ponteiro bl tiver valor NULL, ou seja, nenhuma memória foi alocada, então a ação da função é semelhante à de malloc

    livre() . Tem um protótipo

    Void *grátis(void *bl);

    Libera um bloco de memória previamente alocado, cujo início é apontado pelo ponteiro bl.

    Se não usarmos esta função, a memória dinâmica ainda será liberada automaticamente quando o programa for encerrado. No entanto, ainda é uma boa prática chamar a função free(), que permite liberar memória o mais cedo possível.

Vamos considerar o uso de funções em um problema simples. O comprimento do array é desconhecido e é inserido pelo usuário durante a execução do programa, e também os valores de todos os elementos são inseridos pelo usuário:

#incluir #incluir int main(void) ( int *block; // ponteiro para o bloco de memória int n; // número de elementos do array // insira o número de elementos printf("Size of array="); scanf("%d", &n); // alocar memória para o array // a função malloc retorna um ponteiro do tipo void* // que é automaticamente convertido para o tipo int* block = malloc(n * sizeof(int)); matriz para (int i=0;i)

Saída do console do programa:

Tamanho da matriz = 5 bloco = 23 bloco = -4 bloco = 0 bloco = 17 bloco = 81 23 -4 0 17 81

Aqui, um ponteiro de bloco do tipo int é definido para gerenciar a memória do array. O número de elementos do array é desconhecido antecipadamente e é representado pela variável n;

Primeiro, o usuário insere o número de elementos que se enquadram na variável n. Depois disso, você precisa alocar memória para um determinado número de elementos. Para alocar memória aqui, poderíamos usar qualquer uma das três funções descritas acima: malloc, calloc, realloc. Mas especificamente nesta situação, usaremos a função malloc:

Bloco = malloc(n * sizeof(int));

Em primeiro lugar, deve-se notar que todas as três funções acima, por uma questão de universalidade do valor de retorno, retornam um ponteiro do tipo void * como resultado. Mas no nosso caso, é criado um array do tipo int, que é manipulado por um ponteiro do tipo int * , então o resultado da função malloc é convertido implicitamente para o tipo int * .

A própria função malloc recebe o número de bytes do bloco alocado. Este número é bastante simples de calcular: basta multiplicar o número de elementos pelo tamanho de um elemento n * sizeof(int) .

Após todas as ações serem concluídas, a memória é liberada usando a função free():

Grátis(bloquear);

É importante que após executar esta função não poderemos mais utilizar o array, por exemplo, exibir seus valores no console:

Grátis(bloquear); para(int i=0;i

E se tentarmos fazer isso, obteremos valores indefinidos.

Em vez da função malloc, poderíamos usar de forma semelhante a função calloc(), que pega o número de elementos e o tamanho de um elemento:

Bloco = calloc(n, sizeof(int));

Ou você também pode usar a função realloc():

Int *bloco = NULO; bloco = realloc(bloco, n * sizeof(int));

Ao usar realloc, é desejável (em alguns ambientes, por exemplo, no Visual Studio, obrigatório) inicializar o ponteiro pelo menos NULL.

Mas, em geral, todas as três chamadas neste caso teriam um efeito semelhante:

Bloco = malloc(n * sizeof(int)); bloco = calloc(n, sizeof(int)); bloco = realloc(bloco, n * sizeof(int));

Agora vamos examinar um problema mais complexo - alocar memória dinamicamente para um array bidimensional:

#incluir #incluir int main(void) ( int **table; // ponteiro para um bloco de memória para uma matriz de ponteiros int *rows; // ponteiro para um bloco de memória para armazenar informações de linha int rowscount; // número de linhas int d; / / número de entrada // insira o número de linhas printf("Rows count="); scanf("%d", &rowscount // aloca memória para uma tabela de array bidimensional = calloc(rowscount, sizeof(int*)). ); )*rowscount); // percorre as linhas for (int i = 0; i

A variável de tabela representa um ponteiro para uma matriz de ponteiros do tipo int* . Cada ponteiro table[i] nesta matriz representa um ponteiro para uma submatriz de elementos int, ou seja, linhas individuais da tabela. E a variável table na verdade representa um ponteiro para uma matriz de ponteiros para linhas da tabela.

Para armazenar o número de elementos em cada subarray, um ponteiro de linhas do tipo int é definido. Na verdade, ele armazena o número de colunas para cada linha da tabela.

Primeiro, o número de linhas é inserido na variável rowscount. O número de linhas é o número de ponteiros na matriz apontada pelo ponteiro da tabela. Além disso, o número de linhas é o número de elementos na matriz dinâmica apontada pelo ponteiro de linhas. Portanto, primeiro você precisa alocar memória para todos esses arrays:

Tabela = calloc(rowscount, sizeof(int*)); linhas = malloc(sizeof(int)*rowscount);

A seguir no loop, o número de colunas para cada linha é inserido. O valor inserido vai para a matriz de linhas. E de acordo com o valor inserido, o tamanho de memória necessário é alocado para cada linha:

Scanf("%d", &linhas[i]); tabela[i] = calloc(linhas[i], sizeof(int));

Em seguida, os elementos de cada linha são inseridos.

No final do programa, a memória é liberada durante a saída. No programa, a memória é alocada para linhas da tabela, portanto essa memória deve ser liberada:

Grátis(tabela[i]);

Além disso, a memória alocada para ponteiros de tabela e linhas é liberada:

Grátis(mesa); grátis(linhas);

Saída do console do programa:

Contagem de linhas=2 Contagem de colunas para 1=3 tabela=1 tabela=2 tabela=3 Contagem de colunas para 2=2 tabela=4 tabela=5 1 2 3 4 5

Descobrimos as possibilidades de alocação dinâmica de memória. O que isso significa? Isso significa que com a alocação dinâmica de memória, a memória é reservada não no estágio de compilação, mas no estágio de execução do programa. E isso nos dá a capacidade de alocar memória de forma mais eficiente, principalmente para arrays. Com a alocação dinâmica de memória, não precisamos definir o tamanho do array antecipadamente, especialmente porque nem sempre se sabe qual tamanho o array deve ter. A seguir, vamos ver como a memória pode ser alocada.

Alocação de memória em C (função malloc)

A função malloc() é definida no arquivo de cabeçalho stdlib.h e é usada para inicializar ponteiros com a quantidade necessária de memória. A memória é alocada no setor RAM disponível para quaisquer programas em execução na máquina. O argumento é o número de bytes de memória que precisam ser alocados. A função retorna um ponteiro para o bloco alocado na memória. A função malloc() funciona como qualquer outra função, nada de novo.

Como diferentes tipos de dados têm diferentes requisitos de memória, de alguma forma precisamos aprender como obter o tamanho de bytes para diferentes tipos de dados. Por exemplo, precisamos de uma seção de memória para um array de valores do tipo int - este é um tamanho de memória, e se precisarmos alocar memória para um array do mesmo tamanho, mas do tipo char - este é um tamanho diferente. Portanto, você precisa calcular de alguma forma o tamanho da memória. Isso pode ser feito usando a operação sizeof(), que pega uma expressão e retorna seu tamanho. Por exemplo, sizeof(int) retornará o número de bytes necessários para armazenar um valor int. Vejamos um exemplo:

#incluir int *ptrVar = malloc(tamanho(int));

Neste exemplo, em linha 3 O ponteiro ptrVar recebe um endereço para um local de memória cujo tamanho corresponde ao tipo de dados int. Automaticamente, esta área de memória torna-se inacessível a outros programas. Isso significa que depois que a memória alocada se tornar desnecessária, ela deverá ser liberada explicitamente. Se a memória não for liberada explicitamente, após a conclusão do programa, a memória não será liberada para o sistema operacional, isso é chamado de vazamento de memória. Você também pode determinar o tamanho da memória alocada que precisa ser alocada passando um ponteiro nulo. Aqui está um exemplo:

Int *ptrVar = malloc(sizeof(*ptrVar));

O que está acontecendo aqui? A operação sizeof(*ptrVar) estimará o tamanho do local de memória referenciado pelo ponteiro. Como ptrVar é um ponteiro para um local de memória interna, sizeof() retornará o tamanho do número inteiro. Na verdade, com base na primeira parte da definição do ponteiro, o tamanho da segunda parte é calculado. Então, por que precisamos disso? Isso pode ser necessário se de repente precisarmos alterar a definição de um ponteiro, int , por exemplo, para float e então não precisarmos alterar o tipo de dados em duas partes da definição do ponteiro. Será suficiente se mudarmos a primeira parte:

Float *ptrVar = malloc(sizeof(*ptrVar));

Como você pode ver, há um ponto muito forte nesta notação, não devemos chamar a função malloc() usando sizeof(float) . Em vez disso, passamos um ponteiro para o tipo float para malloc(), caso em que o tamanho da memória alocada será determinado automaticamente!

Isto é especialmente útil se você precisar alocar memória longe da definição do ponteiro:

Float *ptrVar; /* . . . cem linhas de código */ . . . ptrVar = malloc(sizeof(*ptrVar));

Se você usasse uma construção de alocação de memória com a operação sizeof(), teria que encontrar a definição de um ponteiro no código, observar seu tipo de dados e só então seria capaz de alocar memória corretamente.

Liberando memória alocada

A liberação de memória é feita usando a função free(). Aqui está um exemplo:

Grátis(ptrVar);

Após liberar a memória, é uma boa prática zerar o ponteiro, ou seja, atribuir *ptrVar = 0 . Se você atribuir 0 a um ponteiro, o ponteiro se tornará nulo, ou seja, não apontará mais para nada. Sempre atribua 0 ao ponteiro após liberar a memória, caso contrário, mesmo após liberar a memória, o ponteiro ainda aponta para ele, o que significa que você pode danificar acidentalmente outros programas que possam estar utilizando esta memória, mas você nem sabe nada sobre você descobrirá e pensará que o programa funciona corretamente.

P.S.: Qualquer pessoa interessada em edição de vídeo pode se interessar por este editor de vídeo do Windows 7. O editor de vídeo se chama Movavi, talvez alguém já conheça ou até já tenha trabalhado com ele. Com este programa em russo, você pode facilmente adicionar vídeo de sua câmera, melhorar a qualidade e aplicar belos efeitos de vídeo.

C++ oferece suporte a três tipos principais descarga(ou mais "distribuições") memória, dois dos quais já estamos familiarizados:

Alocação de memória estática vale para e variáveis. A memória é alocada uma vez, quando o programa é iniciado, e é retida durante todo o programa.

Alocação automática de memória vale para e . A memória é alocada ao entrar no bloco que contém essas variáveis ​​e removida ao sair dele.

Alocação dinâmica de memóriaé o tema desta lição.

Alocação dinâmica de variáveis

A alocação de memória estática e automática têm duas propriedades comuns:

Como funciona a alocação dinâmica de memória?

Seu computador possui memória (talvez a maior parte dela) disponível para uso por programas. Quando você executa um programa, seu sistema operacional carrega esse programa em alguma parte dessa memória. E essa memória utilizada pelo seu programa é dividida em várias partes, cada uma delas executando uma tarefa específica. Uma parte contém seu código, a outra é usada para realizar operações normais (controlar quais funções são chamadas, criar e destruir variáveis ​​globais e locais, etc.). Falaremos sobre isso mais tarde. No entanto, a maior parte da memória disponível está simplesmente parada, aguardando solicitações de alocação dos programas.

Ao alocar memória dinamicamente, você solicita ao sistema operacional que reserve parte dessa memória para uso do seu programa. Se o sistema operacional puder atender a essa solicitação, o endereço dessa memória será retornado ao seu programa. A partir de agora seu programa poderá utilizar esta memória sempre que desejar. Quando você já tiver feito tudo o que foi necessário com essa memória, ela precisará ser devolvida ao sistema operacional para ser distribuída entre outras solicitações.

Ao contrário da alocação de memória estática ou automática, o programa é responsável por solicitar e retornar a memória alocada dinamicamente.

Liberando memória

Ao alocar dinamicamente uma variável, você também pode inicializá-la por meio de inicialização uniforme (em C++ 11):

int *ptr1 = novo int (7); // usa inicialização direta int *ptr2 = new int ( 8 ); // usa inicialização uniforme

Quando tudo o que era necessário já foi feito com uma variável alocada dinamicamente, você precisa dizer explicitamente ao C++ para liberar essa memória. Para variáveis ​​isso é feito usando operador excluir:

// Suponha que ptr foi alocado anteriormente usando o operador new delete ptr; // retorna a memória apontada por ptr de volta ao sistema operacional ptr = 0; // torna ptr um ponteiro nulo (use nullptr em vez de 0 em C++ 11)

O operador delete na verdade não exclui nada. Ele simplesmente retorna a memória que foi alocada anteriormente para o sistema operacional. O sistema operacional pode então reatribuir essa memória a outro aplicativo (ou ao mesmo aplicativo novamente).

Embora possa parecer que estamos excluindo variável, mas isso não é verdade! Uma variável de ponteiro ainda tem o mesmo escopo de antes e pode receber um novo valor como qualquer outra variável.

Observe que excluir um ponteiro que não aponte para a memória alocada dinamicamente pode causar problemas.

Sinais pendurados

C++ não oferece garantias sobre o que acontecerá com o conteúdo da memória liberada ou com o valor do ponteiro excluído. Na maioria dos casos, a memória devolvida ao sistema operacional conterá os mesmos valores que tinha antes. libertação, e o ponteiro permanecerá apontando apenas para a memória já liberada (excluída).

Um ponteiro apontando para a memória liberada é chamado sinal pendurado. Desreferenciar ou remover um ponteiro pendente produzirá resultados inesperados. Considere o seguinte programa:

#incluir int main() ( int *ptr = new int; *ptr = 8; // coloca o valor no local de memória alocado delete ptr; // retorna a memória de volta ao sistema operacional. ptr agora é um ponteiro pendente std:: corte<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#incluir

int principal()

int *ptr = novo int; // aloca dinamicamente uma variável inteira

*ptr = 8; //coloca o valor na célula de memória alocada

excluir ptr; // retorna a memória para o sistema operacional. ptr agora é um ponteiro pendente

std::cout<< * ptr ; // desreferenciar um ponteiro pendente levará a resultados inesperados

excluir ptr; //tentar liberar memória novamente também levará a resultados inesperados

retornar 0;

No programa acima, o valor 8, que foi atribuído anteriormente a uma variável dinâmica, pode ou não continuar ali após ser liberado. Também é possível que a memória liberada já tenha sido alocada para outro aplicativo (ou para uso do próprio sistema operacional) e a tentativa de acessá-la fará com que o sistema operacional encerre automaticamente o seu programa.

O processo de liberação de memória também pode levar à criação diversos sinais pendurados. Considere o seguinte exemplo:

#incluir int main() ( int *ptr = new int; // aloca dinamicamente uma variável inteira int *otherPtr = ptr; // otherPtr agora aponta para a mesma memória alocada que ptr delete ptr; // retorna a memória de volta ao sistema operacional . ptr e otherPtr agora são ponteiros pendentes ptr = 0; // ptr agora é nullptr // No entanto, otherPtr ainda é um ponteiro pendente return 0;

#incluir

int principal()

int *ptr = novo int; // aloca dinamicamente uma variável inteira

int * outroPtr = ptr; // otherPtr agora aponta para a mesma memória alocada que ptr

excluir ptr; // retorna a memória para o sistema operacional. ptr e otherPtr agora são ponteiros pendentes

ptr = 0; // ptr agora é nullptr

// Contudo, otherPtr ainda é um ponteiro pendente!

retornar 0;

Primeiro, tente evitar situações em que vários ponteiros apontem para a mesma parte da memória alocada. Se isso não for possível, deixe claro qual ponteiro "possui" a memória (e é responsável por excluí-la) e quais ponteiros simplesmente a acessam.

Em segundo lugar, quando você exclui um ponteiro e se ele não sai imediatamente após a exclusão, ele precisa ser tornado nulo, ou seja, atribua o valor 0 (ou em C++ 11). Por "sair do escopo imediatamente após a exclusão" queremos dizer que você exclui o ponteiro bem no final do bloco em que ele foi declarado.

Regra: Defina ponteiros excluídos como 0 (ou nullptr em C++ 11), a menos que eles saiam do escopo imediatamente após serem excluídos.

Operador novo

Ao solicitar memória do sistema operacional, em casos raros ela pode não estar disponível (ou seja, pode não estar disponível).

Por padrão, se o novo operador não funcionou, a memória não foi alocada, então exceção bad_alloc. Se esta exceção não for tratada corretamente (e será, já que ainda não examinamos as exceções e seu tratamento), então o programa simplesmente irá parar de executar (travar) com um erro de exceção não tratada.

Em muitos casos, o processo de lançar uma exceção com o operador new (bem como travar o programa) é indesejável, portanto existe uma forma alternativa do operador new que retorna um ponteiro nulo se a memória não puder ser alocada. Você só precisa adicionar constante std::nothrow entre a nova palavra-chave e o tipo de dados:

int *valor = novo (std::nothrow) int; // o ponteiro de valor se tornará nulo se a alocação dinâmica de uma variável inteira falhar

No exemplo acima, se new não retornar um ponteiro com memória alocada dinamicamente, um ponteiro nulo será retornado.

Desreferenciar também não é recomendado, pois isso levará a resultados inesperados (provavelmente uma falha do programa). Portanto, a prática recomendada é verificar todas as solicitações de alocação de memória para garantir que as solicitações sejam concluídas com êxito e que a memória seja alocada:

int *valor = novo (std::nothrow) int; // solicitação para alocar memória dinâmica para um valor inteiro if (!value) // trata o caso quando new retorna null (ou seja, a memória não está alocada) ( // trata deste caso std::cout<< "Could not allocate memory"; }

Como a não alocação de memória pelo novo operador é extremamente rara, os programadores geralmente esquecem de realizar esta verificação!

Ponteiros nulos e alocação dinâmica de memória

Ponteiros nulos (ponteiros com valor 0 ou nullptr) são especialmente úteis durante o processo de alocação dinâmica de memória. A presença deles como se nos informasse: “Nenhuma memória está alocada para este ponteiro”. E isso, por sua vez, pode ser usado para realizar a alocação de memória condicional:

// Se ptr ainda não tiver memória alocada, então aloque-a if (!ptr) ptr = new int;

Remover o ponteiro nulo não tem efeito. Portanto, o seguinte não é necessário:

se (ptr) excluir ptr;

se(ptr)

excluir ptr;

Em vez disso, você pode simplesmente escrever:

excluir ptr;

Se ptr não for nulo, a variável alocada dinamicamente será excluída. Se o valor do ponteiro for nulo, nada acontecerá.

Vazamento de memória

A memória alocada dinamicamente não tem escopo, ou seja, ele permanece alocado até que seja explicitamente liberado ou até que seu programa termine de ser executado (e o sistema operacional limpe todos os buffers de memória por conta própria). No entanto, os ponteiros usados ​​para armazenar endereços de memória alocados dinamicamente seguem as regras de escopo das variáveis ​​regulares. Essa discrepância pode causar um comportamento interessante. Por exemplo:

void doSomething() ( int *ptr = new int; )

tempo de espera programas. O programa aloca memória do espaço de pilha para variáveis ​​locais. Porém, variáveis ​​locais requerem uma determinação prévia da quantidade de memória alocada para cada situação. Embora o C++ implemente tais variáveis ​​de forma eficiente, elas exigem que o programador saiba antecipadamente quanta memória é necessária para cada situação.

A segunda maneira pela qual o C++ pode armazenar informações é usando um sistema de alocação dinâmica. Neste método, a memória é alocada para informações de uma área de memória livre conforme necessário. A área de memória livre está localizada entre o código do programa com sua área de memória permanente e a pilha (Fig. 24.1). A alocação dinâmica é útil quando você não sabe quantos itens de dados serão processados.


Arroz. 24.1.

À medida que o programa utiliza a área da pilha, ela aumenta para baixo, ou seja, o próprio programa determina a quantidade de memória da pilha. Por exemplo, um programa com um grande número funções recursivas ocupará mais memória de pilha do que um programa sem funções recursivas, já que variáveis ​​locais e endereços de retorno são armazenados em pilhas. Memória para o próprio programa e variáveis ​​globais se destaca por tudo tempo de espera programa e é constante para um ambiente específico.

A memória alocada durante a execução do programa é chamada de dinâmica. Após a seleção dinâmico a memória é retida até que seja explicitamente liberada, o que só pode ser feito usando uma operação especial ou função de biblioteca.

Se a memória dinâmica não for liberada antes do final do programa, ela será liberada automaticamente quando o programa terminar. No entanto, é um sinal de bom estilo de programação liberar explicitamente memória que se tornou desnecessária.

Durante a execução do programa, uma seção de memória dinâmica estará disponível sempre que o ponteiro que endereça esta seção estiver disponível. Assim, são possíveis os seguintes três opções para trabalhar com memória dinâmica, alocado em um determinado bloco (por exemplo, no corpo de uma função não principal).

  • Um ponteiro (para um local de memória dinâmica) é definido como um objeto de memória automática local. Neste caso, a memória alocada não estará disponível ao sair do bloco de localização do ponteiro e deverá ser liberada antes de sair do bloco.
  • O ponteiro é definido como um objeto de memória estática local. A memória dinâmica alocada uma vez em um bloco é acessada por meio de um ponteiro cada vez que o bloco é reinserido. A memória só deve ser liberada quando terminar de usá-la.
  • O ponteiro é um objeto global em relação ao bloco. A memória dinâmica está disponível em todos os blocos onde o ponteiro está "visível". A memória só deve ser liberada quando terminar de usá-la.

Todas as variáveis ​​declaradas no programa estão localizadas em uma área de memória contínua, que é chamada segmento de dados. Tais variáveis ​​não mudam de tamanho durante a execução do programa e são chamadas estático. O tamanho do segmento de dados pode não ser suficiente para acomodar grandes quantidades de informações. A saída para esta situação é usar memória dinâmica. Memória dinâmica- é a memória alocada ao programa para seu funcionamento menos o segmento de dados, a pilha, que contém variáveis ​​locais de sub-rotinas e o próprio corpo do programa.

Ponteiros são usados ​​para trabalhar com memória dinâmica. Com a ajuda deles, é feito acesso a áreas da memória dinâmica chamadas variáveis ​​dinâmicas. Para armazenamento variáveis ​​dinâmicas Uma área especial de memória chamada heap é alocada.

Variáveis ​​Dinâmicas são criados usando funções e operações especiais. Eles existem até o final do programa ou até que a memória alocada para eles seja liberada por meio de funções ou operações especiais. Ou seja, o tempo da vida variáveis ​​dinâmicas– desde o ponto de criação até o final do programa ou até uma explícita liberando memória.

C++ usa duas maneiras de trabalhar com memória dinâmica:

  1. usando as operações new e delete;
  2. uso da família de funções malloc (calloc) (herdada de C).

Trabalhando com memória dinâmica usando as operações new e delete

Na linguagem de programação C++ para alocação dinâmica de memória existem operações novas e de exclusão. Essas operações são usadas para alocar e liberar blocos de memória. A área da memória em que esses blocos estão localizados é chamada memoria livre.

A nova operação permite alocar e disponibilizar uma área livre na memória principal, cujo tamanho corresponde ao tipo de dado identificado pelo nome do tipo.

Sintaxe:

novo TipoNome;

novo TypeName [inicializador];

O valor determinado é inserido na área selecionada inicializador, que é um elemento opcional. Se for bem-sucedido, new retornará o endereço do início da memória alocada. Se uma área do tamanho necessário não puder ser alocada (não há memória), a nova operação retornará um valor de endereço zero (NULL).

A sintaxe para usar a operação é:

Ponteiro = novo TypeName[Inicializador];

A nova operação float aloca um pedaço de memória de 4 bytes. A nova operação int(15) aloca um pedaço de memória de 4 bytes e inicializa esse pedaço com o valor inteiro 15. A sintaxe para usar as operações new e delete envolve o uso de ponteiros. Cada ponteiro deve ser declarado antecipadamente:

digite *NomePonteiro;

Por exemplo:

flutuar *pi; //Declarando uma variável pi pi=new float; //Alocando memória para a variável pi * pi = 2.25; //Atribuindo um valor

Como tipo você pode usar, por exemplo, tipos padrão int, longo, flutuante, duplo, char.

O operador new é mais frequentemente usado para alocar tipos de dados definidos pelo usuário, como estruturas, na memória:

struct Node (char *Nome; int Valor; Node *Next ); Nó *PNó; //ponteiro é declarado PNode = new Node; //a memória é alocada PNode->Name = "Ata"; //os valores são atribuídos PNode->Value = 1; PNode->Próximo = NULL;

Um array pode ser usado como nome de tipo na nova operação:

novoArrayType

Ao alocar memória dinâmica para um array, suas dimensões devem ser totalmente especificadas. Por exemplo:

ptr = new int ;//10 elementos do tipo int ou 40 bytes ptr = new int ;//errado, porque tamanho não determinado

Esta operação permite alocar uma área na memória dinâmica para acomodar um array do tipo apropriado, mas não permite inicializá-lo. Como resultado da execução, a nova operação retornará um ponteiro cujo valor é o endereço do primeiro elemento do array. Por exemplo:

int *n = novo int;

A nova operação aloca uma seção de memória dinâmica suficiente para acomodar um valor do tipo int e grava o endereço do início desta seção na variável n. A memória para a própria variável n (de tamanho suficiente para acomodar o ponteiro) é alocada no estágio de compilação.



gastroguru 2017