Автор: (c)Крис Касперски ака мыщъх
Отлаживал как-то мыщъх одну свою программу, написанную на Си и периодически делающую из чисел винегрет или выдающую критическую ошибку Access violation при трудно воспроизводимых обстоятельствах. Тщательная проверка исходного текста "глазами" ровным счетом ничего не дала. Программа продолжала выпендриваться, сроки сдачи проекта поджимали, дедлайн нависал над головой дамокловым мечом, мыщъх нервничал, много курил, нервничал, закидывался ноотропами, не спал ночами, высаживался на жуткую измену, а глубоко укоренившийся баг игнорировал всякие попытки вытащить его из норы.
Под конец подозрения пали на компилятор и мыщъх, переключивший отладчик в ассемблерный режим, начал шаг за шагом исследовать каждую строчку программы, пока не вышел на конструкцию, компилирующуюся не так, как предполагалась (читай: подсказывал здравый смысл и мои познания языка Си).
Виновницей оказалась мерзопакостная конструкция типа "*p[a]++", которая вопреки логике увеличивает отнюдь не содержимое ячейки, на которую указывает "*(p + a)", а значение самого указателя p, то есть транслируется в следующий ассемблерный код:
Листинг 1. Ассемблерный код в который транслируется *p[a]++.
Специально написанный для этого дела демонстрационный пример (см. листинг 3) в отладчике выглядел так:
Рисунок 1. Откомпилированная конструкция *p[a]++ под лупой отладчика.
В то время, как ожидаемый вариант трансляции должен был выглядеть так:
Листинг 2. Ожидаемый вариант трансляции конструкции *p[a]++.
Мыщъх решил проблему очень просто - явно навязав свое намерение компилятору путем расстановки скобок: "(*p)[a]++". Аналогичного результата было можно достичь заменой оператора "++" на оператор "+=" и тогда коварная конструкция принимала вид "*p[a] += 1".
Выгнав бага из его норы, мыщъх решил провести широкомасштабные археологические раскопки, чтобы добраться до смысла происходящего. Причастность компилятора была отметена сразу, как только остальные компиляторы выдали идентичный результат. Значит, собака зарыта вовсе не в компиляторе, а в самом языке Си.
Странно. Очень странно. Ведь основное кредо Си - краткость. И тут... вдруг такое расточительство! Ведь, чтобы использовать оператор "*" необходимо расставлять скобки, а это - целых два нажатия на клаву. Зачем? Может быть, есть такие ситуации, где именно такой расклад приоритетов дает выигрыш? Вообще: о чем думали в этот момент разработчики языка? В доступных мне книжках никаких вразумительных объяснений мыщъх так и не нашел.
...прозрение наступило внезапно и причина, как выяснилось, оказалась даже не в самом языке, а... в особенностях косвенной автоинкрементной/автодекрементной адресации процессора PDP-11, из которого, собственно, и вырос Си. Команда "MOV @(p)+,xxx" пересылала содержимое **p в xxx, а затем увеличивала значение p. Да! Именно p, а отнюдь не ячейки, на которую **p ссылается!!!
Так стоит ли удивляться тому, что люди, взращенные на идеологии PDP-11, перенесли ее поведение и на разрабатываемый ими язык?!
Ниже приводится демонстрационный пример, который можно погонять на различных Си/Си++ компиляторах, с неизменностью получая один и тот же результат.
Листинг 3. Демонстрационный пример pdp.c.
Рисунок 2. Результат прогона pdp.exe.