Я работаю с компилятором для чипа DSP, который намеренно генерирует код, который обращается к концу массива из кода C, который не имеет!
Это потому, что циклы структурированы так, что конец итерации предварительно выбирает некоторые данные для следующей итерации. Таким образом, данные, предварительно выбранные в конце последней итерации, фактически никогда не используются.
Подобный код на C вызывает неопределенное поведение, но это лишь формальность из документа стандартов, который касается максимальной переносимости.
Чаще всего это не так, программа, которая выходит за границы, не умно оптимизирована. Это просто глючит. Код извлекает некоторое мусорное значение и, в отличие от оптимизированных циклов вышеупомянутого компилятора, код затем использует значение в последующих вычислениях, тем самым разрушая их.
Стоит обнаруживать подобные ошибки, и поэтому стоит сделать поведение неопределенным даже по одной этой причине: во время выполнения может появиться диагностическое сообщение типа «переполнение массива в строке 42 файла main.c».
В системах с виртуальной памятью может случиться так, что массив будет выделен так, что следующий адрес находится в не отображенной области виртуальной памяти. Доступ будет бомбить программу.
Кроме того, обратите внимание, что в C нам разрешено создавать указатель, который находится за концом массива. И этот указатель должен сравнивать больше любого указателя на внутреннюю часть массива. Это означает, что реализация C не может поместить массив прямо в конец памяти, где адрес «один плюс» будет оборачиваться и выглядеть меньше, чем другие адреса в массиве.
Тем не менее, доступ к неинициализированным или выходящим за пределы значениям иногда является допустимым методом оптимизации, даже если он не является максимально переносимым. Именно поэтому инструмент Valgrind не сообщает о доступе к неинициализированным данным, когда такой доступ происходит, а только тогда, когда значение впоследствии используется каким-либо образом, что может повлиять на результат программы. Вы получаете диагностику типа «условная ветвь в xxx: nnn зависит от неинициализированного значения», и иногда бывает трудно отследить, где оно происходит. Если бы все такие обращения были немедленно зафиксированы, было бы много ложных срабатываний, возникающих из оптимизированного компилятором кода, а также из правильно оптимизированного вручную кода.
Говоря об этом, я работал с некоторым кодеком от поставщика, который выдавал эти ошибки при портировании на Linux и запуске под Valgrind. Но продавец убедил меня, что только несколько битиспользуемого значения на самом деле происходило из неинициализированной памяти, и эти биты тщательно избегались логикой. Использовались только хорошие биты значения, и Valgrind не может отследить отдельный бит. Неинициализированный материал был получен при чтении слова после конца потока битов закодированных данных, но код знает, сколько битов находится в потоке, и не будет использовать больше битов, чем есть на самом деле. Поскольку доступ за концом массива битового потока не наносит вреда архитектуре DSP (после массива нет виртуальной памяти, нет портов с отображением в памяти и адрес не переносится), это допустимый метод оптимизации.
«Неопределенное поведение» на самом деле ничего не значит, потому что согласно ISO C простое включение заголовка, который не определен в стандарте C, или вызов функции, которая не определена в самой программе или стандарте C, являются примерами неопределенного поведение. Неопределенное поведение не означает «не определено никем на планете», просто «не определено стандартом ISO C». Но, конечно же , иногда неопределенное поведение действительно является абсолютно не определен никем.