среда, 28 мая 2014 г.

Арифметика указателей: преимущество или недостаток C и C++



Выражения вида *p++ появляются в программах C и C++ постоянно, и мало какие еще языки поддерживают подобную нотацию.
Мы продолжаем дискуссию прошлой недели об унификации массивов и указателей – глбинную идею основ программирования на C и C++. И ярчайшим примером использования подобной идеи является как раз арифметические выражения на указателями: *p++, которые редко где встречаются помимо C и C++.

Я использую слово унификация для ссылки на это использование, потому что оно доказывает, причем эффективно, что существует только один наилучший способ адресации смежной памяти. Этот способ основан на связывании минимально возможных единиц памяти в структуру данных, которая позволяет организовать адресацию. Данный подход позволяет программисту C полагать что если указатель p ссылается на a[n] то p+1 – будет ссылаться на a[n+1], вне зависимости от типа a и архитектуры используемого компьютера.

Значение выражения *p++ основано на следующих характеристиках C:
-          Поддержка языком типов указателей, значения этих типов могут быть использованы для определения других занчений в памяти.
-          Указатель содержит всю информацию необходимую для определения объекта, на который он ссылается.
-          Возможно использование типа укзателя для определения типа соответсвующего объекта.
-          Объекты в памяти могут располагатся друг за другом.
-          Если указатель p ссылается на объект, то ++p или –p изменяет значение p, и в итоге он ссылается на объект, являющийся соседним по отношению к изначальному.
-          Добавление (вычитание) неотрицательного числа n к p имеет тот же эффект, как и выполнение ++p (--p) n раз. Добавление –n имеет тот же эффект что и вычитание n, и наоборот.
 

Объявление указателей подобным образом весьма удобно, но также здесь существует и некоторя опасность. Если мы используем a[n] для ссылки на элемент n массива a, легко обнаружить, что компилятор в процессе вычисления a[n] может проверить значение n на предмет выхода за границы масива a. С другой стороны, в случае указателя p на элемент массива a, если позже мы будет обращатся к этому элементу через выражение *p, возможно возникновение ситуации, когда размер массива изменится и это сделает выражение *p некоректным.  


Указатели C и С++ опасны по той же причине почему они и полезны: они соответсвуют нотации массива но не следуют ей в плане структуры данных. На практике, вычисление которое присутствует при получении элемента массива a[n] по числу n, больше относится к указателю чем к массиву. Перенос вычисления в данном случае дает программисту больше контроля над самим процесом вычисления: если мы знаем положение a[n], то мы можем получить a[n+1] без выполнения дополнительных вычислений индекса. Однако, этот перенос создает проблему выравнивания: если p указывает на элемент a, то нету автоматического способа отражения в p изменений местонаходения памяти a.

Арифметический указатель также опасен и по другой причине. Если мы используем целочисленное значение для опрееления положения элемента массива, то понятно что эти значение могут оказатся за пределами границ массива. Это ведь просто число, и если оно не является индексом некоторого элемента массива, то проблемы как то в этом и нету. В противовес этому, если мы используем указатели для аддресации элементов массива, мы должны быть готовы учесть ситуации, в которых указатель ссылается на участок памяти, не принадлежащий массиву. Вы вероятно можете подумать что возможно такие указатели запретить , но если вы думаете о механизме использования указателей для обработки всех элементов массива, вы обнаружите, что тут существует проблема, в случае когда массив не содержит элементов вовсе.
 
Вкраце, если мы переведем трансформируем программу, использующую индекы в соответствующий вариант, который использует указатели, то мы должны отметить обстоятельства, при которых мы должны осуществить дополнительную обработку “некорректных” указателей. Но об этом мы еще поговорим.