Fragmentação de memória é o mesmo conceito que fragmentação de disco: refere-se ao desperdício de espaço, porque as áreas em uso não são compactadas o suficiente.
Suponha que, para um exemplo simples de brinquedo, você tenha dez bytes de memória:
| | | | | | | | | | |
0 1 2 3 4 5 6 7 8 9
Agora vamos alocar três blocos de três bytes, nomes A, B e C:
| A | A | A | B | B | B | C | C | C | |
0 1 2 3 4 5 6 7 8 9
Agora desaloque o bloco B:
| A | A | A | | | | C | C | C | |
0 1 2 3 4 5 6 7 8 9
Agora, o que acontece se tentarmos alocar um bloco de quatro bytes D? Bem, temos quatro bytes de memória livre, mas não temos quatro contíguos bytes de memória livre, portanto não podemos alocar D! Esse é um uso ineficiente da memória, porque deveríamos ter conseguido armazenar D, mas não conseguimos. E não podemos mover C para liberar espaço, porque muito provavelmente algumas variáveis em nosso programa estão apontando para C e não podemos encontrar e alterar automaticamente todos esses valores.
Como você sabe que é um problema? Bem, o maior sinal é que o tamanho da memória virtual do seu programa é consideravelmente maior que a quantidade de memória que você está realmente usando. Em um exemplo do mundo real, você teria muito mais que dez bytes de memória; portanto, D seria alocado a partir de um byte 9 e os bytes 3-5 permaneceriam sem uso, a menos que você alocasse algo com três bytes de comprimento ou menos.
Neste exemplo, 3 bytes não são muito desperdiçados, mas considere um caso mais patológico em que duas alocações de um par de bytes estão, por exemplo, com dez megabytes de memória e você precisa alocar um bloco de tamanho 10 megabytes + 1 byte. Você precisa solicitar ao sistema operacional mais de dez megabytes de memória virtual para fazer isso, mesmo que você tenha apenas um byte de espaço suficiente.
Como você evita isso? Os piores casos tendem a surgir quando você cria e destrói objetos pequenos com freqüência, pois isso tende a produzir um efeito de "queijo suíço" com muitos objetos pequenos separados por muitos buracos pequenos, tornando impossível alocar objetos maiores nesses buracos. Quando você sabe que vai fazer isso, uma estratégia eficaz é pré-alocar um grande bloco de memória como um pool para seus pequenos objetos e, em seguida, gerenciar manualmente a criação dos pequenos objetos dentro desse bloco, em vez de permitir o alocador padrão lida com isso.
Em geral, quanto menos alocações você fizer, menos provável será a fragmentação da memória. No entanto, a STL lida com isso com bastante eficácia. Se você tem uma string que está usando a totalidade de sua alocação atual e acrescenta um caractere a ela, ela não é re-alocada para o comprimento atual mais um, mas dobra seu comprimento. Essa é uma variação da estratégia "pool para alocações pequenas e frequentes". A string está capturando um grande pedaço de memória para poder lidar eficientemente com pequenos aumentos de tamanho repetidos sem fazer realocações pequenas e repetidas. Na verdade, todos os contêineres STL fazem esse tipo de coisa; portanto, geralmente você não precisa se preocupar muito com a fragmentação causada pela realocação automática de contêineres STL.
Embora, é claro, os contêineres STL não agrupem memória entre si, por isso, se você criar muitos contêineres pequenos (em vez de alguns contêineres que são redimensionados com frequência), talvez seja necessário se preocupar em impedir a fragmentação da mesma maneira que você seria para qualquer objeto pequeno criado com freqüência, STL ou não.