Por que os índices negativos de matriz fazem sentido?

14

Eu me deparei com uma experiência estranha em programação C. Considere este código:

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

  printf("%d\n", array1[-1]);
  return 0;
}

Ao compilar e executar isso, não recebo erros ou avisos. Como meu palestrante disse, o índice do array -1acessa outra variável. Ainda estou confuso, por que diabos uma linguagem de programação tem esse recurso? Quero dizer, por que permitir índices de matriz negativos?

Mohammed Fawzan
fonte
2
Embora essa questão seja motivada por C como linguagem de programação concreta, acho que pode ser entendida como uma questão conceitual que é ontópica aqui (se mal).
Raphael
6
@ Rafael Eu discordo e acredito que ele deva pertencer ao SO, de qualquer forma, este é um comportamento indefinido do livro didático (referenciando a memória fora da matriz) e os sinalizadores do compilador adequados devem alertar sobre isso
catraca aberração
Eu concordo com @ratchetfreak. Parece ser uma falha do compilador, pois o intervalo de índice válido é [0, 5]. Tudo o que estiver fora deve ser um erro de compilação / tempo de execução. Geralmente, vetores são casos particulares de funções cujo índice do primeiro elemento depende do usuário. Como o contrato C é que os elementos começam no índice 0, é um erro acessar elementos negativos.
Val
2
@Raphael C tem duas peculiaridades sobre idiomas típicos com matrizes que são importantes aqui. Uma é que C possui sub-matrizes e a referência ao elemento -1de uma sub-matriz é uma maneira perfeitamente válida de se referir ao elemento antes dessa matriz na matriz maior. A outra é que, se o índice for inválido, o programa é inválido, mas na maioria das implementações você terá um mau comportamento silencioso, não um erro fora do intervalo.
Gilles 'SO- stop be evil'
4
@ Gilles Se esse é o ponto da pergunta, isso realmente deveria estar no Stack Overflow .
Raphael

Respostas:

27

A operação de indexação de matriz a[i]ganha seu significado com os seguintes recursos de C

  1. A sintaxe a[i]é equivalente a *(a + i). Portanto, é válido dizer 5[a]para chegar ao 5º elemento de a.

  2. Aritmética do ponteiro diz que, dado um ponteiro pe um número inteiro i, p + i o ponteiro pavançou por i * sizeof(*p)bytes

  3. O nome de uma matriz aé rapidamente convertido em ponteiro para o 0º elemento dea

Com efeito, a indexação de matriz é um caso especial de indexação de ponteiro. Como um ponteiro pode apontar para qualquer lugar dentro de uma matriz, qualquer expressão arbitrária que pareça nãop[-1] está errada pelo exame e, portanto, os compiladores não (não podem) consideram todas essas expressões como erros.

Seu exemplo a[-1] onde aé realmente o nome de uma matriz é realmente inválido. IIRC, é indefinido se houver um valor significativo de ponteiro como resultado da expressão a - 1onde aé conhecido como um ponteiro para o 0º elemento de uma matriz. Portanto, um compilador inteligente pode detectar isso e sinalizá-lo como um erro. Outros compiladores ainda podem ser compatíveis, permitindo que você atire no próprio pé, fornecendo um ponteiro para um slot de pilha aleatório.

A resposta da ciência da computação é:

  • Em C, o []operador é definido em ponteiros, não em matrizes. Em particular, é definido em termos de aritmética de ponteiro e desreferência de ponteiro.

  • Em C, um ponteiro é abstratamente uma tupla (start, length, offset)com a condição que 0 <= offset <= length. A aritmética do ponteiro é essencialmente aritmética elevada no deslocamento, com a ressalva de que, se o resultado da operação violar a condição do ponteiro, é um valor indefinido. Remover a referência de um ponteiro adiciona uma restrição adicional a isso offset < length.

  • C tem uma noção de undefined behaviourque permite que um compilador represente concretamente essa tupla como um número único e não precisa detectar nenhuma violação da condição do ponteiro. Qualquer programa que satisfaça a semântica abstrata estará seguro com a semântica concreta (com perdas). Qualquer coisa que viole a semântica abstrata pode ser, sem comentários, aceito pelo compilador e pode fazer o que quiser com ele.

Hari
fonte
Por favor, tente dar uma resposta geral, não dependendo das idiossincrasias de qualquer linguagem de programação específica.
Rafael
5
@Raphael, a questão foi explicitamente sobre C. Acho que abordou a questão específica do porquê de um compilador C é permitido para compilar uma expressão aparentemente sem sentido dentro da definição de C.
Hari
Perguntas sobre C em particular são offtopic aqui; observe meu comentário sobre a pergunta.
Raphael
4
Eu acredito que o aspecto lingüístico comparativo da questão ainda é útil. Acredito que dei uma descrição bastante saborosa da "ciência da computação" do porquê de uma implementação específica exibir uma semântica concreta específica.
Hari
15

As matrizes são simplesmente dispostas como blocos de memória contíguos. Um acesso de matriz como um [i] é convertido em um acesso ao endereço de localização da memóriaOf (a) + i. Este é o códigoa[-1] é perfeitamente compreensível, simplesmente se refere ao endereço um antes do início da matriz.

Isso pode parecer loucura, mas há muitas razões pelas quais isso é permitido:

  • é caro verificar se o índice i a um [-] está dentro dos limites da matriz.
  • algumas técnicas de programação exploram o fato de que a[-1]é válido. Por exemplo, se eu sei que anão é realmente o início da matriz, mas um ponteiro no meio da matriz, a[-1]simplesmente obtém o elemento da matriz que fica à esquerda do ponteiro.
