Васин Д.Ю. - Язык программирования Си. Курс лекций - 2003. Руководство для начинающих. М. Мир, 1988г. 512 с. Трой Д. Программирование на языке Си для персонального компьютера ibm pc Пер с англ. М. Радио и связь, 1991г. 432 с
Скачать 1.1 Mb.
|
4.16. Указатели и массивыУказатель - это переменная, содержащая адрес переменной. Указатели широко применяются в Си - отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее. Указатели и массивы тесно связаны друг с другом: в данной главе мы рассмотрим эту зависимость и покажем, как ею пользоваться. Наряду с goto указатели когда-то были объявлены лучшим средством для написания малопонятных программ. Так оно и есть, если ими пользоваться бездумно. Ведь очень легко получить указатель, указывающий на что-нибудь совсем нежелательное. При соблюдении же определенной дисциплины с помощью указателей можно достичь ясности и простоты. Мы попытаемся убедить вас в этом. Изменения, внесенные стандартом ANSI, связаны в основном с формулированием точных правил, как работать с указателями. Стандарт узаконил накопленный положительный опыт программистов и удачные нововведения разработчиков компиляторов. Кроме того, взамен char* в качестве типа обобщенного указателя предлагается тип void* (указатель на void). Для чего нужны указатели? Вот наиболее частые примеры их использования: Доступ к элементам массива Передача аргументов в функцию, от которой требуется изменить эти аргументы Передача в функции массивов и строковых переменных Выделение памяти Создание сложных структур, таких, как связный список или бинарное дерево. Идея указателей несложна. Каждый байт в памяти машины имеет свой уникальный адрес. Адреса начинаются с 0, а затем монотонно возрастают. Если у нас есть 1 Мб ОП, то максимальным адресом будет число 1 048 575. Загружаясь в память, наша программа занимает некоторое количество этих адресов. Это означает, что каждая переменная и каждая функция нашей программы начинается с некоторого конкретного адреса. 4.16.1.Операция получения адреса &Мы можем получить адрес переменной, используя операцию получения адреса &. Int xl=ll, x2=22, x3=33; &xl - возвращает адрес переменной xl; &х2 - возвращает адрес переменной х2; &хЗ - возвращает адрес переменной хЗ; Реальные адреса, занятые в программе, зависят от многих факторов, таких, как компьютер, на котором запущена программа, размер ОП, наличие другой программы в памяти и т.д. По этой причине от запуска к запуску программа будет возвращать в общем случае разные адреса. Пусть мы получили следующие значения Ox8f4ffff4 - адрес переменной xl; Ox8f4ffff2 - адрес переменной х2; Ox8f4ffffO - адрес переменной хЗ; Важно понимать, что адрес переменной - это не то же самое, что значение переменной. Содержимое трех переменных - это числа 11, 22 и 33. Заметим, что адреса отличаются друг от друга двумя байтами. Это произошло потому, что переменная типа int занимает в памяти два байта. Если бы мы использовали переменные типа char, то значения адресов отличались бы на единицу. 4.16.2. Переменные указателиАдресное пространство ограничено. Нам необходимы переменные, хранящие значение адреса. Нам знакомы переменные, хранящие знаки, целые или вещественные числа и т.д. Адреса хранятся точно так же. Переменная,содержащая в себе значение адреса, называется переменной-указателем или просто указателем. Какого же типа может быть переменная-указатель? Она не того же типа, что и переменная, адрес которой мы храним: указатель на int не имеет типа int. Int *ptr; - определение переменной-указателя на целое. * - означает указатель на. Т.е. переменная ptr может содержать в себе адрес переменной типа int. Компилятору необходимы сведения о том, какого именно типа переменная, на которую указывает указатель. Поэтому не существует обобщенный тип pointer такой, чтобы мы могли записать, например pointer ptr; 4.16.3. Указатели должны иметь значениеПусть у нас есть адрес Ox8f4ffff4, мы можем назвать его значением указателя. Указатель ptr называется переменной-указателем. Как переменная int xl может принимать значение, равное 11, так переменная-указатель может принимать значение, равное Ox8f4ffff4. Пусть мы определили некоторую переменную для хранения некоторого значения, но не проинициализировали ее. Она будет содержать некоторое случайное число. В случае с переменной-указателем это случайное число является некоторым адресом в памяти. Этот адрес может оказаться чем угодно: от кода нашей программы до кода ОС. Неинициализированный указатель может привести к краху ОС, его очень тяжело выявить при отладке программы, т.к. компилятор не выдает предупреждения о подобных ошибках. Поэтому всегда важно убедиться, что каждому указателю перед его использованием было присвоено значение, т.е. его необходимо проинициализировать определенным адресом, например ptr =&xl. 4.16.4. Доступ к переменной по указателюСуществует запись, позволяющая получить доступ к значению переменной по ее адресу int xl=ll,x2=22; int *ptr; ptr = &xl; printf("%d",*ptr); //печать содержимого через указатель ptr = &х2; printf("%d",*ptr); Выражение *ptr дважды встречается в нашей программе, позволяет нам получить значения переменных xl и х2. *, стоящая перед именем переменной, как в выражении *ptr, называется операцией разыменования. Эта запись означает: взять значение переменной, на которую указывает указатель. Таким образом, выражение *ptr представляет собой значение переменной, на которую указывает указатель ptr. *, используемая в операции разыменования,- это не то же самое, что звездочка, используемая при объявлении указателя. Операция разыменования предшествует имени переменной и означает значение, находящееся в переменной,на которую указывает указатель. * в объявлении указателя означает указатель на. Доступ к значению переменной, хранящейся по адресу, с использованием операции разыменования называется непрямым доступом или разыменованием указателя. 4.16.5 Указатель на voidОтметим одну особенность указателей. Адрес, который мы помещаем в указатель, должен быть одного с ним типа. Мы не можем присвоить указателю на int адрес переменной типа float. float fx=98.6; int ptr = &fx; //так нельзя, типы int * и float * несовместны Однако есть одно исключение. Существует тип указателя, который может указывать на любой тип данных. Он называется указателем на тип void и определяется следующим образом: void *ptr. Такие указатели предназначены для использования в определенных случаях, например, передача указателей в функции, работающие независимо от типа данных, на которые указывает указатель. int ix; float fx; int *iptr; float *fptr; void *vptr; iptr =&ix; fptr = &fx; vptr = &ix; vptr = &fx; Следующие строки недопустимы iptr - &fx; fptr = &ix; 4.16.6. Указатели-константы и указатели переменныеМожно ли записать *(аггау++), т.е использовать операцию увеличения вместо прибавления шага j к имени array? Нет, так писать нельзя, поскольку нельзя изменять константы. Выражение array является адресом в памяти, где размещен наш массив, поэтому array - это указатель константы. Мы не можем сказать аггау++, так же как не можем сказать 7++. Мы не можем увеличивать адрес, но имеем возможность увеличить указатель, который содержит этот адрес. int main() { int array[5]={3 1,54,77,52,93}; intj; int *prt; prt = array; for(j=0; j<5; j++) printf("%6d",*(prt++); return 0; } Здесь мы определили указатель на int и затем присвоили ему значение адреса массива. После этого мы можем получить доступ к элементам массива, используя операцию разыменования *(prt++). Переменная pit имеет тот же адрес, что и array, поэтому доступ к первому элементу осуществляется как и раньше. Но prt - это уже переменная-указатель, то мы ее можем увеличивать. После этого она уже будет указывать на первый элемент массива array. 4.16.7 Передача простой переменной в функциюРассмотрим функцию void summ(int a, int b, int *us). 3ий параметр объявлен в этой функции как указатель на int. Когда головная программа вызывает функцию summ, то она передает в качестве 3 аргумента адрес переменной summ(a, b,&s). Это не сама переменная, а ее адрес. Т.к. функция summ получает адрес переменной s, то она может применить к ней операцию разыменования *us = а+b. Т.к. us содержит адрес переменной s, то все действия, которые мы совершим с *us, мы в действительности совершим с s. 4.16.8. Передача массивовПусть мы хотим передать в функцию массив int A[10]. Прототип такой функции может выглядеть следующим образом void func(int[]). В этом случае мы обеспечиваем передачу массива по значению. Если мы хотим использовать аппарат указателей, то передача аргумента будет выглядеть как void func(int *). Т.к. имя массива является его адресом, то нет нужды использовать при вызове функции операцию взятия адреса, т.е можно записать func(A). 4.16.9.Указатели и адресаНачнем с того, что рассмотрим упрощенную схему организации памяти. Память типичной машины подставляет собой массив последовательно пронумерованных или проадресованных ячеек, с которыми можно работать по отдельности или связными кусками. Применительно к любой машине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые - как целые типа long. Указатель - это группа ячеек (как правило, две или четыре), в которых может храниться адрес. Так, если c имеет тип char, а p - указатель на c, то ситуация выглядит следующим образом: Унарный оператор & выдает адрес объекта, так что инструкция p = &c; присваивает переменной p адрес ячейки c (говорят, что p указывает на c). Оператор & применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная. Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает. Предположим, что x и y имеют тип int, а ip – укаэатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *. int х = 1, у = 2, z[10]; int *ip; ip - указатель на int ip = &x; теперь ip указывает на x y = *ip; y теперь равен 1 *ip = 0; x теперь равен 0 ip = &z[0]; ip теперь указывает на z[0] Объявления x, y и z нам уже знакомы. Объявление указателя ip int *ip; мы стремились сделать мнемоничным - оно гласит: "выражение *ip имеет тип int". Синтаксис объявления переменной "подстраивается" под синтаксис выражений, в которых эта переменная может встретиться. Указанный принцип применим и в объявлениях функций. Например, запись double *dp, atof (char *); означает, что выражения *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char. Вы, наверное, заметили, что указателю разрешено указывать только на объекты определенного типа. (Существует одно исключение: "указатель на void" может указывать на объекты любого типа, но к такому указателю нельзя применять оператор косвенного доступа) Если ip указывает на x целочисленного типа, то *ip можно использовать в любом месте, где допустимо применение x; например, *ip = *ip + 10; увеличивает *ip на 10. Унарные операторы * и & имеют более высокий приоритет, чем арифметические операторы, так что присваивание y = *ip + 1; берет то, на что указывает ip, и добавляет к нему 1, а результат присваивает переменной y. Аналогично *ip += 1; увеличивает на единицу то, на что указывает ip; те же действия выполняют ++*ip; и (*iр)++; В последней записи скобки необходимы, поскольку если их не будет, увеличится значение самого указателя, а не то, на что он указывает. Это обусловлено тем, что унарные операторы * и ++ имеют одинаковый приоритет и порядок выполнения - справа налево. И наконец, так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа. Например, если iq есть другой указатель на int, то iq = ip; копирует содержимое ip в iq, чтобы ip и iq указывали на один и тот же объект. 4.16.10. Указатели и аргументы функцийПоскольку в Си функции в качестве своих аргументов получают значения параметров, нет прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции. В программе сортировки нам понадобилась функция swap, меняющая местами два неупорядоченных элемента. Однако недостаточно написать swap(a, b); где функция swap определена следующим образом: void swap(int х, int у) /* НЕВЕРНО */ { int temp; temp = х; x = y; у = temp; } Поскольку swap получает лишь копии переменных a и b, она не может повлиять на переменные a и b той программы, которая к ней обратилась. Чтобы получить желаемый эффект, вызывающей программе надо передать указатели на те значения, которые должны быть изменены: swap(&a, &b); Так как оператор & получает адрес переменной, &a есть указатель на a. В самой же функции swap параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно. void swap(int *px, int *py) /* перестановка *px и *py */ { int temp; temp = *рх; *рх = *py; *ру = temp; } Графически это выглядит следующим образом: в вызывающей программе: Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты. 4.16.11. Указатели и массивыВ Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее, но разобраться в нем, особенно непосвященному, довольно трудно. Объявление int a[10]; Определяет массив a размера 10, т. е. блок из 10 последовательных объектов с именами a[0], a[1], ..., a[9]. Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int, т. е. объявлен как int *pa; то в результате присваивания pa = &a[0]; pa будет указывать на нулевой элемент a, иначе говоря, pa будет содержать адрес элемента a[0]. Теперь присваивание x = *pa; будет копировать содержимое a[0] в x. Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i - на i-й элемент после pa, a pa-i - на i-й элемент перед pa. Таким образом, если pa указывает на a[0], то *(pa+1) есть содержимое a[1], a+i - адрес a[i], a *(pa+i) - содержимое a[i]. Сделанные замечания верны безотносительно к типу и размеру элементов массива a. Смысл слов "добавить 1 к указателю", как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, a pa+i - на i-й после pa. Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания pa = &a[0]; ра и a имеют одно и то же значение. Поскольку имя массива является синонимом расположения его начального элемента, присваивание pa=&a[0] можно также записать в следующем виде: pa = a; Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i). Вычисляя a[i], Си сразу преобразует его в *(a+i); указанные две формы записи эквивалентны. Из этого следует, что полученные в результате применения оператора & записи &a[i] и a+i также будут эквивалентными, т. е. и в том и в другом случае это адрес i-го элемента после a. С другой стороны, если pa - указатель, то его можно использовать с индексом, т. е. запись pa[i] эквивалентна записи *(pa+i). Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом. Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель - это переменная, поэтому можно написать pa=a или pa++. Но имя массива не является переменной, и записи вроде a=pa или a++ не допускаются. Указатели и массивы очень похожи. Оказывается, что доступ к элементам массива можно получить как используя операции с массивами, так и используя указатели. Рассмотрим два способа доступа к элементам массива int main() { int array[5]={31,54,77,52,93}; int j; for(j=0;j<5;j++){printf("%6d",arraj[j]); printf("%6d",*(array+j);} return 0; } Заметим, что результат действия выражения *(array+j) тот же, что и выражения arraj[j]. Вспомним, что имя массива является его адресом. Таким образом, выражение array+j – это адрес чего-то в массиве. Пусть j-З, тогда можно ожидать, что array+j будет означать 3 байта массива array. Поскольку array - это массив элементов типа int, и три байта в этом массиве - середина второго элемента, что не очень полезно для нас. Мы хотим получить четвертый элемент массива, а не его четвертый байт. a rray array[0] не (array+3) array[1] array[2] (array+3) array[3] array[4] Компилятору С достаточно получить размер данных в счетчике для выполнения вычислений с адресами данных. Ему известно, что array - массив типа int. Поэтому, видя выражение аггау+3, компилятор интерпретирует его как адрес четвертого элемента массива, а е четвертого байта. Но нам необходимо значение четвертого элемента, а не его адрес. Для его получения мы используем операцию разыменования(*). Поэтому результатом выполнения выражения *(аггау+3) будет значение четвертого элемента массива, то есть 52. Теперь становится понятно, почему при объявлении указателя мы должны указать тип переменной, на которую он будет указывать. Компилятору необходимо знать, на переменные какого типа указатель указывает, чтобы осуществлять правильный доступ к элементам массива.Он умножает значение индекса на 2, в случае типа int, или на 4 - в случае типа float и т.д. Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Формальные параметры char s[]; и char *s; в определении функции эквивалентны. Мы отдаем предпочтение последнему варианту, поскольку он более явно сообщает, что s есть указатель. Если функции в качестве аргумента передается имя массива, то она может рассматривать его так, как ей удобно - либо как имя массива, либо как указатель, и поступать с ним соответственно. Она может даже использовать оба вида записи, если это покажется уместным и понятным. Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a - массив, то в записях f(&a[2]) или f(a+2) функции f передается адрес подмассива, начинающегося с элемента a[2]. Внутри функции f описание параметров может выглядеть как f(int arr[]) {...} или f(int *arr) {...} Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения. Если есть уверенность, что элементы массива существуют, то возможно индексирование и в "обратную" сторону по отношению к нулевому элементу; выражения p[-1], p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0]. Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам. 4.16.12. Адресная арифметикаЕсли p есть указатель на некоторый элемент массива, то p++ увеличивает p так, чтобы он указывал на следующий элемент, а p+=i увеличивает его, чтобы он указывал на i-й элемент после того, на который указывал ранее. Эти и подобные конструкции - самые простые примеры арифметики над указателями, называемой также адресной арифметикой. Си последователен и единообразен в своем подходе к адресной арифметике. Это соединение в одном языке указателей, массивов и адресной арифметики - одна из сильных его сторон. В общем случае указатель, как и любую другую переменную, можно инициализировать, но только такими осмысленными для него значениями, как нуль или выражение, приводящее к адресу ранее определенных данных соответствующего типа. #define ALLOCSIZE 10000 /* размер доступного пространства */ static char allocbuf[ALLOCSIZE]; /* память для alloc */ static char *allocp = allocbuf; /* указатель на своб. место */ Объявление static char *allocp = allocbuf; определяет allocp как указатель на char и инициализирует его адресом массива allocbuf, поскольку перед началом работы программы массив allocbuf пуст. Указанное объявление могло бы иметь и такой вид: static char *allocp = &allocbuf[0]; поскольку имя массива и есть адрес его нулевого элемента. Си гарантирует, что нуль никогда не будет правильным адресом для данных, поэтому мы будем использовать его в качестве признака аварийного события, в нашем случае нехватки памяти. Указатели и целые не являются взаимозаменяемыми объектами. Константа нуль - единственное исключение из этого правила: ее можно присвоить указателю, и указатель можно сравнить с нулевой константой. Чтобы показать, что нуль - это специальное значение для указателя, вместо цифры нуль, как правило, записывают NULL - константу, определенную в файле Несколько важных свойств арифметики с указателями. Во- первых, при соблюдении некоторых правил указатели можно сравнивать. Если p и q указывают на элементы одного массива, то к ним можно применять операторы отношения ==, !=, <, >= и т. д. Например, отношение вида p < q истинно, если p указывает на более ранний элемент массива, чем q. Любой указатель всегда можно сравнить на равенство и неравенство с нулем. А вот для указателей, не указывающих на элементы одного массива, результат арифметических операций или сравнений не определен. (Существует одно исключение: в арифметике с указателями можно использовать адрес несуществующего "следующего за массивом" элемента, т. е. адрес того "элемента", который станет последним, если в массив добавить еще один элемент.) Во-вторых, указатели и целые можно складывать и вычитать. Конструкция p + n означает адрес объекта, занимающего n-е место после объекта, на который указывает p. Это справедливо безотносительно к типу объекта, на который указывает p; n автоматически домножается на коэффициент, соответствующий размеру объекта. Информация о размере неявно присутствует в объявлении p. Если, к примеру, int занимает четыре байта, то коэффициент умножения будет равен четырем. Допускается также вычитание указателей. Например, если p и q указывают на элементы одного массива и p , то q-p+1 есть число элементов от p до q включительно |