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