Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Глава 4 УКАЗАТЕЛИ, МАССИВЫ, СТРОКИ В предыдущих главах были введены все базовые (основные) типы языка Си. Для их определения и описания используются служебные слова: char, short, int, long, signed, unsigned, float, double, enum, void. В языке Си, кроме базовых типов, разрешено вводить и использовать производные типы, каждый из которых получен на основе более простых типов. Стандарт языка определяет три способа получения производных типов: массив элементов заданного типа; указатель на объект заданного типа; функция, возвращающая значение заданного типа. С массивами и функциями мы уже немного знакомы по материалам главы 2, а вот указатели требуют особого рассмотрения. В языке Си указатели введены как объекты, значениями которых служат адреса других объектов либо функций. Рассмотрим вначале указатели на объекты. Указатели на объекты Адреса и указатели. Начнем изучение указателей, обратившись к понятию переменной. Каждая переменная в программе - это объект, имеющий имя и значение. По имени можно обратиться к переменной и получить (а затем, например, напечатать) ее значение. В операторе присваивания выполняется обратное действие - имени переменной из левой части оператора присваивания ставится в соответствие значение выражения его правой части. С точки зрения машинной реализации, имя переменной соответствует адресу того участка памяти, который для нее выделен, а значение переменной - содержимому этого участка памяти. Соотношение между именем и адресом условно представлено на рис. 4.1. E - имя Программный уровень ; ■ Значение Переменная Участок Содержимое памяти Машинный уровень Рис. 4.1. Соотношение между именем и адресом На рис. 4.1 имя переменной явно не связано с адресом, однако, например, в операторе присваивания Е=С+В; имя переменной Е адресует некоторый участок памяти, а выражение С+В определяет значение, которое должно быть помещено в этот участок памяти. В операторе присваивания адрес переменной из левой части оператора обычно не интересует программиста и недоступен. Чтобы получить адрес в явном виде, в языке Си применяют унарную операцию &. Выражение &Е позволяет получить адрес участка памяти, выделенного на машинном уровне для переменной Е. Операция & применима только к объектам, имеющим имя и размещенным в памяти. Ее нельзя применять к выражениям, константам-литералам, битовым полям структур (см. главу 6). Рисунок 4.2, взятый с некоторыми изменениями из [6], хорошо иллюстрирует связь между именами, адресами и значениями переменных. На рис. 4.2 предполагается, что в программе использована, например, такая последовательность определений (с инициализацией): char ch='G'; int date=1937; float summa=2.015E-6; Примечание В примере переменная ch занимает 1 байт, date - 2 байта и summa - 4 байта. В современных ПК переменная типа int может занимать 4 байта, а переменная типа float - 8 байтов. В соответствии с приведенным рисунком переменные размещены в памяти, начиная с байта, имеющего шестнадцатеричный адрес
Рис. 4.2. Разные типы данных в памяти ЭВМ 1A2B. При указанных выше размерах участков памяти в данном примере &ch = = 1A2B (адрес переменной ch); &date = = 1A2C; &summa = = 1A2E. Адреса имеют целочисленные беззнаковые значения, и их можно обрабатывать как целочисленные величины. Имея возможность с помощью операции & определять адрес переменной или другого объекта программы, нужно уметь его сохранять, преобразовывать и передавать. Для этих целей в языке Си введены переменные типа «указатель», которые для краткости будем называть просто указателями, если это не приводит к неоднозначности или путанице. Указатель в языке Си можно определить как переменную, значением которой служит адрес объекта конкретного типа. Кроме того, значением указателя может быть заведомо не равное никакому адресу значение, принимаемое за нулевой адрес. Для его обозначения в ряде заголовочных файлов, например в файле stdio.h, определена специальная константа NULL. Как и всякие переменные, указатели нужно определять и описывать, для чего используется, во-первых, разделитель '*'. В описании и определении переменных типа «указатель» необходимо сообщать, на объект какого типа ссылается описываемый указатель. Поэтому, кроме разделителя '*', в определения и описания указателей входят спецификации типов, задающие типы объектов, на которые ссылаются указатели. Примеры определения указателей: После определения указателя к нему применима унарная операция '*', называемая операцией разыменования, или операцией обращения по адресу. Операндом операции разыменования всегда является указатель. Результат этой операции - тот объект, который адресует указатель-операнд. Таким образом, *z обозначает объект типа char (символьная переменная), на который указывает z; *k - объект типа int (целая переменная), на который указывает k, и т. д. Обозначения *z, *i, *f имеют права переменных соответствующих типов. Оператор *z=' '; засылает символ «пробел» в тот участок памяти, адрес которого определяет указатель z. Оператор *k=*i=0; заносит целые нулевые значения в те участки памяти, адреса которых заданы указателями k, i. Обратите внимание на то, что указатель может ссылаться на объекты того типа, который присутствует в определении указателя. Исключением являются указатели, в определении которых использован тип void - отсутствие значения. Такие указатели могут ссылаться на объекты любого типа, однако к ним нельзя применять операцию разыменования, то есть операцию '*'. Скрытую в операторе присваивания E=B+C; работу с адресом переменной левой части можно сделать явной, если заменить один оператор присваивания следующей последовательностью: /* Определения переменных и указателя m: */ int E,C,B,*m; /* Значению m присвоить адрес переменной Е: */ m=&E; /* Переслать значение выражения C+B в участок памяти с адресом, равным значению m: */ *m=B+C; Данный пример не объясняет необходимости применения указателей, а только иллюстрирует их особенности. Возможности и преимущества указателей проявляются при работе с функциями, массивами, строками, структурами и т. д. Перед тем как привести более содержательные примеры использования указателей, остановимся подробнее на допустимых действиях с указателями. Операции над указателями. В языке Си допустимы следующие (основные) операции над указателями: присваивание; получение значения того объекта, на который ссылается указатель (синонимы: косвенная адресация, разыменование, раскрытие ссылки); получение адреса самого указателя; унарные операции изменения значения указателя; аддитивные операции и операции сравнений. Рассмотрим перечисленные операции подробнее. Операция присваивания предполагает, что слева от знака операции присваивания помещено имя указателя, справа - указатель, уже имеющий значение, либо константа NULL, определяющая условное нулевое значение указателя, либо адрес любого объекта того же типа, что и указатель слева. Если для имен действуют описания предыдущих примеров, то допустимы операторы: i=&date; k=i; z=NULL; Комментируя эти операторы, напомним, что выражение *имя_ указателя позволяет получить значение, находящееся по адресу, который определяет указатель. В предыдущих примерах было определено значение переменной date (1937), затем ее адрес присвоен указателю i и указателю k, поэтому значением *k является целое 1937. Обратите внимание, что имя переменной date и разыменования *i, *k указателей i, k обеспечивают в этом примере доступ к одному и тому же участку памяти, выделенному только для переменной date. Любая из операций *k=выражение, *i=выражение, date=выражение приведет к изменению содержимого одного и того же участка в памяти ЭВМ. Иногда требуется присвоить указателю одного типа значение указателя (адрес объекта) другого типа. В этом случае используется «приведение типов», механизм которого понятен из следующего примера: char *z; /* z- указатель на символ */ int *k; /* k - указатель на целое */ z=(char *)k; /* Преобразование указателей */ Подобно любым переменным, переменная типа указатель имеет имя, собственный адрес в памяти и значение. Значение можно использовать, например печатать или присваивать другому указателю, как это сделано в рассмотренных примерах. Адрес указателя может быть получен с помощью унарной операции &. Выражение &имя_указателя определяет, где в памяти размещен указатель. Содержимое этого участка памяти является значением указателя. Соотношение между именем, адресом и значением указателя иллюстрирует рис. 4.3.
Указатель А указателем А *A - объект, адресуемый Рис. 4.3. Имя, адрес и значение указателя С помощью унарных операций '++' и '—' числовые (арифметические) значения переменных типа указатель меняются по-разному в зависимости от типа данных, с которыми связаны эти переменные. Если указатель связан с типом char, то при выполнении операций '++' и '—' его числовое значение изменяется на 1 (указатель z в рассмотренных примерах). Если указатель связан с типом int (указатели i, k), то операции ++i, i++, —k, k— изменяют числовые значения указателей на 2. Указатель, связанный с типом float или long унарными операциями '++', '—', изменяется на 4. Размеры участков памяти указаны в соответствии с примечанием на стр. 142. Таким образом, при изменении указателя на единицу указатель «переходит к началу» следующего (или предыдущего) поля той длины, которая определяется типом. Аддитивные операции по-разному применимы к указателям, точнее имеются некоторые ограничения при их использовании. Две переменные типа указатель нельзя суммировать, однако к указателю можно прибавить целую величину. При этом вычисляемое значение зависит не только от значения прибавляемой целой величины, но и от типа объекта, с которым связан указатель. Например, если указатель, как в примере, относится к целочисленному объекту типа int, то прибавление к нему единицы увеличивает реальное значение на 2, то есть выполняется «переход» к адресу следующего участка. В отличие от операции сложения, операция вычитания применима не только к указателю и целой величине, но и к двум указателям на объекты одного типа. С ее помощью можно находить разность (со знаком) двух указателей (одного типа) и тем самым определять «расстояние» между размещением в памяти двух объектов. При этом «расстояние» вычисляется в единицах, кратных «длине» отдельного элемента данных того типа, к которому отнесен указатель. Например, после выполнения операторов int x[5], *i, *k, j; i=&x[0]; k=&x[4]; j=k-i; j принимает значение 4, а не 8, как можно было бы предположить, исходя из того, что каждый элемент массива x[ ] занимает два байта. В данном примере разность указателей присвоена переменной типа int. Однако тип разности указателей определяется по-разному в зависимости от особенностей компилятора и аппаратной платформы. Чтобы сделать язык Си независимым от реализаций, в заголовочном файле stddef.h определено имя (название) ptrdiff_t, с помощью которого обозначается тип разности указателей в конкретной реализации. В следующей программе используется рассмотренная возможность однозначного задания типа разности указателей. Программа будет корректно выполняться со всеми компиляторами, соответствующими стандартам языка Си. #include #include { int x[5]; int *i,*k; ptrdiff_t j; i=&x[0]; k=&x[4]; j=k-i; printf(“\nj=%d”,(int)j); } Результат будет таким: j=4 Арифметические операции и указатели. Унарные адресные операции '&' и '*' имеют более высокий приоритет, чем арифметические операции. Рассмотрим следующий пример, иллюстрирующий это правило: float a=4.0, *u, z; u=&z; *u=5; a=a + *u + 1; /* a равно 10, u - не изменилось, z равно 5 */ При использовании адресной операции '*' в арифметических выражениях следует остерегаться случайного сочетания знаков операций деления '/' и разыменования '*', так как комбинацию '/*' компилятор воспринимает как начало комментария. Например, выражение a/*u следует заменить таким: a/(*u) Унарные операции '*' и '++' или '--' имеют одинаковый приоритет и при размещении рядом выполняются справа налево. Добавление целочисленного значения n к указателю, адресующему некоторый элемент массива, приводит к тому, что указатель получает значение адреса того элемента, который отстоит от текущего на n позиций (элементов). Если длина элемента массива равна d байтов, то численное значение указателя изменяется на (d*n). Рассмотрим следующий фрагмент программы, иллюстрирующий перечисленные правила:
Указатели и отношения. К указателям применяются операции сравнения '>', '>=', '!=', '==', '<=', '<'. Таким образом, указатели можно использовать в отношениях. Но сравнивать указатели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса. Приведем пример, в котором используются операции над указателями и выводятся (печатаются) получаемые значения. Обратите внимание, что для вывода значений указателей (адресов) в форматной строке функции printf( ) используется спецификация преобразования %p. #include float x[ ] = { 10.0, 20.0, 30.0, 40.0, 50.0 }; void main( ) { float *u1, *u2; int i; printf("\n Адреса указателей: &u1=%p &u2=%p", &u1, &u2 ); printf("\n Адреса элементов массива: \n"); for(i=0; i<5; i++) { if (i==3) printf("\n"); printf(" &x[%d] = %p", i, &x[i]); } printf("\n Значения элементов массива: \n"); for(i=0; i<5; i++) { if (i==3) printf("\n"); printf(" x[%d] = %5.1f ", i, x[i]); } for(u1=&x[0], u2=&x[4]; u2>=&x[0]; u1++, u2--) { printf("\n u1=%p *u1=%5.1f u2=%p *u2=%5.1f",u1,*u1,u2,*u2); printf("\n u2-u1=%d", u2-u1); } } При печати значений разностей указателей и адресов в функции printf( ) использована спецификация преобразования %d - вывод знакового десятичного целого. Возможный результат выполнения программы (конкретные значения адресов могут быть другими): Адреса указателей: &u1=FFF4 &u2=FFF2 Адреса элементов массива: &x[0]=00A8 &x[1]=00AC &x[2]=00B0 &x[3]=00B4 &x[4]=00B8 Значения элементов массива: 150
На рис. 4.4 приводится схема размещения в памяти массива float x[5] и указателей до начала выполнения цикла изменения указателей. Рис. 4.4. Схема размещения в памяти массива и указателей Указатели и массивы Указатели и доступ к элементам массивов. По определению, указатель - это либо объект со значением «адрес объекта» или «адрес функции», либо выражение, позволяющее получить адрес объекта или функции. Рассмотрим фрагмент: int x,y; int *p =&x; p=&y; Здесь p - указатель-объект, a &x, &y - указатели-выражения, то есть адреса-константы. Мы уже знаем, что p - переменная того же типа, что и значения &x, &y. Различие между адресом (то есть указателем-выражением) и указателем-объектом заключается в возможности изменять значения указателей-объектов. Именно поэтому указатели-выражения называют указателями-константами или адресами, а для указателя-объекта используют название указатель- переменная или просто указатель. В соответствии с синтаксисом языка Си имя массива без индексов является указателем-константой, то есть адресом его первого элемента (с нулевым индексом). Это нужно учитывать и помнить при работе с массивами и указателями. Рассмотрим задачу «инвертирования» массива символов и различные способы ее решения с применением указателей (заметим, что задача может быть легко решена и без указателей - с использованием индексации). Предположим, что длина массива типа char равна 80. Первое решение задачи инвертирования массива: char z[80],s; char *d,*h; /* d и h — указатели на символьные объекты */ for (d=z, h=&z[79]; d |