... simplesmente decrementar um ponteiro fora do intervalo alocado parece altamente superficial para mim. Esse comportamento é "permitido" em C?
Permitido? Sim. Boa ideia? Normalmente não.
C é uma abreviação de linguagem assembly, e na linguagem assembly não há ponteiros, apenas endereços de memória. Os ponteiros de C são endereços de memória que têm um comportamento lateral de aumentar ou diminuir pelo tamanho do que apontam quando submetidos à aritmética. Isso simplifica o seguinte da perspectiva da sintaxe:
double *p = (double *)0xdeadbeef;
--p; // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];
Matrizes não são realmente uma coisa em C; são apenas indicadores de intervalos contíguos de memória que se comportam como matrizes. O []
operador é uma abreviação para fazer aritmética e desreferenciamento de ponteiros, o que a[x]
realmente significa *(a + x)
.
Existem razões válidas para fazer o acima, como alguns dispositivos de E / S tendo alguns double
s mapeados em 0xdeadbee7
e 0xdeadbeef
. Muito poucos programas precisariam fazer isso.
Ao criar o endereço de algo, como usando o &
operador ou a chamada malloc()
, você deseja manter intacto o ponteiro original para saber que o que ele aponta é realmente válido. Decrementar o ponteiro significa que um pouco de código incorreto pode tentar desreferenciá-lo, obtendo resultados errôneos, derrubando algo ou, dependendo do seu ambiente, cometendo uma violação de segmentação. Isso é especialmente verdadeiro com o malloc()
fato de você colocar o fardo em quem está ligando free()
para lembrar de passar o valor original e não uma versão alterada que fará com que todos os pedaços se soltem.
Se você precisar de matrizes baseadas em 1 em C, poderá fazê-lo com segurança às custas de alocar um elemento adicional que nunca será usado:
double *array_create(size_t size) {
// Wasting one element, so don't allow it to be full-sized
assert(size < SIZE_MAX);
return malloc((size+1) * sizeof(double));
}
inline double array_index(double *array, size_t index) {
assert(array != NULL);
assert(index >= 1); // This is a 1-based array
return array[index];
}
Observe que isso não faz nada para proteger contra exceder o limite superior, mas isso é fácil de lidar.
Termo aditivo:
Alguns capítulos e versos do rascunho da C99 (desculpe, é só isso que posso vincular):
§6.5.2.1.1 diz que a segunda expressão ("outro") usada com o operador subscrito é do tipo inteiro. -1
é um número inteiro e isso torna p[-1]
válido e, portanto, também torna o ponteiro &(p[-1])
válido. Isso não implica que o acesso à memória nesse local produza um comportamento definido, mas o ponteiro ainda é um ponteiro válido.
§6.5.2.2 diz que o operador de subscrito da matriz é avaliado como o equivalente a adicionar o número do elemento ao ponteiro, portanto p[-1]
é equivalente a *(p + (-1))
. Ainda válido, mas pode não produzir um comportamento desejável.
§6.5.6.8 diz (ênfase minha):
Quando uma expressão que possui o tipo inteiro é adicionada ou subtraída de um ponteiro, o resultado tem o tipo do operando do ponteiro.
... se a expressão P
aponta para o i
-ésimo elemento de um objeto de matriz, as expressões (P)+N
(equivalentemente N+(P)
) e (P)-N
(onde N
tem o valor n
) apontam para, respectivamente, os i+n
-ésimos e
i−n
-ésimos elementos do objeto de matriz, desde que existam .
Isso significa que os resultados da aritmética do ponteiro precisam apontar para um elemento em uma matriz. Não diz que a aritmética deve ser feita de uma só vez. Portanto:
double a[20];
// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];
double *p = a - 1; // This is just a pointer. No dereferencing.
double e = p[0]; // Does not point at any element of a; behavior is undefined.
double f = p[1]; // Points at element 0 of a; behavior is defined.
Eu recomendo fazer as coisas dessa maneira? Não, e minha resposta explica o porquê.