Указатели

advertisement
Адреса и указатели

Во время выполнения всякой программы, используемые ею данные
размещаются в оперативной памяти компьютера, причем каждому
элементу данных ставится в соответствие его индивидуальный адрес

При реализации многих алгоритмов часто оказывается полезно
непосредственно работать с адресами памяти. Подобная ситуация
возникает, например, при обработке массивов переменных

Действительно, поскольку соседние элементы массива располагаются
в смежных ячейках памяти, то для перехода от одного его элемента к
другому можно вместо изменения значения индексного выражения
манипулировать адресами этих элементов.

Предположим для определенности, что нулевой элемент
целочисленного массива расположен в ячейке памяти с номером A0.
Тогда, зная, что длина элемента данных типа int составляет четыре
байта, нетрудно вычислить номер ячейки, в которой будет находиться
i-ый элемент этого массива:
Ai = Ao + 4*i
Если массив двумерный
int a[4][5];
то A[i][j] элемент имеет адрес
Ai = Ao + 5*4*j + 4*i

На первый взгляд работа с адресами может показаться утомительной и
бесполезной. На самом же деле она является даже более естественной,
нежели работа с индексами, поскольку в процессе компиляции
программы всякое индексное выражение трансформируется в
операции над адресами. Это похоже на поиск комнаты 501 в
общежитии. Если вы нашли комнату 500, вам не нужно возвращаться
назад к 1 и считать снова. Достаточно добавить единицу к 500

Объекты языка Си, значениями которых являются адреса оперативной
памяти, получили название указателей

В общем случае указатели являются переменными величинами и над
ними можно выполнять определенный набор операций подобно тому,
как мы оперировали обычными числовыми переменными


В языке Си всякий указатель имеет базовый тип, который совпадает с
типом элемента данных, на который может ссылаться этот указатель.
Такое соглашение существенно упрощает и делает значительно более
эффективной работу с указателями
Переменные-указатели, как и переменные любых других типов, перед
их использованием в программе должны быть предварительно
объявлены в одной из инструкций описания данных. В случае
указателей на простые переменные это делается следующим образом:
<sc-specifier> type-specifier *identifier <, ... >;

Здесь type-specifier задает тип переменной, на которую
ссылается указатель с именем identifier, а символ звездочка (*)
определяет саму переменную как указатель

Приведем несколько примеров правильного описания указателей в
программе:
int *ptr;
long *sum;
float *result, *value;

Каждая из этих инструкций говорит о том, что соответствующая
переменная есть указатель на элемент данных определенного типа, а
комбинация, например, вида *ptr представляет собой величину типа
int. По существу это означает, что подобные комбинации могут
использоваться как операнды произвольных выражений

В частности, сохраняя обозначения предыдущего примера, мы могли
бы написать
int
int
sum
ptr
for
*sum, *ptr;
x=0,i;
= &x;
= &i;
(*ptr = 1; *ptr <= 100; (*ptr)++)
*sum = *sum + (*ptr)*(*ptr);
Фрагмент программы вычисления суммы квадратов первых ста
натуральных чисел. Круглые скобки в корректирующем выражении
оператора цикла являются существенными

Строго говоря, компилятор языка Си рассматривает комбинации вида
*identifier в составе выражений как некоторую операцию над
указателями. Эта операция, символом которой как раз и является
звездочка перед именем указателя, носит название операции
косвенной адресации и служит для доступа к значению,
расположенному по заданному адресу

Существует и другая операция, в определенном смысле
противоположная операции косвенной адресации и именуемая
операцией получения адреса. Она обозначается символом амперсанда
(&) перед именем простой переменной или элемента массива:
&identifier или &identifier[expression]
и сопоставляет своему аргументу адрес его размещения в памяти, т.е.
указатель. Естественно, что этим аргументом может быть и указатель,
поскольку указатели, как и другие переменные, хранятся в ячейках
оперативной памяти

Всевозможные выражения, построенные с использованием указателей
или операторов * и &, принято называть адресными выражениями, а
сами арифметические операции над указателями - адресной
арифметикой

Одноместные операции * и & имеют такой же высокий приоритет, как
и другие унарные операции, и в составе выражений обрабатываются
справа налево. Именно по этой причине мы обратили внимание на
необходимость круглых скобок в выражении (*ptr)++ предыдущего
примера, ибо без них оператор ++ относился бы к указателю ptr, а не
к значению, на которое ссылается этот указатель

Замечание. Если, например, mas есть массив переменных, то
выражениe &mas[0] равносильно простому употреблению имени
массива без следующего за ним индексного выражения, поскольку
последнее отождествляется с адресом размещения в памяти самого
первого элемента этого массива

