В языке Си указатели введены как объекты, значениями которых служат адреса других объектов либо функций. Каждая переменная в программе – это объект, имеющий имя и значение. По имени можно обратиться к переменной и получить её значение. В операторе присваивания выполняется обратное действие – имени переменной ставится в соответствие значение выражения. С точки зрения машинной реализации имя переменной соответствует адресу того участка памяти, который для неё выделен, а значение переменной – содержимому этого участка памяти. Чтобы получить адрес в явном виде, в языке Си применяют унарную операцию &. Выражение &a позволяет получить адрес участка памяти, выделенного для переменной a.
Имея возможность с помощью операции & (взятие адреса) определять адрес переменной или другого объекта программы, нужно уметь его сохранять, преобразовывать и передавать. Для этих целей в языке Си введены переменные типа “указатель”, которые обычно называют просто указателями. Значением указателя служит или адрес объекта конкретного типа, или нулевой адрес, для обозначения которого определена специальная константа NULL (в файле stdio.h).
Как и всякие переменные, указатели нужно определять и описывать, для чего используется символ ‘*’. В описании и определении указателей необходимо сообщать, на объект какого типа ссылается этот указатель:
char *z; // z- указатель на объект символьного типа
int *k,*i; // k,i – указатели на объекты целого типа
float *f; // f – указатель на объект вещественного типа
Обозначения *z, *i, *f имеют права переменных соответствующих типов. Оператор *z=’ ‘; засылает символ “пробел” в тот участок памяти, адрес которого определяет указатель z. Оператор *k=*i=0; заносит целые нулевые значения в те участки памяти, адреса которых заданы указателями k,i. Указатель может ссылаться на объекты того типа, который присутствует в определении указателя. Указатели, в определении которых использован тип void, могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования ‘*’.
В языке Си допустимы следующие операции над указателями: присваивание, разыменование (получение значения того объекта, на который ссылается указатель), получение адреса самого указателя, унарные операции изменения значения указателя ++ и - - , аддитивные операции + - и операции сравнений.
Операция присваивания
Операция присваивания предполагает, что слева от знака присваивания помещено имя указателя, справа – указатель, уже имеющий значение, либо адрес объекта того же типа, что и указатель, либо константа NULL. Например, i=&d; k=i; z=NULL; Иногда требуется присвоить указателю одного типа значение указателя другого типа. В этом случае используется “приведение типов”, например:
char *z; // z - указатель на символ
int *k; // k – указатель на целое
z=(char *) k // приведение типов
Подобно любым переменным, переменная типа указатель имеет имя, собственный адрес в памяти и значение. Значение можно использовать, например, присваивать другому указателю или печатать. Адрес указателя может быть также получен с помощью операции взятия адреса &, т. е. &<имя указателя> определяет, где в памяти размещён указатель. Содержимое этого участка памяти является значением указателя.
Унарные операции ++ и - -
С помощью операций ++ и - - числовые значения указателей меняются по-разному в зависимости от типа данных, на которые ссылается указатель. Если указатель связан с типом char, то при выполнении операций ++ и - - его числовое значение изменяется на 1 (указатель z в рассмотренных примерах). Если указатель связан с типом int (указатель k), то операции ++ и - - изменяют его числовое значение на 2 (тип int имеет размер 2 байта). Указатель, связанный с типом float или long операциями ++ ,- - изменяется на 4 . Таким образом, при изменении указателя на единицу указатель переходит к началу следующего (или предыдущего) поля той длины, которая определяется типом.
Аддитивные операции
Аддитивные операции по-разному применимы к указателям. Две переменные типа указатель нельзя суммировать, однако к указателю можно прибавить целую величину. При этом вычисляемое значение зависит от типа объекта, с которым связан указатель. Например, если указатель относится к объекту типа long, то прибавление к нему единицы увеличивает реальное значение на 4, т.е. осуществляется переход к адресу следующего в памяти объекта типа long. В отличие от операции сложения операция вычитания применима не только к указателю и целой величине, но и к двум указателям на объекты одного типа. При этом разность вычисляется в единицах, кратных длине отдельного элемента того типа, к которому отнесён указатель:
int x[5], *i, *k, j;
i=&x[0]; k=&x[4]; j=k-i;
j принимает значение 4, а не 8, как можно было бы предположить, исходя из того, что каждый элемент массива x [] занимает 2 байта.
Добавление целочисленного значения n к указателю, адресующему некоторый элемент массива, приводит к тому, что указатель получает значение адреса того элемента, который отстоит от текущего на n элементов. Если длина элемента массива равна d байтов, то численное значение указателя изменяется на (d*n). Рассмотрим пример, иллюстрирующий перечисленные правила:
int x[4]={0,2,4,6}; int *i, y; i=&x[0]; // i равно адресу элемента x[0] y=*i; // y равно 0; i равно &x[0] y=*i++; // y равно 0; i равно &x[1] y=++*i; // y равно 3; i равно &x[1] y=*++i; // y равно 4; i равно &x[2] y=(*i)++ // y равно 4; i равно &x[2] y=++(*i) // y равно 6; i равно &x[2]
Операции отношения
К указателям применяются операции сравнения ‘>’, ’>=’, ’<’, ’<=’, ’==’, ’!=’. Но сравнивать указатели можно только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса.
Приведём пример, в котором используются операции над указателями и выводятся полученные значения. Для вывода значений указателя в форматной строке функции printf() используется спецификация преобразования %p.
#include <stdio.h> #include <conio.h> float x[]={10.0, 20.0, 30.0, 40.0, 50.0} void main() { float *u1, *u2; int i; clrscr(); printf("\n Адреса указателей: &u1=%p &u2=%p",&u1, &u2); printf("\n Адреса элементов массива: \n"); for (i=0; i<5; i++) printf(" &x[%d] =%p", i, &x[i]); printf("\n Значения элементов массива: \n"); for (i=0; i<5; i++) printf( "x[%d]=%5.1f ", i, x[i]); printf(" \n Доступ к элементам массива с помощью указателей: \n"); for (u1=&x[0], u2=&x[4]; u1<=u2; u1++) printf("u1=%p, *u1=%5.1f \n",u1,*u1); for (u1=&x[0], u2=&x[4]; u2>=u1; u2--) printf("u2=%p, *u2=%5.1f \n",u2,*u2); getch(); }
В соответствии с синтаксисом языка Си имя массива без индексов является указателем – константой, т. е. адресом его первого элемента (с нулевым индексом).
Рассмотрим задачу инвертирования массива символов и различные способы её решения с применением указателей.
Способ 1.
char z[80], s; char *d, *h; // d и h – указатели на символьные объекты for (d=z, h=&z[79]; d<h; d++, h--) { s=*d; *d=*h; *h=s; }
В заголовке цикла указателю d присваивается адрес первого ( с нулевым индексом) элемента массива z. Здесь можно было бы применить и другую операцию, а именно d=&z[0]. Указатель h получает значение адреса последнего элемента массива z. Цикл выполняется до тех пор, пока z<h. После каждой итерации значение d увеличивается, значение h уменьшается на 1. При первой итерации в теле цикла выполняется обмен значений z[0] и z[79], так как d – адрес z[0], h – адрес z[79]. При второй итерации значением d является адрес z[1], для h – адрес z[78] и т. д.
Способ 2.
char z[80], s *d, *h; for (d=z, h=&z[79]; d<h) { s=*d; *d++ = *h; *h-- =s;}
Приращение указателя d и уменьшение указателя h перенесены в тело цикла. В выражениях *d++ и *h—операции увеличения и уменьшения на 1 имеют тот же приоритет, что и унарная операция *. Поэтому изменяются на 1 не значения элементов массива, на которые указывают d и h, а сами указатели. Последовательность действий такая: по значению указателя d или h обеспечивается доступ к элементу массива; в этот элемент заносится значение из правой части оператора присваивания; затем увеличивается (уменьшается) на 1 значение указателя d (или h).
Способ 3.
char z[80], s, *d, *h; d=z; h=&z[79]; while (d<h) { s=*d; *d++ =*h; h*-- = s;}
Способ 4.
char z[80], s; int i; for (i=0; i<40; i++) { s=*(z+i); *(z+i)=*(z+(79-i)); *(z+(79-i))=s; }
Данный пример иллюстрирует возможность использования вместо индексированного элемента z[i] выражения *(z+i), т. к. имя массива без индексов есть адрес его первого элемента. Прибавив к имени массива целую величину, получаем адрес соответствующего элемента, т. о. &z[i] и z+i - это две формы определения адреса одного и того же элемента массива.
Итак, операция индексирования z[n] определена таким образом, что она эквивалентна *(z+n), где z – имя массива, n – целое. Для многомерного массива правила остаются теми же. Таким образом, z[n][m] эквивалентно *(*(z+n)+m).
Отметим, что имя массива не является переменной типа указатель, а есть константа – адрес начала массива. Таким образом, к имени массива не применимы операции ++ и -- , имени массива нельзя присвоить значение, т. е. имя массива не может использоваться в левой части оператора присваивания.
Изменение указателя на 1 приводит к разным результатам
в зависимости от типа объекта, с которым связан указатель. Значение указателя
изменяется на длину участка памяти, выделенного под этот объект, поэтому
для символьного массива переход к соседнему элементу изменяет указатель
на 1, для целочисленного – на 2, для вещественного – на 4.
1. Понятие и определение указателя.
2. Какие операции допустимы над указателями?
3. Что можно присвоить переменной типа указатель?
4. Как можно использовать унарные и аддитивные операции для указателей?
5. Особенности применения к указателям операций сравнения.
6. Способы доступа к элементам массива с помощью указателей.