Dave Clarke
fonte
6
Em outras palavras, provavelmente não deve ser usado. Período. Seu nome é Donald Knuth e você tenta salvar outras 17 instruções? Por todos os meios, vá em frente.
Raphael
Obrigado pela resposta, mas não entendi a idéia. BTW eu vou lê-lo novamente e novamente até que eu entendo .. :)
Mohammed Fawzan
2
@ Rafael: A implementação do modelo de objeto cola usa a posição -1 para armazenar a tabela : piumarta.com/software/cola/objmodel2.pdf . Assim, os campos são armazenados na parte positiva do objeto e a tabela no negativo. Não me lembro dos detalhes, mas acho que tem a ver com consistência.
Dave Clarke
@ DeZéroToxin: Um array é realmente apenas um local na memória, com alguns locais próximos a ele que fazem parte logicamente do array. Mas, na verdade, uma matriz é apenas um ponteiro.
Dave Clarke
1
@Raphael, a[-1]faz todo o sentido para alguns casos de a, neste caso particular, é claro ilegal (mas não pego pelo compilador)
vonbrand
4

Como as outras respostas explicam, esse é um comportamento indefinido em C. Considere que C foi definido (e é usado principalmente) como um "assembler de alto nível". Os usuários de C o valorizam por sua velocidade intransigente, e verificar as coisas em tempo de execução está (principalmente) fora de questão por uma questão de puro desempenho. Algumas construções C que parecem absurdas para pessoas vindas de outras línguas fazem todo sentido em C, assim a[-1]. Sim, nem sempre faz sentido (

vonbrand
fonte
1
Eu gosto desta resposta. Dá uma verdadeira razão pela qual está tudo bem.
Darxsys
3

Pode-se usar esse recurso para escrever métodos de alocação de memória que acessam diretamente a memória. Um desses usos é verificar o bloco de memória anterior usando um índice de matriz negativo para determinar se os dois blocos podem ser mesclados. Usei esse recurso ao desenvolver um gerenciador de memória não volátil.

Theron W Genaux
fonte
2

C não é fortemente digitado. Um compilador C padrão não verifica os limites da matriz. A outra coisa é que uma matriz em C nada mais é do que um bloco contíguo de memória e a indexação inicia em 0; portanto, um índice de -1 é o local de qualquer padrão de bits anteriora[0] .

Outros idiomas exploram índices negativos de uma maneira agradável. No Python, a[-1]retornará o último elemento, a[-2]retornará o penúltimo elemento e assim por diante.

saadtaame
fonte
2
Como os índices fortes de digitação e matriz se relacionam? Existem idiomas com um tipo para naturais onde os índices de matriz devem ser naturais?
Raphael
@ Rafael Até onde eu sei, digitação forte significa que erros de tipo são detectados. Uma matriz é um tipo, IndexOutOfBounds é um erro, portanto, em uma linguagem fortemente tipada, isso será relatado; em C, isso não acontecerá. Foi isso que eu quis dizer.
Saadtaame 27/03
Nas linguagens que eu conheço, os índices de matriz são do tipo int, a[-5]e, geralmente, int i; ... a[i] = ...;são digitados corretamente. Erros de índice são detectados apenas em tempo de execução. Obviamente, um compilador inteligente pode detectar algumas violações.
Raphael
@ Rafael Estou falando sobre o tipo de dados da matriz como um todo, não os tipos de índice. Isso explica por que C permite que os usuários escrevam um [-5]. Sim, -5 é o tipo de índice correto, mas está fora dos limites e é um erro. Não há menção de verificação de tipo de compilação ou tempo de execução na minha resposta.
Saadtaame 27/03
1

Em palavras simples:

Todas as variáveis ​​(incluindo matrizes) em C são armazenadas na memória. Digamos que você tenha 14 bytes de "memória" e inicialize o seguinte:

int a=0;
int array1[6] = {0, 1, 2, 3, 4, 5};

Além disso, considere o tamanho de um int como 2 bytes. Então, hipoteticamente, nos 2 primeiros bytes de memória, o número inteiro a será salvo. Nos próximos 2 bytes, o número inteiro da primeira posição da matriz será salvo (isso significa matriz [0]).

Então, quando você diz que o array [-1] é como se referir ao número inteiro salvo na memória que está logo antes do array [0], que em nosso é, hipoteticamente, o número inteiro a. Na realidade, não é exatamente assim que as variáveis ​​são armazenadas na memória.

Dchris
fonte
0
//:Example of negative index:
//:A memory pool with a heap and a stack:

unsigned char memory_pool[64] = {0};

unsigned char* stack = &( memory_pool[ 64 - 1] );
unsigned char* heap  = &( memory_pool[ 0     ] );

int stack_index =    0;
int  heap_index =    0;

//:reserve 4 bytes on stack:
stack_index += 4;

//:reserve 8 bytes on heap:
heap_index  += 8;

//:Read back all reserved memory from stack:
for( int i = 0; i < stack_index; i++ ){
    unsigned char c = stack[ 0 - i ];
    //:do something with c
};;
//:Read back all reserved memory from heap:
for( int i = 0; i < heap_index; i++ ){
    unsigned char c = heap[ 0 + i ];
    //:do something with c
};;
JMI MADISON
fonte
Bem-vindo ao CS.SE! Estamos procurando respostas que sejam fornecidas com explicações ou uma descrição da leitura. Não somos um site de codificação e não queremos respostas que sejam apenas um bloco de código. Você pode considerar se pode editar sua resposta para fornecer esse tipo de informação. Obrigado!
DW