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.


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.


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.


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 há 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.


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ê.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.