Указатели в языке программирования С
Теоретическая часть
По краткому определению, указатель – это переменная, содержащая адрес другой переменной [7.1]. Так как указатель содержит адрес переменной (объекта), это дает возможность "косвенного" доступа к этой переменной (объекту) через указатель.
Для понимания работы и назначения указателей рассмотрим упрощенную схему организации памяти компьютера. Память представляет собой массив последовательно пронумерованных или адресованных ячеек, с которыми можно работать по отдельности или связанными кусками. Известно, что различным типам данных отводится определенное количество байтов памяти. Поэтому указатель – это группа ячеек, в которых может храниться адрес. Например, если переменная ch имеет тип char, а ptr (от латинского pointer – указатель) есть указатель на переменную ch, то взятие адреса переменной ch осуществляется с помощью унарного (одноместного) оператора &, т.е.
ptr = &ch;
Приведенная инструкция означает, что переменной ptr присваивается адрес ячейки ch. Принято считать, что ptr указывает на ch. Оператор & применяется только к объектам, расположенным в памяти: к переменным, элементам массива. Операндом оператора & не может быть ни выражение, ни константа, ни регистровая переменная [7.1]. Унарный оператор & называется еще оператором адресации [7.2].
Имена указателям даются в соответствии с правилами, принятыми в языке программирования С для обычных переменных.
Другая унарная операция * называется операцией ссылки по указателю ( indirection ), или разыменования ( dereferencing ). Если применить ее к указателю, то получим объект, на который он указывает. Рассмотрим пример. Пусть х и у – целые переменные, а *ptr – указатель на целую переменную. Поставим задачу присвоения переменной у значения переменной х с помощью указателя. Фрагмент С -кода будет следующий:
int x = 1, y = 2; int *ptr; // объявили указатель на целую переменную ptr = &x; // взяли адрес переменной х = 1 y = *ptr; // переменная у стала равной 1 *ptr = 0; // переменная х стала равной 0
В приведенных объявлениях новым является объявление указателя:
int *ptr;
Следует помнить, что любой указатель может указывать только объекты одного конкретного типа данных, заданного при объявлении [7.1].
Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает.
Одноместные (унарные) операции * и & имеют более высокий приоритет для своих операндов, чем арифметические операции.
Для указателей одного типа можно, например, выполнять присваивание без разыменования. Это вытекает из того, что указатели сами по себе являются переменными. Пусть определен еще один указатель типа int, например, ptr2.
Тогда возможно произвести присвоение:
ptr2 = ptr;
После присвоения указатель ptr2 будет указывать на ту же переменную, что и указатель ptr.
В языке С допустимы следующие (основные) операции над указателями: присваивание; получение значения того объекта, на который ссылается указатель (синонимы: косвенная адресация, разыменование, раскрытие ссылки); получение адреса самого указателя; унарные операции изменения значения указателя; аддитивные операции и операции сравнений (отношений) [7.3].
С помощью унарных операций "++" и "––" числовые (арифметические) значения переменных типа указатель меняются по-разному в зависимости от типа данных, с которыми связаны эти переменные [7.3]. Если указатель связан с типом char, то при выполнении операций "++" и "––" его числовое значение изменяется на 1 (единицу). Если указатель связан с типом int, то операции "++" и "––" изменяют числовые значения указателей на 2. Указатель, связанный с типами float или long, унарными операциями "++" и "––" изменяется на 4. Таким образом, при изменении указателя на единицу указатель "переходит к началу" следующего (или пр едыдущего) поля той длины, которая определяется типом.
Следует особо остановиться на указателях и квалификаторе (модификаторе) const. Как известно, квалификатор const превращает переменную в константу, значение которой не должно меняться. Например, нет смысла изменять число . Значение константы должно быть инициализировано в месте ее определения. В связи с этим различают указатели на константы и константные указатели [7.6]. Приведем следующий пример:
long value = 9999L; const long *pvalue = &value;
Последняя строчка приведенного кода определяет собой указатель на константу. Попытка указателю pvalue присвоить иное числовое значение будет восприниматься компилятором как ошибка [7.6]. Но само значение переменной value изменять допустимо. При этом указатель держит адрес переменной, значение которой изменилось. В тоже время саму переменную также можно объявить с помощью квалификатора const. В этом случае нельзя изменять ни переменную, ни значение указателя (т. е. присвоить иное числовое значение указателю). Указатели на константы часто используются как формальные параметры функций (о функциях будет сказано позднее).
Константный указатель может адресовать как константу, так и переменную. В случае, когда определен константный указатель, то через него нельзя уже брать адрес другой переменной. Приведем следующий пример определения константного указателя:
int count = 43; int *const pcount = &count;
Вторая строчка приведенного кода определяет и инициализирует константный указатель pcount, который "привязан" к адресу переменной count. Если определить новую переменную того же типа, то взять адрес новой переменной с помощью константного указателя pcount будет нельзя, компилятор сделает сообщение об ошибке и работа программы будет невозможной. В тоже время возможно изменить значение константного указателя через другое числовое значение. Но это повлечет за собой изменение переменной, на которую указатель ссылается. Например,
int count = 43; int *const pcount = &count; *pcount = 345;
В соответствии с приведенным кодом переменная count будет иметь значение 345 [7.6].
Соответственно, если константный указатель ссылается на константный объект (например, на константную переменную), то в этом случае ни значение объекта, на который ссылается такой указатель, ни значение самого указателя (когда будет сделана попытка присвоить иное числовое значение указателю) не может быть изменено в программе. Например,
const int card = 21; const int *const pcard = &card
Указанные особенности для указателей с квалификатором const присущи и для переменных (объектов) других типов.
Указатели, значения которых изменять нельзя используются, например, при заполнении константных таблиц.
Практическая часть
Пример 1. Напишите программу определения адресов целых чисел от 0 до 9 и строчных букв латинского алфавита.
Программный код решения примера:
#include <stdio.h> #include <conio.h> int main (void) { int i, j = 0; char c = 'a', *ptr2; ptr2 = &c; printf("\n\t Figures, symbols and their addresses:\n"); for (i = 0; i < 10; ++i) printf("\n\t %3d) %2d --> %5p", i + 1, i, &i); printf("\n"); for ( ; *ptr2 <= 'z'; (*ptr2)++) printf("\n\t %3d) %2c --> %5p", ++j, *ptr2, ptr2); printf("\n\n Press any key: "); _getch(); return 0; }
Результат выполнения программы показан на рис. 7.1.
В программе использован спецификатор формата %5p для определения адреса переменных. Число 5 определяет отступ от левого края на пять позиций.
Задание 1
- Добавьте вывод кодов цифр и букв, для которых определены адреса в памяти компьютера.
- В программе вместо операторов цикла for примените операторы while.
- В программу введите указатель на тип int и применить этот указатель по аналогии с указателем *ptr2.
- Добавьте определение адресов прописных букв латинского алфавита и вывести их дополнительным столбцом к адресам строчных букв.
- Выведите в столбец свою фамилию (буквами латинского алфавита), имя и адреса соответствующих букв фамилии и имени.
Пример 2. Напишите программу однозначного задания типа разностей указателей, и определения адресов заданных указателей.
Для решения данного примера подключим заголовок stddef.h для определения типа разности указателей с помощью зарезервированного имени типа ptrdiff_t.
Программный код решения примера:
#include <stdio.h> #include <conio.h> #include <stddef.h> int main (void) { int x, y; int *px, *py; ptrdiff_t z; // инициализация указателей px = &x; py = &y; // разница двух указателей z = px - py; printf("\n The difference of two pointers to %p and %p is: %d", px, py, (int) z); printf("\n\n The addresses are: px = %p, py = %p\n", &px, &py); printf("\n Press any key: "); _getch(); return 0; }
Результат выполнения программы показан на рис. 7.2.