Билет 9

advertisement
Адреса и указатели
Между объектами реального мира, которые моделируются на компьютерах,
существуют разнообразные, постоянно меняющиеся связи. Эти связи, как и сами объекты,
могут появляться и исчезать. Поэтому если мы хотим описать в программе группу с
переменным числом объектов, связи между которыми тоже подвержены изменениям,
нужны соответствующие структуры. Эти структуры должны позволять устанавливать,
изменять и разрывать связи между моделируемыми объектами, а также порождать или
уничтожать сами объекты.
Для этой цели в Си используются указатели и динамические структуры.
До сих пор мы рассматривали только статические структуры. Этим термином
обозначаются структуры данных (массивы, файлы, структуры), которые возникают
непосредственно перед выполнением программы в соответствии со своим описанием,
существуют в течение всего времени ее выполнения, и размер которых задается заранее с
помощью описания их типов и не изменяется в ходе выполнения программы.
Однако при решении многих задач мы заранее, то есть на этапе написания
программы, не знаем не только размера того или иного программного объекта (структуры
данных), но и даже того, будет ли вообще нужен этот объект. Такого рода программные
объекты или структуры данных, возникающие уже в процессе выполнения программы по
мере необходимости, размер которых определяется или изменяется при ее выполнении,
называются динамическими объектами или структурами.
Язык Си отличается от других языков программирования, прежде всего
широким использованием указателей. Именно наличие в нем указателей сделало его очень
удобным для системного программирования.
Под указателем (ссылкой) понимается переменная, содержащая адрес
(номер) ячейки памяти, в которой хранится значение любого программного объекта –
переменной, элемента массива, функции, структуры.
Оперативная память компьютера – это последовательность байтов, каждый
из которых имеет номер (адрес). Адреса байтов памяти идут по возрастанию: если к – это
номер текущего байта, то к-1 – номер предыдущего, а к+1 – последующего байтов.
Указатель как раз и содержит адрес некоторого байта памяти. Меняя значение указателя,
можно перемещаться по памяти, адресуясь к различным данным, записанным в
просматриваемых ячейках.
Таким образом, в Си под указателем понимается переменная, содержащая
адрес любого объекта программы. Значит, указатель говорит о том, где в памяти размещен
тот или иной программный объект (переменная, массив, структура, функция), но ничего
не говорит об имени и значении этого объекта. По аналогии – можно знать почтовый
адрес человека, но этот адрес не дает нам информацию о количестве комнат в его
квартире и ее обстановке.
Применение указателей полезно:

при работе с массивами – обеспечивается использование сразу всех
элементов массива,

при обращении к функциям – можно одновременно возвратить сразу
несколько значений, вычисляемых функцией,

при работе с файлами – обеспечивается быстрый доступ к компонентам

