Eu estou trabalhando com um compilador para um chip DSP que deliberadamente gera código que acessa um após o final de uma matriz fora do código C que não!
Isso ocorre porque os loops são estruturados para que o final de uma iteração prefira alguns dados para a próxima iteração. Portanto, o dado pré-buscado no final da última iteração nunca é realmente usado.
Escrever código C assim invoca um comportamento indefinido, mas isso é apenas uma formalidade de um documento de normas que se preocupa com a portabilidade máxima.
Mais frequentemente, um programa que acessa fora dos limites não é otimizado de maneira inteligente. É simplesmente buggy. O código busca algum valor de lixo e, diferentemente dos loops otimizados do compilador mencionado acima, o código usa o valor em cálculos subsequentes, corrompendo-os.
Vale a pena capturar bugs como esse e, portanto, vale a pena tornar o comportamento indefinido apenas por esse motivo: para que o tempo de execução possa produzir uma mensagem de diagnóstico como "array overrun na linha 42 de main.c".
Em sistemas com memória virtual, uma matriz pode ser alocada de modo que o endereço a seguir esteja em uma área não mapeada da memória virtual. O acesso bombardeará o programa.
Como um aparte, observe que em C temos permissão para criar um ponteiro que esteja um após o final de uma matriz. E esse ponteiro deve comparar mais do que qualquer ponteiro ao interior de uma matriz. Isso significa que uma implementação C não pode colocar uma matriz no final da memória, onde o endereço mais um seria contornado e pareceria menor que outros endereços na matriz.
No entanto, o acesso a valores não inicializados ou fora dos limites às vezes é uma técnica de otimização válida, mesmo que não seja maximamente portátil. É por exemplo, por que a ferramenta Valgrind não relata acessos a dados não inicializados quando esses acessos acontecem, mas somente quando o valor é usado posteriormente de alguma maneira que possa afetar o resultado do programa. Você obtém um diagnóstico como "ramificação condicional em xxx: nnn depende de valor não inicializado" e às vezes pode ser difícil rastrear sua origem. Se todos esses acessos fossem capturados imediatamente, haveria muitos falsos positivos decorrentes do código otimizado do compilador, bem como do código otimizado manualmente.
Falando nisso, eu estava trabalhando com algum codec de um fornecedor que estava emitindo esses erros quando portado para Linux e executado no Valgrind. Mas o fornecedor me convenceu de que apenas vários bitso valor usado realmente veio da memória não inicializada e esses bits foram cuidadosamente evitados pela lógica. Somente os bons bits do valor estavam sendo usados e o Valgrind não tem a capacidade de rastrear o bit individual. O material não inicializado veio da leitura de uma palavra após o final de um fluxo de bits de dados codificados, mas o código sabe quantos bits estão no fluxo e não usará mais bits do que realmente são. Como o acesso além do final da matriz de fluxo de bits não causa nenhum dano à arquitetura DSP (não há memória virtual após a matriz, nenhuma porta mapeada na memória e o endereço não quebra), é uma técnica de otimização válida.
"Comportamento indefinido" não significa muito, porque, de acordo com a ISO C, simplesmente incluir um cabeçalho que não está definido no padrão C ou chamar uma função que não está definida no próprio programa ou no padrão C são exemplos de indefinidos. comportamento. Comportamento indefinido não significa "não definido por ninguém no planeta" apenas "não definido pelo padrão ISO C". Mas, claro, o comportamento às vezes indefinido realmente é absolutamente não definido por qualquer pessoa.