Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Лекция 25 Указатели, массивы и структуры
Цели лекции: Изучение принципы работы с указателями и структурными типами данных (массивы и структуры).
Указатель - это переменная, содержащая адрес переменной. Указатели широко применяются в Си - отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее. Указатели и массивы тесно связаны друг с другом: в данной главе мы рассмотрим эту зависимость и покажем, как ею пользоваться. Наряду с goto указатели когда-то были объявлены лучшим средством для написания малопонятных программ. Так оно и есть, если ими пользоваться бездумно. Ведь очень легко получить указатель, указывающий на что-нибудь совсем нежелательное. При соблюдении же определенной дисциплины с помощью указателей можно достичь ясности и простоты. Мы попытаемся убедить вас в этом. Изменения, внесенные стандартом ANSI, связаны в основном с формулированием точных правил, как работать с указателями. Стандарт узаконил накопленный положительный опыт программистов и удачные нововведения разработчиков компиляторов. Кроме того, взамен char* в качестве типа обобщенного указателя предлагается тип void* (указатель на void).
Указатели и адреса. Память типичной машины подставляет собой массив последовательно пронумерованных или проадресованных ячеек, с которыми можно работать по отдельности или связными кусками. Упрощенная схема организации памяти изображена на рисунке 25.1.
Рисунок 25.1
Применительно к любой машине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые - как целые типа long. Указатель - это группа ячеек (как правило, две или четыре), в которых может храниться адрес. Так, если c имеет тип char, а p - указатель на c, то ситуация выглядит следующим образом: Унарный оператор & выдает адрес объекта, так что инструкция p = &c; присваивает переменной p адрес ячейки c (говорят, что p указывает на c). Оператор & применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная. Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает. Предположим, что x и y имеют тип int, а ip – укаэатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *.
Объявление указателя 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 указывали на один и тот же объект. Указатели и аргументы функций. Поскольку в Си функции в качестве своих аргументов получают значения параметров, нет прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции. В программе сортировки нам понадобилась функция swap, меняющая местами два неупорядоченных элемента. Однако недостаточно написать
где функция 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;}
Графически это представлено на рисунке 25.2. Рисунок 25.2
Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты. Рассмотрим, например, функцию getint, которая осуществляет ввод в свободном формате одного целого числа и его перевод из текстового представления в значение типа int. Функция getint должна возвращать значение полученного числа или сигнализировать значением EOF о конце файла, если входной поток исчерпан. Эти значения должны возвращаться по разным каналам, так как нельзя рассчитывать на то, что полученное в результате перевода число никогда не совпадет с EOF. Одно из решений состоит в том, чтобы getint выдавала характеристику состояния файла (исчерпан или не исчерпан) в качестве результата, а значение самого числа помещала согласно указателю, переданному ей в виде аргумента. Показанный ниже цикл заполняет некоторый массив целыми числами, полученными с помощью getint. int n, array[SIZE], getint (int *); for (n = 0; n < SIZE && getint (&array[n])!= EOF; n++);Результат каждого очередного обращения к getint посылается в array[n], и n увеличивается на единицу. Заметим, и это существенно, что функции getint передается адрес элемента array[n]. Если этого не сделать, у getint не будет способа вернуть в вызывающую программу переведенное целое число. В предлагаемом нами варианте функция getint возвращает EOF по концу файла; нуль, если следующие вводимые символы не представляют собою числа; и положительное значение, если введенные символы представляют собой число. #include <ctype.h> int getch (void);void ungetch (int); /* getint: читает следующее целое из ввода в *pn */int getint(int *pn){ int c, sign; while (isspace(c = getch())) ; /* пропуск символов-разделителей */ if(!isdigit(c) && c!= EOF && c!= '+' && c!= '-') { ungetch (c); /* не число */ return 0; } sign =(c =='-')? -1: 1; if (с == '+' || с == '-') с = getch(); for (*pn = 0; isdigit(c); c = getch()) *pn = 10 * *pn + (c -'0'); *pn *= sign; if (c!= EOF) ungetch(c); return c;}Везде в getint под *pn подразумевается обычная переменная типа int. Функция ungetch вместе с getch включена в программу, чтобы обеспечить возможность отослать назад лишний прочитанный символ.
Указатели и массивы. В Си существует связь между указателями и массивами, и связь эта настолько тесная. Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее.
Объявление int a[10]; Определяет массив a размера 10, т. е. блок из 10 последовательных объектов с именами a[0], a[1],..., a[9], изображенным на рисунке 25.3.
Рисунок 25.3
Запись 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], показанное на рисунке 25.4. Рисунок 25.4
Сделанные замечания верны безотносительно к типу и размеру элементов массива 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++ не допускаются. Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Мы можем воспользоваться отмеченным фактом и написать еще одну версию функции strlen, вычисляющей длину строки.
/* strlen: возвращает длину строки */ int strlen(char *s) { int n; for (n = 0; *s!= '\0'; s++) n++; return n; } Так как переменная s - указатель, к ней применима операция ++; s++ не оказывает никакого влияния на строку символов функции, которая обратилась к strlen. Просто увеличивается на 1 некоторая копия указателя, находящаяся в личном пользовании функции strlen. Это значит, что все вызовы, такие как: strlen("3дравствуй, мир"); /* строковая константа */ strlen(array); /* char array[100]; */ strlen(ptr); /* char *ptr; */ правомерны. Формальные параметры 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]. Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам.
Контрольные вопросы 1. Дать определение указателя. 2. Каким образом можно получить адрес переменной в явном виде? 3. Опишисать передачу параметров в функцию по указателю. 4. Дать определение массива. 5. Какой результат будет получен при разыменовании имени массива? 6. Что происходит при добавлении целочисленного значения n к указателю, адресующему некоторый элемент в массиве? 7. Каков результат вычитания из адреса любого элемента массива имени массива? 8. В чем отличие статического и динамического выделения памяти для массива? 9. В чем отличие функций динамического выделения памяти calloc() и malloc()?
|
||||||||
Последнее изменение этой страницы: 2021-02-07; просмотров: 174; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.137.221.116 (0.031 с.) |