при создании новых переменных в процессе выполнения программы –
файла,
можно динамически выделять память для них.
Для работы с указателями в Си используются две операции:
* - доступ по адресу (обращение по адресу),
& - получение адреса.
Знак * , стоящий перед именем переменной-указателя, означает «взять
(записать) значение по данному адресу»: *ptr – записать значение по адресу ptr.
Знак & , стоящий перед именем обычной переменной, означает «получить
адрес переменной»: &x – получить адрес переменной x..
Перед использованием в программе указатели, как и любые другие
переменные, должны быть описаны – задан тип объекта, адрес которого будет хранить
указатель, поставлена звездочка * и задано имя указателя:
int i, j, *ptr;
float x, y, *ukaz;
char c, d, *adr;
Описаны указатели:
рtr – на любой объект (переменную, массив, функцию) целого типа,
ukaz – на любой объект вещественного типа,
adr – на любой объект символьного типа.
После описания указателей им можно присвоить значения адресов
переменных этого же типа, используя операцию получения адреса &:
ptr = &i;
ukaz = &x;
adr = &c;
Сейчас указатели ptr, ukaz, adr будут хранить адреса (номера)
первых (младших) байтов памяти, отведенной для переменных i, x, c (адрес байта
памяти – это шестнадцатеричное число).
Присвоим этим переменным некоторые значения:
i = 5;
x = 3.1416;
c = ‘@’;
Обратимся к ним по адресу:
j = *ptr;
y = *ukaz;
d = *adr;
Содержимое ячейки памяти с адресом ptr (а там записано число 5 –
ведь это адрес переменной i) будет присвоено переменной j, содержимое ячейки
памяти с адресом ukaz – переменной y, содержимое ячейки памяти с адресом adr –
переменной d.
Таким образом, два оператора присваивания:
ptr = &i;
j = *ptr;
выполняют то же самое, что и один оператор:
j = i;
Значит, можно организовать не только прямую передачу данных от одной
переменной к другой (j = i), но и косвенную – через адреса переменных, даже не
упоминая их имен. Указатели позволяют обращаться к конкретным ячейкам памяти и
изменять их содержимое, не интересуясь тем, значения каких именно переменных
хранятся в этих ячейках:
*adr = ‘+’;
переменой по адресу adr присвоено значение символа ‘+’,
(*ptr)++;
значение переменной по адресу ptr увеличивается на единицу,
(*ptr) += 3;
значение переменной по адресу ptr увеличивается на 3,
j = (*ptr)++;
значение переменной по адресу ptr присваивается переменной j и после этого
увеличивается на единицу.
Если это выражение записать без скобок:
j = *ptr++;
то сначала значение адреса ptr
увеличивается на единицу (получаем адрес следующей
ячейки памяти), а затем содержимое новой ячейки памяти присваивается переменной j :
операции доступа по адресу * и инкремента ++ имеют одинаковый приоритет, но
выполняются справа налево.
Операции над указателями
Помимо операций доступа по адресу *
и получения адреса & , над
указателями определены следующие операции:

сложение с константой,

вычитание,

инкремент,

декремент,

