Funções Essenciais de Manipulação de Memória em C

  1. A Função memcpy

A função memcpy é uma ferramenta da biblioteca padrão C utilizada para copiar um número específico de bytes de uma área de memória de origem para uma área de memória de destino. Sua assinatura é:

void *memcpy(void *destino, const void *origem, size_t num_bytes);

O parâmetro destino é um ponteiro para o bloco de memória onde os dados serão gravados, origem é um ponteiro para o bloco de memória de onde os dados serão lidos, e num_bytes especifica a quantidade de bytes a serem copiados. É crucial notar que memcpy pressupõe que as regiões de memória de origem e destino não se sobrepõem. Se houver qualquer sobreposição, o comportamento da função é indefinido, o que pode levar a erros difíceis de depurar.

Considere o seguinte exemplo para ilustrar seu uso:

#include <stdio.h>
#include <string.h> // Necessário para memcpy
#include <stddef.h> // Necessário para size_t

int main() {
    int dados_fonte[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int dados_alvo[10] = {0}; // Inicializa todos os elementos com zero

    // Copia os primeiros 5 inteiros (5 * sizeof(int) bytes)
    memcpy(dados_alvo, dados_fonte, 5 * sizeof(int));

    printf("Conteúdo do array de destino após memcpy: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", dados_alvo[i]);
    }
    printf("\n"); // Saída esperada: 10 20 30 40 50 0 0 0 0 0 

    return 0;
}

Implementação Simplificada de memcpy

Para compreender o mecanismo subjacente de memcpy, podemos desenvolver uma versão própria. Uma vez que a aritmética de ponteiros com void* não é permitida diretamente em C (pois o compilador não sabe o "tamanho" do tipo apontado), convertemos os ponteiros para char*. Isso nos permite manipular a memória byte a byte, garantindo que qualquer tipo de dado possa ser copiado com precisão.

#include <stddef.h> // Para size_t
#include <stdio.h>  // Para printf

void* minha_copia_mem(void* destino, const void* origem, size_t bytes_para_copiar) {
    char* ptr_destino = (char*)destino;
    const char* ptr_origem = (const char*)origem;

    for (size_t i = 0; i < bytes_para_copiar; ++i) {
        ptr_destino[i] = ptr_origem[i];
    }
    return destino; // Retorna o ponteiro para o destino, como a função padrão
}

int main() {
    int valores_originais[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int valores_novos[10] = {0};

    minha_copia_mem(valores_novos, valores_originais, 5 * sizeof(int));

    printf("Array de destino após minha_copia_mem: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", valores_novos[i]);
    }
    printf("\n"); // Saída esperada: 1 2 3 4 5 0 0 0 0 0 

    return 0;
}

  1. A Função memmove

Ao contrário da memcpy, a função memmove foi projetada especificamente para lidar com cópias de memória onde as regiões de origem e destino podem se sobrepor. Ela garante que a cópia seja realizada corretamente, mesmo que partes dos blocos de memória sejam as mesmas. A sua assinatura é idêntica à de memcpy:

void *memmove(void *destino, const void *origem, size_t num_bytes);

Implementação Personalizada de memmove

A inteligência da memmove reside na sua capacidade de escolher a direção de cópia (para frente ou para trás) para evitar a corrupção de dados. A estratégia depende da posição relativa dos ponteiros de destino e origem:

  • Se o endereço de destino for anterior ao endereço de origem (destino < origem), a cópia é realizada progressivamente (do início para o fim). Isso previne que dados na origem sejam sobrescritos antes de serem lidos.
  • Se o endereço de destino for igual ou posterior ao endereço de origem (destino >= origem), a cópia é realizada regressivamente (do fim para o início). Essa abordagem garante que os dados originais sejam lidos antes que a região de destino, que pode sobrepor parte da origem, os apague.

Vamos exemplificar a lógica da cópia regressiva: se estamos copiando N bytes de origem para destino, e destino está à frente de origem, iniciar a cópia do último byte (origem + N - 1 para destino + N - 1) e mover para trás (decrementando os índices) assegura que os bytes mais à direita da origem sejam copiados antes que os bytes mais à esquerda da origem sejam sobrescritos pelo início do destino.

