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?


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.


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.

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 (


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.


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.


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.


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
};;

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