Студенту >> Язык программирования Си


Адресная арифметика языка Си

Язык Си имеет средства работы непосредственно с областями оперативной памяти ЭВМ, задаваемыми их адресами (указателями). В языке C указатели строго типизированы, т. е. различают указатели (адреса) символьных, целых, вещественных величин, а также типов данных, создаваемых программистом.

Для описания указателя на какой-либо тип данных перед именем переменной ставится *. Например в строке

int *a, *b, c, d;  

описываются два адреса и две переменные целого типа. В строке

double *bc;

описан адрес переменной вещественного типа. Никогда не следует писать знак * слитно с типом данных, например как в следующей строке:

int* a, b;

В этой строке создается ложное впечатление о том, что описаны два указателя на тип int, в то время как на самом деле описан один указатель на int, а именно a, и одна переменная типа int.

Описание переменных заставляет компилятор выделять память для хранения этих переменных. Описание указателя выделяет память лишь для хранения адреса. В этом смысле указатель на целое данное и на тип double будут занимать в ЭВМ одинаковое количество байт памяти, зависящее от модели памяти, на которую настроен компилятор. Например, в 16-ти разрядной Small модели длина указателя равна двум байтам, а в 16-ти разрядной Large модели - четырем.

При описании указателей в качестве имени типа данных можно использовать ключевое слово void, например

void *vd;

При таком описании с указателем не связывается никакой тип данных, то есть получаем указатель на данное произвольного типа.

Для указателей одного и того же типа допустимой является операция присваивания, кроме того указателю типа void может быть присвоено значение адреса данного любого типа, но не наоборот, например

int *a, *b; 
double *d; 
void *v;
...
a = b; /* Правильно */
v = a; /* Правильно */
v = d; /* Правильно */
b = v; /* Неправильно */
d = a; /* Неправильно */ 

В случае неправильного присваивания указателей компиляторы обычно выдают предупреждающие сообщения, которыми никогда не следует пренебрегать. Например, компилятор фирмы Borland выдает сообщение: "Suspicious pointer conversion", которое переводится как "Подозрительное преобразование указателей".

Если по какой-либо причине необходимо выполнить операцию присваивания между указателями разного типа, то следует использовать явное преобразование типов, например для указателей из предыдущего примера

b = (int *) v;
d = (double *) a; 

При этом ответственность за корректность подобных операций целиком ложится на программиста. Действительно, в предыдущем примере a является указателем на ячейку памяти для хранения величины типа int. Обычно это ячейка размером 2 байта. После присваивания указателей с явным преобразования типов, делается возможным обращение к этой ячейке посредством указателя d, как к ячейке с величиной типа double. Размер этого типа обычно 8 байт, да и внутреннее представление данных в корне отличается от типа int. Никакого преобразования самих данных не делается, ведь речь идет только об указателях. Дальнейшая работа с указателем d скорее всего заденет байты, соседние с байтами на которые указывает a. Результат интерпретации этих байт будет тоже неверным.

Для поддержки адресной арифметики в языке Си имеются две специальные операции - операция взятия адреса & и операция получения значения по заданному адресу * (операция разадресации).

Операция & может применяться к любому объекту программы, адрес которого в принципе может быть определен. Результатом операции является указатель того же типа, что и тип объекта, имеющий значение адреса объекта. Если эту операцию применить к указателю, то результатом будет адрес ячейки памяти, в которой хранится значение указателя. Результат операции & можно использовать в любом выражении, где допускается использование указателя соответствующего типа.

Операция * может применяться только к указателям и только в том случае когда они типизированы. При необходимости применить эту операцию к указателю типа void следует использовать явное преобразование типов. Результатом операции * является значение того объекта, к адресу которого применялась операция *, тип результата совпадает с типом объекта. Результат операции * можно использовать в любом выражении, где допускается использование объекта соответствующего типа.

Рассмотрим работу вышеописанных операций на следующем примере

int *p, a, b; 
double d; 
void *pd;
p = &a;
*p = 12;
p = &b;
*p = 20;
/* Здесь a содержит число 12, b - число 20 */
pd = &d;
*( (double *) pd ) = a;
/* Здесь d содержит число 12.0 */ 

Поясним первые четыре присваивания рисунком, в котором прямоугольники изображают ячейки памяти для хранения величин типа int и указателей, внутри которых проставлены значения величин, а над ними записаны их названия и адреса.

Состояние ячеек до первого присваивания

P, адрес 1000

a, адрес 2000

b, адрес 4000

мусор

мусор

мусор

Состояние ячеек после присваивания p = &a

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

мусор

мусор

Состояние ячеек после присваивания *p = 12

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

12

мусор

Состояние ячеек после присваивания p = &b

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

мусор

Состояние ячеек после присваивания *p = 20

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

20

Описание указателя не является требованием на выделение памяти для хранения данных. Память выделяется только для хранения адреса. Поэтому прежде, чем использовать указатель, ему нужно присвоить значение адреса реально существующего объекта. В противном случае результат работы программы непредсказуем. Рассмотрим, например, следующую последовательность строк

  double *a, b;
  b = *a;
  *a = 135.7;  

В этой последовательности используется указатель, которому предварительно не присвоено никакого значения. В ячейке a находится произвольное значение, возможно оставшееся от работы предыдущей программы. Первая операция присваивания приведет к тому, что переменная b получит значение из ячейки памяти с непредсказуемым адресом. Вторая - к тому, что по непредсказуемому адресу будут записаны 8 байт, являющиеся двоичным представлением числа 135.7. Если эти байты попадут на область данных программы, то программа скорее всего выдаст неправильный результат. Если они попадут на область кода программы или на системную область MS DOS, то в лучшем случае программа аварийно завершится, а в худшем компьютер полностью зависнет.

Если делается попытка присвоить какое-либо значение по адресу указателя, значение которого равно нулю, то многие компиляторы выдают сообщение Null pointer assingment

К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться. Для компилятора фирмы Borland легко можно отследить момент некорректного обращения к памяти. Для этого нужно в окно просмотра значений выражений поместить выражение (char *) 4, затем пошагово выполнять программу до тех пор, пока строка-подпись фирмы Borland в окне просмотра не изменится. Если программа имеет большой размер, то более целесообразно выполнять ее от одной точки останова до другой.

Следует также опасаться случая, когда указатель содержит адрес объекта программы, завершившего свое существование. Например, результат работы следующей программы неверен и непредсказуем:

  
#include <stdio.h>
#include <math.h> 
double * Cube(double x)
{
  double cube_val;
  cube_val = x*x*x;
  return &cube_val;
} 
void main(void)
{
  double *py;
  py = Cube(5);
  printf("y1 = %lf\n", *py);
  sin(0.7);
  printf("y1 = %lf\n", *py);
}

Это происходит потому, что функция Cube() возвращает указатель на локальную переменную cube_val, которая существует только в пределах функции Cube(). После возврата из функции память, ранее распределенная под переменную cube_val, освобождается и может быть использована компилятором для других целей несмотря на то, что значение возвращенного указателя по-прежнему содержит ее адрес.

НАВЕРХ