#include <stddef.h> // Para size_t
#include <stdio.h>  // Para printf

void* minha_movimentacao_mem(void* destino, const void* origem, size_t tamanho) {
    char* p_destino = (char*)destino;
    const char* p_origem = (const char*)origem;
    void* valor_retorno = destino; // Salva o endereço original do destino

    if (p_destino < p_origem) {
        // Cópia progressiva (do início para o fim)
        // Destino está antes da origem, sem risco de sobrescrever dados necessários.
        for (size_t i = 0; i < tamanho; ++i) {
            p_destino[i] = p_origem[i];
        }
    } else {
        // Cópia regressiva (do fim para o início)
        // Destino está depois ou se sobrepõe à origem.
        // Começar pelo final evita que dados da origem sejam sobrescritos antes de serem copiados.
        for (size_t i = tamanho; i > 0; --i) {
            p_destino[i-1] = p_origem[i-1];
        }
    }
    return valor_retorno;
}

int main() {
    int serie_numeros[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    printf("Array original: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", serie_numeros[i]);
    }
    printf("\n");

    // Exemplo de sobreposição (destino > origem): copiar {1,2,3,4,5} para o índice 2
    // A chamada copia 5 inteiros de serie_numeros para serie_numeros + 2.
    // Isso requer cópia regressiva (do final para o início).
    minha_movimentacao_mem(serie_numeros + 2, serie_numeros, 5 * sizeof(int)); 

    printf("Após movimentação (serie_numeros+2 <- serie_numeros, 5 inteiros): ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", serie_numeros[i]);
    }
    printf("\n"); // Saída esperada: 1 2 1 2 3 4 5 8 9 10

    // Outro exemplo (destino < origem): copiar {4,5,6,7,8} para o início do array
    // A chamada copia 5 inteiros de serie_numeros + 3 para serie_numeros.
    // Isso requer cópia progressiva (do início para o fim).
    int outra_serie[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    printf("Array original para o segundo teste: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", outra_serie[i]);
    }
    printf("\n");
    minha_movimentacao_mem(outra_serie, outra_serie + 3, 5 * sizeof(int));
    printf("Após movimentação (outra_serie <- outra_serie+3, 5 inteiros): ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", outra_serie[i]);
    }
    printf("\n"); // Saída esperada: 4 5 6 7 8 6 7 8 9 10

    return 0;
}

  1. A Função memset

A função memset é usada para preencher um bloco de memória com um valor de byte específico. É particularmente útil para inicializar estruturas de dados ou para limpar áreas de memória, preenchendo-as com um valor padrão (como zero). Sua assinatura é:

void *memset(void *ponteiro, int valor, size_t num_bytes);

Aqui, ponteiro é o endereço inicial da memória a ser preenchida, valor é o byte a ser usado (passado como int, mas internamente convertido para unsigned char para a operação), e num_bytes é o número de bytes a serem preenchidos a partir de ponteiro.

É crucial compreender que memset opera em bytes individuais. O valor fornecido é interpretado como um único byte, que será repetido por todo o bloco de memória.

Exemplo de uso com caracteres:

#include <stdio.h>
#include <string.h> // Necessário para memset

int main() {
    char buffer_msg[20] = "Mensagem de teste.";
    printf("Conteúdo original: %s\n", buffer_msg);

    // Preenche os primeiros 7 bytes do buffer com o caractere 'X'
    memset(buffer_msg, 'X', 7); 
    printf("Após preenchimento com 'X': %s\n", buffer_msg); // Saída esperada: XXXXXXXem de teste.

    return 0;
}

É importante ter atenção ao utilizar memset com tipos de dados que ocupam mais de um byte, como int. O valor fornecido é repetido byte a byte, e não como o valor completo do tipo. Por exemplo, tentar preencher um array de inteiros com o "valor intiero 1" não fará com que cada inteiro tenha o valor 1.

O seguinte exemplo ilustra um uso comum que pode levar a resultados inesperados se o funcionamento de memset não for totalmente compreendido:

#include <stdio.h>
#include <string.h> // Necessário para memset

int main() {
    int array_inteiros[5]; // Um array de 5 inteiros
    
    // Tenta preencher cada inteiro com o valor 1.
    // Na verdade, preenche cada byte do array com o byte '1'.
    memset(array_inteiros, 1, sizeof(array_inteiros)); 

    printf("Conteúdo do array após memset com byte '1':\n");
    for (int i = 0; i < 5; i++) {
        // Se um 'int' tem 4 bytes, e cada byte é '00000001' (decimal 1),
        // o valor do 'int' resultante será 0x01010101, que é 16843009 em decimal.
        printf("array_inteiros[%d] = %d\n", i, array_inteiros[i]);
    }
    // Saída esperada: 16843009 para cada elemento (assumindo int de 4 bytes)

    return 0;
}

  1. A Função memcmp

A função memcmp é empregada para comparar dois blocos de memória, byte a byte. A comparação prossegue até que um número especificado de bytes tenha sido comparado ou até que uma diferença seja encontrada. Sua assinatura é:

int memcmp(const void *ponteiro1, const void *ponteiro2, size_t num_bytes);

Esta função compara os primeiros num_bytes bytes apontados por ponteiro1 com os apontados por ponteiro2. O valor de retorno indica o resultado da comparação:

  • Um valor negativo (menor que zero) se o primeiro byte diferente em ponteiro1 tiver um valor menor que o byte correspondente em ponteiro2.
  • Zero (0) se os blocos de memória forem idênticos pelos num_bytes especificados.
  • Um valor positivo (maior que zero) se o primeiro byte diferente em ponteiro1 tiver um valor maior que o byte correspondente em ponteiro2.

É importante ressaltar que memcmp compara os valores dos bytes como se fossem do tipo unsigned char. Essa característica é relevante ao lidar com caracteres ASCII estendidos ou dados binários, onde a interpretação de sinal pode alterar o resultado da comparação.

Exemplo de comparação de blocos de memória:

#include <stdio.h>
#include <string.h> // Necessário para memcmp

int main() {
    char bloco_um[] = "Exemplo ABC 123";
    char bloco_dois[] = "Exemplo XYZ 123";
    char bloco_tres[] = "Exemplo ABC 123";
    
    int resultado_comparacao;

    // Comparando bloco_um e bloco_dois
    // (A comparação considerará os tamanhos, incluindo o '\0' terminador se sizeof for usado)
    resultado_comparacao = memcmp(bloco_um, bloco_dois, sizeof(bloco_um));

    if (resultado_comparacao > 0) {
        printf("'%s' é maior que '%s'.\n", bloco_um, bloco_dois);
    } else if (resultado_comparacao < 0) {
        printf("'%s' é menor que '%s'.\n", bloco_um, bloco_dois); // Saída esperada, 'A' < 'X'
    } else {
        printf("'%s' é igual a '%s'.\n", bloco_um, bloco_dois);
    }

    // Comparando bloco_um e bloco_tres (idênticos)
    resultado_comparacao = memcmp(bloco_um, bloco_tres, sizeof(bloco_um));

    if (resultado_comparacao == 0) {
        printf("'%s' é igual a '%s'.\n", bloco_um, bloco_tres); // Saída esperada
    } else {
        printf("'%s' não é igual a '%s'.\n", bloco_um, bloco_tres);
    }
    
    // Comparando apenas uma parte dos blocos (ex: os primeiros 8 bytes)
    resultado_comparacao = memcmp(bloco_um, bloco_dois, 8); // "Exemplo " tem 8 caracteres

    if (resultado_comparacao == 0) {
        printf("Os primeiros 8 bytes de '%s' e '%s' são iguais.\n", bloco_um, bloco_dois); // Saída esperada
    } else {
        printf("Os primeiros 8 bytes de '%s' e '%s' são diferentes.\n", bloco_um, bloco_dois);
    }

    return 0;
}

Tags: C Language Memory Management memcpy memmove memset

Publicado em 9-24 20:18