Каждая переменная в программе – это объект, имеющий имя (идентификатор) и значение. По имени можно обратиться к переменной и получить ее значение. С точки зрения машинной реализации имя переменной соответствует адресу того участка памяти, который для нее выделен, а значение переменной – содержимому этого участка памяти.
Какие бы данные не хранились в переменной, в основе всегда лежит один и тот же фундаментальный принцип:
любая переменная является только именем, которое компилятор ассоциирует с определенным адресом памяти компьютера.
Таким образом, у переменной помимо имени есть еще адрес, и есть содержимое этого адреса.
Адреса
| Содержимое
|
|
Переменная Symbol
| ‘A’
| 0x0012ff50
|
| ‘B’
| 0x0012ff51
|
| ‘C’
| 0x0012ff52
|
Для компьютера все, что находится в памяти, является просто данными – идет ли речь об адресе или о содержимом переменной. Это означает, что адрес переменной можно рассматривать как новый вид данных: извлекать его из памяти, как-то манипулировать им и сохранять результат этой манипуляции. Именно такой тип данных определен в языке С и носит название указателя.
На вопрос для чего нужны указатели, можно ответить так. Через механизм указателей программист имеет прямой доступ к оперативной памяти. Эта возможность бывает полезна при работе с простыми объектами (переменными) и совершенно необходима при работе со сложными объектами, например, массивами. Без помощи указателей невозможно организовать передачу данных между программами.
Указатель – это переменная, которая содержит адрес (в памяти) другой переменной, точнее адрес первого байта занимаемого этой переменной фрагмента памяти.
Указатель может указывать на что угодно в памяти. Более того, с его содержимым (адресом) можно производить арифметические операции, в результате чего он будет указывать на другую область памяти, содержащую другую переменную или другую часть переменной-массива.
Указатель является просто переменной, поэтому прежде чем работать с ним, его следует объявить. Как и у любой переменной, у указателя имеется имя и тип данных. Единственное отличие в объявлении указателя состоит в прибавлении знака операции косвенной ссылки «*» (еще говорят «косвенной адресации» или «разыменования» или «раскрытия ссылки» или «обращения по адресу»), говорящего компилятору, что создается указатель.
Можно смешивать в одном объявлении указатели и обычные переменные:
double *pVal, Value;
int *pNum0, *pNum1;
В первом из примеров создаются две переменные: pVal является указателем на переменную типа double, а Value – обыкновенной переменной этого типа; во втором примере объявляются два указателя на целое.
Операция разыменования (*) применяется не только для объявления. Ее операндом всегда является указатель, а результатом – тот объект, который адресует указатель операнд. Таким образом, *pNum0 обозначает объект типа целое (целая переменная), на который указывает pNum0. Обозначения *pVal, *pNum0, *pNum1, *plVal имеют права переменных соответствующих типов. Так, оператор *pNum0=100; засылает число 100 в тот участок памяти, адрес которого определяет указатель pNum0.
Пример
int x=100, y = 99, *pNum0; // pNum0 - указатель,
pNum0 = &x; // pNum0 = 0x0012fec3 - адрес в памяти
y = *pNum0; // y = 100 - поменяло значение
В приведенном примере «&» является операцией взятия адреса и в словесной форме вторая строка примера может быть описана как «указателю pNum0 присвоить значение адреса переменной x».
Следует обратить особое внимание на то, что указатель может ссылаться только на объект того типа, который задан в его определении. Исключением являются указатели, в определении которых использован тип void – отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования (для void *pAdr объект по адресу pAdr не определен).
Как и любые другие переменные, объявленный указатель можно инициализировать. Это действие реализуется операцией присваивания следующего вида:
double *pVal, Value;
. . .
pVal = &Value;
Поскольку различные типы данных имеют различную длину, то существенным является соответствие типов объявленного указателя и присваиваемого ему значения. Так, приведенный далее пример порождает ошибку:
int *pNum;
float Number;
Number = 10.5;
pNum = &Number; ошибка!
Следует отметить, что операция взятия адреса «&» применима только к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, константам, регистровым переменным, битовым полям структур (которые мы будем рассматривать на следующей лекции) и внешним объектам, с которыми может взаимодействовать программа (например, файлам).
Указатель можно инициализировать специальной константой NULL, которая определена в файле stdio.h, и соответствует заведомо не равному никакому адресу значению, принимаемому за нулевой адрес.
В языке С над указателями допустимы следующие (основные) операции:
1 – присваивание
2 – получение значения того объекта, на который ссылается указатель
3 – получение адреса самого указателя
4 – унарные операции изменения значения указателя
5 – аддитивные операции
6 – операции сравнения.
В С существует сильная взаимосвязь между указателями и массивами. Любое действие, которое достигается индексированием массива, может быть выполнено также с помощью указателей, причем последний вариант будет быстрее.
В соответствии с синтаксисом языка имя массива без индексов является адресом его первого элемента, т.е. указателем. Поэтому использование указателей при программировании задач преобразования массивов является вполне оправданным и полезным.
Операция индексирования М[E] определена таким образом, что она эквивалентна *(М + Е), где М – имя массива, Е – целое. Для многомерного массива правила остаются теми же, т.е. M[n][m][k] эквивалентно *(M[n][m]+k) и далее *(*(*(M+n)+m)+k).
Особо отметим, что имя массива не является переменной типа указатель, а есть константа – адрес начала массива. В силу этого к имени массива не применимы операции увеличения или уменьшения, имени массива нельзя присвоить значение, т.е. имя массива не может использоваться в левой части оператора присваивания.
При рассмотренном нами правиле определения массива его размер (по крайней мере, максимум) должен быть известен уже при написании программы. Однако на практике часто размер массива является результатом проведенных вычислений и в момент написания программы еще не известен. В этом случае создаются так называемые массивы динамической памяти.
Формирование массивов с переменными размерами можно организовать с помощью указателей и средств для динамического выделения памяти. Такими средствами является набор специальных функций, описанных в заголовочных файлах alloc.h – Borland, malloc.h – Microsoft).