Адреса и указатели. Операции получения адреса и косвенной адресации. Отождествление массивов и указателей. Адресная арифметика. Указатели на массивы
Вид материала | Лекция |
- Адреса и указатели. Операции получения адреса и косвенной адресации. Отождествление, 124.21kb.
- Понятие протокола, и связанные с ним понятия, 3193.16kb.
- Опитувальник клієнта – фізичної особи-підприємця, 95.84kb.
- Броварський міський центр соціальних служб для сім′ї, дітей та молоді Адреса, 33.35kb.
- Міністерства юстиції України в Автономній Республіці Крим вул, 19.85kb.
- Х своего паспорта, а при его отсутствии иного документа, удостоверяющего личность, 7.97kb.
- Понятие о глобальных сетях, 222.22kb.
- Реализовать функцию протокола ModBus $03 для чтения из адресного пространства данных., 125.1kb.
- Студент третьий курс, Группа 191. Казань-2011 Адресация в сети интернет Адресеция, 211.02kb.
- Сервер, 202.32kb.
ЛЕКЦИЯ 4
Адреса и указатели. Операции получения адреса и косвенной адресации. Отождествление массивов и указателей. Адресная арифметика. Указатели на массивы. Массивы указателей и многомерные массивы. Динамическое выделение памяти под массивы. Инициализация указателей.
$ 1. АДРЕСА И УКАЗАТЕЛИ.
Во время выполнения всякой программы, используемые ею данные размещаются в оперативной памяти ЭВМ, причем каждому элементу данных ставится в соответствие его индивидуальный адрес. При реализации многих алгоритмов и представлении сложных структур данных часто оказывается полезной возможность непосредственной работы с адресами памяти. Подобная ситуация возникает, например, при обработке массивов переменных. Действительно, поскольку соседние элементы массива располагаются в смежных ячейках памяти, то для перехода от одного его элемента к другому можно вместо изменения значения индексного выражения манипулировать адресами этих элементов. Предположим для определенности, что нулевой элемент целочисленного массива расположен в ячейке памяти с адресом (номером) Ао. Тогда, зная, что длина элемента данных типа int составляет два байта, нетрудно вычислить адрес (номер) ячейки, в которой будет находиться i-ый элемент этого массива:
Ai = Ao + 2*i
На первый взгляд работа с адресами может показаться утомительной и бесполезной. На самом же деле она является даже более естественной, чем работа с индексами, поскольку в процессе компиляции программы всякое индексное выражение трансформируется в операции над адресами.
Объекты языка Си, значениями которых являются адреса оперативной памяти, получили название указателей. В общем случае указатели являются переменными величинами и над ними можно выполнять определенный набор операций подобно тому, как мы оперировали числовыми переменными. В языке Си всякий указатель имеет базовый тип, который совпадает с типом элемента данных, на который может ссылаться этот указатель. Такое соглашение, возможно несколько ограниченое, существенно упрощает и делает значительно более эффективной работу с указателями.
Переменные-указатели, как и переменные любых других типов, перед их использованием в программе должны быть предварительно объявлены в одной из инструкций описания данных. В случае указателей на простые переменные это делается следующим образом:
тип *идентификатор;
где символ (*) определяет саму переменную как указатель.
Примеры:
int *ptr;
long *sum;
float *rez, *val;
Каждая из этих инструкций говорит о том, что соответствующая переменная есть указатель на элемент данных определенного типа, а комбинация, например, вида *ptr представляет собой величину типа int, а ссылка на ptr - адрес этой величины в оперативной памяти. По существу это означает, что подобные комбинации могут использоваться как операнды произвольных выражений. В частности, сохраняя обозначения предыдущего примера, мы могли бы написать:
*sum = 0;
for(*ptr = 1; *ptr <= 100; (*ptr)++)
*sum = *sum + (*ptr) * (*ptr);
что соответствует фрагменту программы вычмсления суммы квадратов первых 100 натуральных чисел. Круглые скобки в корректирующем выражении оператора цикла являются существенными.
Строго говоря, компилятор языка Си рассматривает комбинации вида
*идентификатор
в составе выражений как некоторую операцию над указателями. Эта операция, символом которой как раз и является звездочка перед именем указателя, носит название косвенной адресации и служит для доступа к значению, расположенному по заданному указателем адресу.
Существует и другая операция, в определенном смысле противоположная операции косвенной адресации и именуемая операцией получения адреса. Она обозначается символом амперсанд ( & ) перед именем простой переменной или элемента массива:
&a &mas[4]
и сопоставляет своему аргументу адрес его размещения в памяти, т.е.указатель. Естественно, что этим аргументом может быть и указатель,поскольку указатели, как и другие переменные, хранятся в ячейках оперативной памяти.
Всевозможные выражения, построенные с использованием указателей или операторов * и &, принято называть адресными выражениями, а сами сами арифметические операции над указателями - адресной арифметикой. Одноместные операции * и & имеют такой же высокий приоритет, как и другие унарные операции, и в составе выражений обрабатываются справа налево. Именно поэтому в предыдущем примере небходимы скобки в выражении (*ptr)++ , ибо без них оператор ++ относился бы к указателю ptr, а не к значению, на которое ссылается этот указатель.
Замечание. Если, например, mas есть массив переменных, то выражение &mas[0] равносильно простому употреблению имени массива без следующего за ним индексного выражения, поскольку последнее отождествляется с адресом размещения в памяти первого элемента этого массива.
Примеры.
1. Аргументами функции scanf являются адреса переменных, которым должны быть присвоены прочитанные значения:
scanf("%d%d", &m,&n);
2. Следующая пара операторов
px = &x;
y = *px;
где переменная рх объявлена предварительно как указатель, равносильна
непосредственному присваиванию
y = x;
3. При выполнении следующего фрагмента программы сравнение в операторе if всегда будет истинно, поскольку значение указателя numptr совпадает с адресом переменной number:
int number;
int *numptr = &number;
scanf("%d%d", &number, numptr);
if(number == *numptr)
printf("сравнение истинно");
else
printf("Сравнение ложно");
$ 2. ОТОЖДЕСТВЛЕНИЕ МАССИВОВ И УКАЗАТЕЛЕЙ. АДРЕСНАЯ АРИФМЕТИКА.
Как мы уже отмечали, при отсутствии индексного выражения имя массива по существу есть указатель на его первый (нулевой) элемент. Поэтому доступ к i-ому элементу этого массива можно получить, увеличивая значение указателя на соответствующую величину.
Рассмотрим в качестве примера следующее описание int a[10];
определяющее массив из десяти элементов типа int. Поскольку a == &a[0], то адрес элемента a[i] равен
a + sizeof(int) * i
Хотя приведенная запись и отражает существо дела, тем не менее она является недостаточно удобной из-за своей громоздкости. Действительно, учитывая, что всякий элемент массива а имеет тип int и занимает sizeof(int) байт памяти, из адресного выражения можно было бы исключить информацию о длине элемента массива. Для этого достаточно, например, принять соглашение о том, что выражение вида a = i как раз и определяет адрес i-ого элемента, т.е.
&a[i] == a+i
Тогда обозначение a[i] становится эквивалентным адресному выражению *(a+i) в том смысле, что оба они определяют одно и то же числовое значение, а именно:
a[i] == *(a+i)
Пусть теперь имеется пара описаний
int a[10];
int *pa;
Выполняя операцию присваивания
pa = a или pa =&a[0]
мы устанавливаем указатель ра на нулевой элемент массива а и поэтому
справедливы равенства
&a[i] == pa+i и a[i] == *(a+i)
т.е. операцию pa=i, увеличивающую значение указателя, можно интерпретировать как смещение вправо на i элементов базового типа. Все это означает, что всякое обращение к i-ому элементу массива или его адресу допустимо представлять как в индексной форме, так и на языке указателей.
Одно важное обстоятельство отличает массивы от указателей. Поскольку указатели являются переменными величинами, то оказываются допустимыми следующие адресные выражения
pa = pa+i или pa = a или pa++
Однако ввиду того, что имя массива есть константа, определяющая фиксированный адрес размещения этого массива в памяти ЭВМ, операции вида
a = pa a = a+i a++ pa = &a
следует считать лишенными какого-либо смысла.
Продрлжая далее анологию массивов и указателей, необходимо разрешить индексирование указателей, полагая
pa[i] == *(pa+i) или &pa[i] == pa+i
что является совершенно естественным, если обозначение pa[i] понимать
как взятие значения по адресу pa+i. Индексируя элементы массива, мы
по сути дела находимся в рамках того же самого соглашения.
Заметим, что было бы грубой ошибкой считать, что описания int a[10];
int *pa;
полностью равносильны одно другому. Дело в том, что в первом случае
определен адрес начала массива и выделено место в памяти ЭВМ, достаточное для хранения десяти его элементов. Во втором случае указатель имеет неопределенное (нулевое) значение и не ссылается ни на какую связную цепочку байт. Для того, чтобы указатель стал полностью эквивалентен массиву, необходимозаставить его ссылаться на область памяти соответствующей длины. Это можно сделать при помощи стандартных функций malloc() и alloca(), захватывающих требуемое количество байт памяти и возвращающих адрес первого из них. Так, например, после выполнения оператора
pa = (int*)alloca(10*sizeof(int));
определенные выше массив а и указатель ра становятся в полном смысле эквивалентными. Однако второе решение будет более гибким, ибо здесь затребованная память выделяется динамически в процессе выполнения программы и может быть при необходимости возвращена системе с помощью функции free(), чего нельзя сделать в случае массива.
Указатели также можно использовать для представления и обработки символьных строк. Так, например, описание
char string[] = "Это строка символов";
определяет массив из 20 символов типа char, инициализируя их символами строки. Обращение к какому-либо элементу этого массива обеспечивает доступ к отдельному символу, а адрес начала строки равен &string[0]. С другой стороны, ввиду того, что строковая константа в правой части нашего описания отождествляется компилятором с адресом первого символа, правомерной является запись следующего вида:
char *strptr = "Это строка символов";
инициализирующая указатель значением адреса строки-константы. Различие двух приведенных описаний такое же, как и отмеченное выше различие массивов и указателей. Так во втором случае мы могли бы написать
strptr = strptr + 4;
сместив тем самым указатель на начало второго слова строки. Более того, является допустимым присваивание
strptr = "Это другая строка символов";
изменяющее значение указателя (но не выполняющее копирования строки символов !). Подобная операция не имеет смысла для имени массива, которое во внутреннем машинном представлении отождествляется с фиксированным адресом его нулевого элемента.
Кроме определенной выше операции увеличения указателя, можно также использовать операцию его уменьшения, что равносильно движению вдоль массива в направлении уменьшения значения индекса. Более того, множество значений переменной-указателя является упорядоченным (т.к. упорядочены адреса оперативной памяти) и поэтому использование указателей в качестве операндов условных и логических выражений не противоречит семантическим правилам языка Си.
$ 3. УКАЗАТЕЛИ НА МАССИВЫ. МАССИВЫ УКАЗАТЕЛЕЙ И МНОГОМЕРНЫЕ МАССИВЫ
Введённое в предыдущем параграфе понятие указателя на простую переменную естественным образом распространяется на любые структурированные типы данных. В частности декларация
float ( *vektor) [15];
определяет имя vektor как указатель не массив из пятнадцати элементов типа float, причем круглые скобки в этой записи являются существенными. Обращение к i -ому элементу такого массива будет выглядеть следующим образом:
(*vektor)[ i ]
Определяя указатель на массив, мы сохраняем все преимущества работы с указателями и, кроме того, требуем от компилятьра выделить реальную память для размещения элементов этого массива.
Так как сами по себе указатели являются переменными, то нетрудно построить ограниченный вектор элементов-указателей на некоторый базовый тип данных. Такие структуры в языке С принято называть массивами указателей. Их описание строится на той же синтаксической основе, что и описание обычных массивов. Например, описание
char *text[100]
определяет массив из ста указателей на переменны (элементы данных) типа char/ Поскольку каждый отдельный элемент этого массива может хранить адрес некоторой строки символов, то весь массив будет задавать набор ста строк неопределённой, вообще говоря, длины. Элементы массива указателей могут быть инициализированы подобно тому, как инициализировались отдельные указатели и обычные массивы:
char *week[ ] = { «Понедельник»,
«Вторник»,
«Среда»,
«Четверг»,
«Пятница»,
«Суббота»,
«Воскресенье»
};
week[0] – адрес 0 строки («Понедельник»), week[ 1] [ 3 ] – буква р в слове Вторник. Аналогично week[3] – адрес 3 строки ( «Четверг»).