Como os ponteiros C realmente mapeiam para a memória do array

13

C torna arrays e ponteiros inseparáveis. Você não pode escrever um código eficaz sem compreender o relacionamento deles. Leva tempo para dominar. A recompensa é o controle imediato sobre a memória.

Considere uma configuração básica. Você declara duas matrizes inteiras de tamanho dez.

Compile isso. Ele falha. Você não pode atribuir um array a outro usando o sinal de igual. C não oferece suporte à cópia em massa por meio de operadores de atribuição. Para copiar a em b, você deve percorrer cada elemento.

Você poderia compactar esse loop em uma única linha com um operador de vírgula, mas a legibilidade é prejudicada. A abordagem padrão é usar memcpy de string.h. Ele lida com a cópia em nível de byte com eficiência.

A falha de b=a revela uma regra fundamental. Em C, variáveis ​​de array não são arrays. Eles são ponteiros fixos. a e b são identificadores permanentes para o endereço inicial de seus respectivos blocos de memória. a aponta para a[0]. b aponta para b[0]. Esses endereços são imutáveis. Você não pode redirecionar um nome de variável para um local de memória diferente. É por isso que a atribuição falha. O compilador trata o nome do array como um ponteiro constante.

Como o nome é efetivamente um ponteiro, você pode usar aritmética de ponteiro para acessar dados. É aqui que a sintaxe se torna poderosa.

Aqui, p é uma variável de ponteiro. Você pode atribuir o endereço da matriz a a p. A instrução p=a copia o endereço. Agora p contém o mesmo valor que o nome do array a. Desreferenciar p com *p recupera o primeiro elemento. A saída é zero.

Essa distinção é importante porque variáveis ​​de ponteiro podem ser alteradas. Você pode fazer p apontar para a[1] ou a[5] usando aritmética. O próprio nome do array permanece preso no início. Essa flexibilidade permite que funções iterem pela memória sem copiar grandes estruturas de dados. É a espinha dorsal do desempenho de baixo nível em C.

Compreender este link evita erros sutis. Explica por que algumas funções aceitam ponteiros em vez de sintaxe de array. Esclarece o layout da memória. Ignore-o e você enfrentará falhas de segmentação ou substituições não intencionais de dados. Aprenda e você verá exatamente o que a máquina está fazendo.

A sintaxe pode parecer pouco natural no início. O duplo sentido dos colchetes e asteriscos confunde os iniciantes. Mas uma vez que o modelo funciona, a abstração desaparece. Você fica com endereços e valores. Nada mais.

Vale a pena o esforço mental? O código fica mais rápido. O uso de memória cai. Você obtém acesso direto às operações em nível de hardware. A maioria dos programas C depende

Você pode atribuir uma variável de array diretamente a um ponteiro porque, nos bastidores, eles são essencialmente a mesma coisa.

p=a; funciona não por causa de mágica, mas porque a decai em um ponteiro. Especificamente, aponta para o endereço de memória do primeiro elemento do array (índice 0). Como esse elemento é um número inteiro, a atua como um ponteiro para um único int. Declarar p como um ponteiro inteiro e defini-lo igual a a é apenas permitir que duas variáveis ​​compartilhem o mesmo endereço. Você poderia escrever p=&a[0]; em vez disso. O resultado é idêntico.

Depois que p estiver conectado ao início de a, você ganha mobilidade. Nomes de array como a são constantes; são endereços fixos que não podem ser alterados. Indicadores como p são diferentes. Você pode movê-los usando aritmética de ponteiro.

É aqui que C fica inteligente.

Quando você escreve p++;, o compilador não adiciona apenas um byte ao endereço. Ele sabe que p aponta para um número inteiro. Em vez disso, ele adiciona bytes sizeof(int), pousando exatamente no próximo elemento. Se p apontasse para uma estrutura de 100 bytes de comprimento, p++ avançaria 100 bytes. C cuida da matemática. Você apenas move o ponteiro.

Copiando arrays sem índices

Você pode copiar um array para outro usando esses ponteiros. Em vez de um loop padrão com índices:

Você pode usar ponteiros:

É detalhado. Você pode compactá-lo.

Ou recolha-o em uma única linha.

A precedência do operador aqui é fazer trabalho pesado. * tem precedência maior que ++, mas o pós-incremento acontece depois que o valor é obtido. Então *p++ busca o valor atual e então move o ponteiro. *q++ faz o mesmo para o destino. A tarefa acontece no meio. É compacto. É rápido. Também é fácil interpretar mal se você não estiver prestando atenção.

A zona de perigo

O que acontece se você pressionar p ou q além do final do array?

C não te impede.

O compilador assume que você sabe o que está fazendo. Ele continuará incrementando os ponteiros, copiando dados para a memória que não pertencem ao array. Você pode substituir outras variáveis. Você pode corromper a pilha. Você pode travar o programa mais tarde, em algum lugar não relacionado ao bug.

É por isso que a verificação de limites é manual em C. A linguagem confia em você. Quando você sai da linha, você paga o preço.

Passando Arrays para Funções

Você pode passar arrays para funções de duas maneiras. Eles parecem diferentes, mas se comportam de maneira semelhante.

Considere uma função que imprime um array:

Ou a versão do ponteiro:

O parâmetro nia (número no array) é obrigatório. Matrizes em C não carregam seu tamanho com elas. A função precisa saber quando parar.

Em ambos os casos, você está passando um ponteiro para o início do array, não