Вот несколько примеров использования указателей и адресных
выражений
1. Аргументами функции форматированного ввода scanf являются
адреса переменных, которым должны быть присвоены прочитанные
значения:
scanf("%d",&n);
2. Следующая пара операторов
px = &x;
y = *px;
где переменная px объявлена предварительно
равносильна непосредственному присваиванию
y = x;
как
указатель,
Отождествление массивов и указателей
Адресная арифметика

Нам предстоит разобраться в том, какой смысл следует вкладывать в
арифметические операции над указателями и в каком отношении
между собой находятся массивы и указатели

Рассмотрим в качестве примера следующее описание
int a[10];
определяющее массив из десяти элементов типа int. Поскольку
a==&a[0] , то адрес элемента a[i] равен
a + sizeof(int)*i

Хотя приведенная запись и отражает существо дела, тем не менее она
является неудобной из-за своей громоздкости. Действительно,
учитывая, что всякий элемент массива a имеет тип 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]
мы устанавливаем указатель pa на нулевой элемент массива a и
поэтому справедливы равенства
&a[i] == pa+i и a[i] == *(pa+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;
полностью равносильны одно другому
int a[10];
int *pa;

Дело в том, что в первом случае определен адрес начала массива и
выделено место в памяти компьютера, достаточное для хранения
десяти его элементов

Во втором же случае указатель имеет неопределенное значение и не
ссылается ни на какую связную цепочку байт.

Для того, чтобы указатель стал полностью эквивалентен массиву,
необходимо заставить его ссылаться на область памяти
соответствующей длины. Это можно сделать при помощи
стандартных функций malloc() и alloca(), захватывающих
требуемое количество байт памяти и возвращающих адрес первого из
них

Так, например, после выполнения оператора
pa = (int*)malloc(10*sizeof(int));

определенные выше массив a и указатель pa становятся в полном
смысле эквивалентными. Однако второе решение будет более гибким,
ибо здесь затребованная память выделяется динамически в процессе
выполнения программы и может быть при необходимости возвращена
системе с помощью функции free(), чего нельзя сделать в случае
массива

Остановимся особо на вопросе использования указателей для
представления и обработки символьных строк. Поскольку в языке Си
нет специального типа данных, который можно было бы использовать
для описания символьных строк, последние хранятся в памяти
компьютера в виде массивов символов

Так, например, описание
char string[] = "Это строка символов";

определяет массив двадцати элементов типа char, инициализируя их
символами строки

Обращение к какому-либо элементу этого массива обеспечивает
доступ к отдельному символу, а адрес начала строки равен
&string[0]

С другой стороны, ввиду того, что строковая константа в правой части
нашего описания отождествляется компилятором с адресом ее первого
символа, правомерной является запись следующего вида:
char *strptr = "Это строка символов";

инициализирующая указатель значением адреса строки-константы.
Различие двух приведенных описаний такое же, как и отмеченное
выше различие массивов и указателей. Так, во втором случае мы
могли бы написать
strptr = strptr + 4;
сместив тем самым указатель на начало второго слова строки

Кроме определенной выше операции увеличения указателя, можно
также определить операцию его уменьшения, что равносильно
движению вдоль массива в направлении уменьшения значений
индекса

Более того, множество значений переменной-указателя является
упорядоченным (ибо упорядочены адреса оперативной памяти) и
поэтому использование указателей в качестве операндов условных и
логических выражений не противоречит семантическим правилам
языка Си
Указатели на массивы
Массивы указателей и многомерные массивы

Введенное в предыдущем параграфе понятие указателя на простую
переменную естественным образом распространяется на любые
структурированные типы данных. В частности, декларация
float (*vector)[15];

определяет имя vector как указатель на массив пятнадцати
элементов типа float, причем круглые скобки в этой записи
являются существенными. Обращение к i-ому элементу такого
массива будет выглядеть следующим образом:
(*vector)[i]

Определяя указатель на массив, мы сохраняем все преимущества
работы с указателями и, кроме того, требуем от компилятора выделить
реальную память для размещения элементов этого массива

Так как сами по себе указатели являются переменными, то нетрудно
построить ограниченный вектор элементов-указателей на некоторый
базовый тип данных. Такие структуры данных в языке Си принято
называть массивами указателей. Их описание строится на той же
синтаксической основе, что и описание обычных массивов

Например, инструкция
char *text[300];
определяет массив трехсот указателей на элементы данных типа char

Поскольку каждый отдельный элемент этого массива может хранить
адрес начала некоторой цепочки символов, то после фактического
выделения памяти под размещение трехсот таких цепочек и
присвоения адреса каждой из них определенному элементу массива,
весь массив указателей будет задавать набор соответствующего
количества строк переменной

Элементы массива указателей могут быть инициализированы подобно
тому, как инициализировались отдельные указатели и обычные
массивы:
char *week[] = { "Понедельник",
"Вторник",
"Среда",
"Четверг",
"Пятница",
"Суббота",
"Воскресенье"
};

Вспоминая проведенную аналогию между массивами и указателями,
можно сказать, что массив указателей в определенном смысле
эквивалентен "массиву массивов "
char table[10][20];
определяет массив десяти массивов, каждый из которых содержит по
двадцать элементов типа char

Легко заметить, что это есть ни что иное, как синоним двумерного
массива, причем первый индекс определяет номер строки, а второй номер столбца

Очевидно, что желая сохранить тесную связь массивов и указателей,
следует потребовать, чтобы двумерные массивы размещались в
памяти компьютера по строкам, отождествив имя массива с адресной
ссылкой &table[0][0]

Обращение же к индивидуальным элементам двумерного массива
осуществляется, как и в случае одного измерения, посредством
индексных выражений

Отличие массива указателей от массива массивов состоит, главным
образом, в том, что в первом случае резервируются лишь ячейки
памяти для хранения адресов строк двумерной таблицы, в то время
как реальная память под размещение элементов каждой строки не
выделяется. Во втором же случае полностью определен объем памяти,
занимаемой всей таблицей
С другой стороны, общее сходство между двумя этими структурами
данных позволяет работать с массивами указателей точно так же, как
и с двумерными массивами, используя, например, двойную
индексацию week[2][3] для выделения четвертого по счету
символа в третьей строке, и наоборот, рассматривая ссылку вида
table[i] как адрес нулевого элемента i-ой строки таблицы table


Такое соглашение выглядит достаточно естественным, если вместо
термина "многомерный массив" всюду использовать понятие "массив
массивов«

Проведенная аналогия между массивами указателей и массивами
массивов дает возможность придать вполне конкретный смысл
выражению вида table[i]+k, задающему адрес k-ого элемента i-ой
строки массива table, который в терминах операции взятия адреса
определяется как &table[i][k]

Поэтому наряду с традиционной ссылкой table[i][k] на значение
элемента (i, k) этого массива можно пользоваться эквивалентной ей
ссылкой *(table[i] + k) на языке указателей

Поскольку имя всякого массива при отсутствии индексных выражений
отождествляется с адресом его самого первого элемента, видно, что
выражение table+j является обычным адресным выражением,
определяющим размещение в памяти нулевого элемента j-ой строки
таблицы table

Массивы указателей обеспечивают возможность более гибкого
манипулирования данными, нежели многомерные массивы.
Дальнейшее увеличение гибкости структур данных связано с
понятием косвенного указателя или " указателя на указатель", который
может быть определен следующим образом:
<sc_specifier> type-specifier **identifier <,... >;

Здесь вновь сохраняется аналогия с рассмотренными выше
объектами, т.е. такое описание окажется полностью равносильным
двумерному массиву после того, как будет выделена реальная память
под хранение адресов его строк и размещение элементов каждой
отдельной строки

Это можно сделать, используя, например, функцию malloc() или
alloca()
double **dataptr;
dataptr = (double**)alloca(m*sizeof(double*));
for (i = 0; i < m; i++)
dataptr[i] = (double*)alloca(n*sizeof(double));

В
последнем
примере
осуществляется
размещение
в
памятикомпьютера двумерного массива размера m*n элементов типа
double

Продолжая начатые построения, можно было бы по индукции ввести
понятия массива произвольного числа измерений и указателя любого
уровня косвенности, имея в виду установленную выше
эквивалентность между этими объектами в одномерном и двумерном
случаях. Однако, учитывая сравнительно редкое практическое
использование таких структур данных и в то же время логическую
простоту их построения, мы не будем особо останавливаться здесь на
этом вопросе
Динамическое выделение памяти под массивы

В двух предыдущих параграфах при обсуждении вопроса об
эквивалентности массивов и указателей мы воспользовались
стандартными
функциями
malloc()
и
alloca()
для
динамического выделения памяти под хранение элементов массива.
Здесь будут рассмотрены некоторые детали затронутой проблемы

Во многих задачах вычислительной математики и при реализации
алгоритмов обработки информационных структур возникает
потребность работы с массивами, количество элементов которых
изменяется от одного прогона программы к другому. Простейшее
решение этой проблемы состоит в статическом описании
соответствующих массивов с указанием максимально необходимого
количества элементов. Однако такой подход приводит, как правило, к
неоправданному завышению объема памяти, требуемой для работы
программы. Альтернативное решение открывается в связи с
использованием указателей для представления массивов переменных

Пусть нам необходимо написать программу скалярного умножения
векторов A и B, размерность которых заранее не известна. Для этого
поступим следующим образом. Опишем в заголовке программы
переменную m, определяющую длину соответствующих массивов, и
указатели a, b, c, которые будут определять размещение в памяти
векторов-сомножителей и вектора-результата:
int m;
float *a, *b, *c;

После того, как значение m будет определено (оно может быть,
например, введено с клавиатуры терминала), необходимо выделить
достаточный объем памяти для хранения всех трех векторов.
Поскольку речь здесь идет о динамическом размещении массивов в
процессе выполнения программы, мы должны воспользоваться одной
из трех специальных функций, входящих в состав стандартной
библиотеки и сведения о которых приведены ниже


Имя функции и назначение: alloca - резервирует size байт памяти из
ресурса программного стека; выделенная память освобождается по
завершении работы текущей программной компоненты.
Формат и описание аргументов:
void *alloca(size)
int size;
/* Требуемое количество байт памяти */

Возвращаемое значение является указателем типа char на первый
байт зарезервированной области программного стека и равно NULL
при отсутствии возможности выделить память требуемого размера.
Для получения указателя на тип данных, отличный от char,
необходимо применить к возвращаемому значению операцию явного
преобразования типа

Имя функции и назначение: calloc - резервирует память для
размещения n элементов массива, каждый из которых имеет длину
size байт, инициализируя все элементы нулями; выделенная память
освобождается по завершении работы программы или при помощи
функции free()

Формат и описание аргументов:
void *calloc(n, size)
int n; /* Общее количество элементов в массиве */
int size; /* Длина в байтах каждого элемента */

Возвращаемое значение является указателем неопределенного типа на
первый байт зарезервированной области статической памяти и равно
NULL при отсутствии возможности разместить требуемое количество
элементов заданной длины. Для получения указателя на конкретный
тип данных, необходимо применить к возвращаемому значению
операцию явного преобразования типа

Имя функции и назначение: malloc - резервирует блок памяти
размером size байт; затребованная память освобождается по
завершении работы программы или при помощи функции free()

Формат и описание аргументов:
void *malloc(size)
int size; /* Требуемое количество байт памяти */

Возвращаемое значение является указателем неопределенного типа на
первый байт зарезервированной области статической памяти и равно
NULL при отсутствии возможности выделить память требуемого
размера.

Для получения указателя на конкретный тип данных, необходимо
применить к возвращаемому значению операцию явного
преобразования типа

Предварительные описания всех этих функций помещены в файлы
stdlib.h и malloc.h и при их использовании один из них
должен быть включен в состав исходного текста программы при
помощи директивы препроцессора #include

Выбрав в нашей задаче для размещения массивов a, b и c какуюлибо из этих функций, например calloc() , можно записать:
a = (float*)calloc(m, sizeof(float));
b = (float*)calloc(m, sizeof(float));
c = (float*)calloc(m, sizeof(float));

где операция приведения (float*) преобразует
неопределенного типа в указатель типа float
указатель
Теперь, после предварительного ввода числовых значений элементов
векторов, может быть выполнено их скалярное умножение:
for (i = 0; i < m; i++)
c[i] = a[i]*b[i];
Для динамического размещения двумерного массива необходимо
воспользоваться косвенным указателем
int
m, n;
float **matr;
и выделять память в два этапа:
matr = (float**)malloc(m*sizeof(float*));
for (i = 0; i < m; i++)
matr[i] = (float*)calloc(n, sizeof(float));

После этого работа с matr может выполняться точно так же, как и с
обычным двумерным массивом

Память, затребованная у системы путем использования функций
calloc() и malloc() , может быть возвращена назад до полного
завершения работы программы при помощи функции free()

Имя функции и назначение: free - освобождает блок памяти,
предварительно зарезервированный одной из функций calloc() ,
malloc() или realloc()

Формат и описание аргументов:
void free(ptr)
void *ptr; /* Указатель на освобождаемый блок */

Эта функция в результате своей работы не возвращает никакого
значения. Кроме того, она игнорирует указатель ptr, если он равен
NULL
Инициализация указателей

Ввиду того, что с инициализацией указателей мы уже столкнулись при
их обсуждении в предыдущих параграфах, здесь будет рассмотрен
лишь один частный вопрос

Пусть необходимо разместить простую переменную или массив на
фиксированных адресах оперативной памяти. Для этого указатель на
соответствующий элемент или структуру данных должен быть
инициализирован числовым значением, определяющим абсолютный
физический адрес

Поскольку такая потребность чаще всего возникает при работе с
видеопамятью компьютера IBM PC, рассмотрим способ обращения к
ячейкам видеопамяти в алфавитно-цифровом режиме

Учитывая, что интересующая нас область памяти имеет сегментный
адрес 0xB800 и каждой позиции экрана отвечают два байта этой
памяти, достаточно определить массив элементов типа int,
расположив его по требуемому адресу

В том случае, когда видеосистема установлена в режим 25 строк по 80
символов, соответствующее описание должно иметь следующий вид:
(*vmem_16)[25][80] = 0xB8000000;

После этого занесению какой-либо информации во всякий элемент
массива (*vmem_16) будет соответствовать определенный эффект на
экране видеотерминала
Download