Indexação de ponteiro

11

Atualmente, estou lendo um livro intitulado "Receitas Numéricas em C". Neste livro, o autor detalha como certos algoritmos funcionam inerentemente melhor se tivéssemos índices começando com 1 (não sigo inteiramente o argumento dele e esse não é o objetivo deste post), mas C sempre indexa suas matrizes começando com 0 Para contornar isso, ele sugere simplesmente diminuir o ponteiro após a alocação, por exemplo:

float *a = malloc(size);
a--;

Isso, ele diz, efetivamente fornecerá um ponteiro com um índice começando com 1, que será liberado com:

free(a + 1);

Até onde eu sei, esse comportamento é indefinido pelo padrão C. Aparentemente, este é um livro altamente respeitável dentro da comunidade HPC, então não quero simplesmente desconsiderar o que ele está dizendo, mas simplesmente decrementar um ponteiro fora do intervalo alocado parece altamente superficial para mim. Esse comportamento é "permitido" em C? Testei-o usando o gcc e o icc, e ambos os resultados parecem indicar que não estou me preocupando com nada, mas quero ser absolutamente positivo.

wolfPack88
fonte
3
qual padrão C você se refere? Eu pergunto porque por minha lembrança, "Numerical Recipes in C" foi publicado em 1990, nos tempos antigos de K & R e talvez ANSI C
mosquito
2
Pergunta relacionada SO: stackoverflow.com/questions/10473573/...
dan04
3
"Eu testei usando o gcc e o icc, e ambos os resultados parecem indicar que não estou me preocupando com nada, mas quero ser absolutamente positivo". Nunca assuma que, como o seu compilador permite, a linguagem C permite. A menos, claro, que você esteja bem com seu código quebrado no futuro.
Doval
5
Sem querer ser irreverente, as "Receitas Numéricas" geralmente são consideradas um livro útil, rápido e sujo, não um paradigma de desenvolvimento de software ou análise numérica. Confira o artigo da Wikipedia sobre "Receitas Numéricas" para um resumo de algumas das críticas.
Charles E. Grant
1
Como um aparte, aqui é por isso que o índice de zero: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Russell Borogove

Respostas:

16

Você está certo nesse código, como

float a = malloc(size);
a--;

gera comportamento indefinido, de acordo com o padrão ANSI C, seção 3.3.6:

A menos que o operando do ponteiro e o resultado aponte para um membro do mesmo objeto de matriz ou um após o último membro do objeto de matriz, o comportamento é indefinido

Para códigos como esse, a qualidade do código C no livro (quando eu o usei no final dos anos 90) não era considerada muito alta.

O problema com o comportamento indefinido é que, independentemente do resultado que o compilador produz, esse resultado é por definição correto (mesmo que seja altamente destrutivo e imprevisível).
Felizmente, pouquíssimos compiladores se esforçam para realmente causar um comportamento inesperado nesses casos, e a mallocimplementação típica em máquinas usadas para HPC possui alguns dados de contabilidade imediatamente antes do endereço retornado; portanto, o decréscimo normalmente indicava esses dados de contabilidade. Não é uma boa idéia escrever lá, mas apenas criar o ponteiro é inofensivo para esses sistemas.

Lembre-se de que o código pode quebrar quando o ambiente de tempo de execução for alterado ou quando o código for portado para um ambiente diferente.

Bart van Ingen Schenau
fonte
4
Exatamente, é possível em uma arquitetura multibancário que o malloc possa fornecer o 0º endereço em um banco e diminuí-lo pode causar uma interceptação de CPU com um fluxo insuficiente para um.
Vality 8/07/2014
1
Eu discordo que isso é "sorte". Eu acho que seria muito melhor se os compiladores emitissem código que falhasse imediatamente sempre que você invocasse um comportamento indefinido.
David Conrad
4
@ DavidConrad: Então C não é o idioma para você. Grande parte do comportamento indefinido em C não pode ser facilmente detectado ou apenas com um grave impacto no desempenho.
Bart van Ingen Schenau
Eu estava pensando em adicionar "com uma opção de compilador". Obviamente, você não gostaria disso para um código otimizado. Mas, você está certo, e foi por isso que desisti de escrever C há dez anos.
David Conrad
@BartvanIngenSchenau dependendo do que você quer dizer com 'grave desempenho atingido', há execução simbólica para C (por exemplo, clang + klee) e também para sanatizadores (asan, tsan, ubsan, valgrind etc.), que tendem a ser muito úteis para depuração.
Maciej Piechotka
10

Oficialmente, é um comportamento indefinido ter um ponto de ponteiro fora da matriz (exceto um após o final), mesmo que nunca seja desreferenciado .

Na prática, se o seu processador tiver um modelo de memória simples (ao contrário de estranhos como x86-16 ), e se o compilador não fornecer um erro de tempo de execução ou otimização incorreta se você criar um ponteiro inválido, o código funcionará bem.

dan04
fonte
1
Isso faz sentido. Infelizmente, são dois if's demais para o meu gosto.
amigos estão dizendo sobre wolfpack88
3
O último ponto é IMHO o mais problemático. Como os compiladores atualmente não deixam apenas acontecer o que a plataforma "naturalmente" faz no caso do UB, mas os otimizadores estão explorando -a agressivamente , eu não brincaria com isso de ânimo leve.
Matteo Italia
3

Primeiro, é um comportamento indefinido. Atualmente, alguns compiladores de otimização ficam muito agressivos com comportamentos indefinidos. Por exemplo, como a-- nesse caso é um comportamento indefinido, o compilador pode decidir salvar uma instrução e um ciclo do processador e não diminuir a. O que é oficialmente correto e legal.

Ignorando isso, você pode subtrair 1, 2 ou 1980. Por exemplo, se eu tiver dados financeiros para os anos de 1980 a 2013, posso subtrair 1980. Agora, se usarmos float * a = malloc (tamanho); certamente uma constante k grande tal que a - k é um ponteiro nulo. Nesse caso, realmente esperamos que algo dê errado.

Agora dê uma grande estrutura, digamos, um megabyte de tamanho. Aloque um ponteiro p apontando para duas estruturas. p-1 pode ser um ponteiro nulo. p-1 pode ser contornado (se uma estrutura for um megabyte e o bloco malloc for 900 KB desde o início do espaço de endereço). Portanto, poderia ser sem qualquer malícia do compilador que p - 1> p. As coisas podem ficar interessantes.

gnasher729
fonte
1

... 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 doubles mapeados em 0xdeadbee7e 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 Paponta para o i-ésimo elemento de um objeto de matriz, as expressões (P)+N(equivalentemente N+(P)) e (P)-N (onde Ntem 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ê.

Blrfl
fonte
8
-1 Uma definição de 'permitido' que inclui o código que o padrão C declara como gerador de resultados indefinidos não é útil.
Pete Kirkham
Outros apontaram que é um comportamento indefinido, então você não deve dizer que é "permitido". No entanto, a sugestão de alocar um elemento extra não utilizado 0 é boa.
200_success
Isso realmente não está certo, observe pelo menos que isso é proibido pelo padrão C.
Vality 8/07/2014
@PeteKirkham: Eu discordo. Veja o adendo à minha resposta.
Blrfl
4
@Blrfl 6.5.6 da norma ISO C11 afirma no caso de adicionar um número inteiro a um ponteiro: "Se o operando do ponteiro e o resultado apontarem para elementos do mesmo objeto de matriz ou um após o último elemento do objeto de matriz , a avaliação não produzirá um estouro; caso contrário, o comportamento será indefinido. "
Vality 8/07/2014