операции отношений.
Операция доступа по адресу * предназначена для доступа к величине, адрес
которой хранится в указателе. Эту операцию можно использовать как для получения, так
и для изменения значения величины (если она не объявлена как константа):
char a;
// a - переменная типа char
char *ptr;
// выделение памяти под указатель
*ptr = ‘@’; // по адресу
a = *ptr;
ptr
ptr
записано значение
@
// переменной а присвоено значение,
// записанное по адресу
ptr
Как видно из примера, конструкцию *указатель можно использовать
в левой части оператора присваивания, так как она определяет адрес области памяти. Эту
конструкцию можно считать именем переменной, на которую ссылается указатель. С ней
допустимы все действия, определенные для величин соответствующего типа.
Арифметические операции с указателями (сложение с константой,
вычитание, инкремент и декремент) автоматически учитывают размер типа величин,
адресуемых указателями. Эти операции применимы только к указателям одного типа и
имеют смысл в основном при работе со структурами данных, последовательно
размещенными в памяти, например, с массивами.
Инкремент ++ перемещает указатель к следующему элементу массива,
декремент - к предыдущему. Если указатель на определенный тип увеличивается или
уменьшается на константу, то его значение изменяется на величину этой константы,
умноженной на размер объекта данного типа. Эта операция производится автоматически.
Разность двух указателей – это расстояние между двумя областями памяти,
определяемое в единицах, кратных размеру (в байтах) объекта того типа, к которому
отнесены эти указатели. Таким образом, разность указателей, адресующих два смежных
объекта одинакового типа, по абсолютной величине всегда равна единице.
Внимание! Суммирование двух указателей недопустимо!
Указатели редко используются с простыми данными вроде отдельных
переменных. Преимущества указателей наглядно проявляются при использовании их
вместе с массивами, функциями и строками.
Указатели и массивы
В Си существует самая тесная связь между указателями и массивами, поэтому
лучше эти средства рассматривать вместе.
Как мы знаем, структура массива полностью соответствует структуре оперативной
памяти – элементы массива занимают в ней подряд идущие ячейки. Значит, если описан
массив:
int mass[5];
то в оперативной памяти для его элементов выделяется пять подряд идущих ячеек:
mass[0] mass[1] mass[2] mass[3] mass[4]
К i-му элементу этого массива можно обратиться, назвав его индекс: mass[i] .
Доступ к любому элементу массива, осуществляемый по его индексу (номеру),
может быть выполнен при помощи указателя, причем это будет сделано быстрее. Опишем
переменную ptr как указатель на данные целого типа:
int *ptr;
В результате присваивания
ptr
= &mass[0];
эта переменная будет содержать адрес начального (нулевого) элемента этого массива, то
есть указатель ptr будет указывать на элемент
mass[0]:
ptr
mass[0] mass[1] mass[2] mass[3] mass[4]
Адрес начального элемента любого массива называется базовым адресом этого массива.
Таким образом, сейчас указатель ptr содержит базовый адрес массива mass .
Если увеличить значение указателя на единицу, то ptr + 1 будет указывать на
следующий элемент массива, то есть на mass[1] , ptr + 2 - на элемент
mass[2] и так далее. В общем случае, если значение указателя увеличить на k , то
можно получить адрес k-го элемента массива mass.
Значит, адрес любого элемента массива равен сумме его базового адреса, который
является адресом его начального элемента, и смещения этого элемента от начала
массива. Для начального (нулевого) элемента массива это смещение равно нулю, для
первого элемента – единице, для второго – двум, для k-го оно равно k. Это верно для
массива любого типа. Смысл выражения “увеличить указатель ptr на единицу “ , как и
смысл любой арифметики с указателями заключается в том, что ptr + 1 указывает
на следующий за ptr элемент, а ptr + k на k-й после ptr элемент массива.
Между индексированием элементов массива и арифметикой с указателями
существует очень тесная связь, потому что имя любого массива в Си есть адрес его
начального элемента, то есть имя массива является его базовым адресом. Значит, в
нашем примере присваивание:
ptr
= &mass[0];
можно записать в другом виде:
ptr
= mass;
Это будет одно и то же: записи &mass[0] и mass эквивалентны.
Из всего этого следует, что в общем случае запись &mass[k] будет
эквивалентна записи
(mass + k) , а сам k-й элемент массива можно определить
как mass[k] или как *(mass + k). С другой стороны, если ptr – указатель,
то в выражениях его можно использовать с индексом, то есть запись ptr[k]
эквивалентна записи *(ptr + k).
Таким образом, элемент массива в Си разрешается изображать и в виде указателя
со смещением, и в виде имени массива с индексом.
Между именем массива и указателем, выступающим в роли имени массива, однако
существует различие. Указатель – это переменная, поэтому можно записать ptr =
mass или ptr++. Но имя массива не является переменной, и записи типа mass =
ptr или mass++ не допускаются.
Помимо рассмотренной операции сложения, над указателями можно выполнять
следующие операции:
 складывать и вычитать указатели и целые данные,
 вычитать и сравнивать два указателя, ссылающиеся на элементы одного и того же
массива,
 присваивать значение указателя другому указателю того же типа,
 присваивать указателю нуль и сравнивать его с нулем.
Над указателями нельзя выполнять следующие операции:
 складывать два указателя, перемножать их, делить, сдвигать, выделять разряды,
 складывать указатели со значениями типа float и double,
 присваивать указателю одного типа значение указателя другого типа (исключение
составляют указатели типа void).
Указатели можно использовать и при работе с многомерными массивами:
int trio[5][2][3];
int *i_ptr;
Описан трехмерный массив trio целого типа и указатель ptr на данные целого типа.
Присвоим этому указателю значение базового адреса массива:
i_ptr=&trio[0][0][0];
Необходимо учесть, что для многомерных массивов нельзя использовать операцию
присваивания базового адреса указателю в таком виде:
i_ptr=trio;
как это имеет место для векторов (одномерных массивов).
Доступ к j-му элементу i-ой строки k-го слоя массива trio может быть
осуществлен или с помощью индексов:
trio[k][i][j]=1;
либо с помощью указателей:
*(i_ptr + k*(2*3) + i*3 + j)=1;
Как и в Паскале, в языке Си запрещается присваивать значения элементов одного массива
другому массиву целиком:
float r[2][2], s[2][2];
r = s;
// ошибка!
Эти ограничения можно обойти с помощью указателя:
float *f_ptr;
f_ptr = &s[0][0];
r = *f_ptr;
При этом элементам массива r будут присвоены значения соответствующих элементов
массива s.
Download