Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
билеты информатика.rtf
Скачиваний:
39
Добавлен:
02.05.2015
Размер:
6.9 Mб
Скачать

14. Указатели

Указатель - это переменная, значением которой является адресс другой переменной. Так как указатель может ссылаться на переменные разных типов, с указателем в языке Си связывается тип того объекта, на который он ссылается. Для описания указателей используется операция косвенной адресации *. Например, указатель целого типа uk описывается так : int *uk. Унарная операция &, примененная к некоторой переменной, показывает, что нам нужен адресс этой переменной, а не ее текущее значение. Если переменная uk объявлена как указатель, то оператор присваивания uk=&x означает: "взять адресс переменной x и присвоить его значение переменной-указателю uk".

  Унарная операция *. примененная к указателю, обеспечивает доступ к содержимому ячейки памяти, на которую ссылается указатель. Например, *uk можно описать словами как "то, что содержится по адресу, на который указывает uk"Указатели могут использоваться в выражениях. Если. например, переменная uk указывает на целое x, то *uk может во всех случаях использоваться вместо x; так, *uk+1 увеличивает на единицу, а *uk=0равносильно x=0. Два оператора присваивания uk=&x;y=*uk; выполняет то же самое, что и один оператор y=x.Польза от применения указателей в таких ситуациях, мягко выражаясь, невелика.

   Наиболее полно их преимущества при обработке массивов и, в частности, символьных строк. Указатели и массивы тесно связаны друг с другом. Прежде чем рассмотреть эту связь подробно, заметим, что если uk - некоторый указатель, то uk++ увеличивает его значение и он теперь указывает на следющий, соседний адресуемый объект. Значение uk используется в выражении, а затем увеличивается. Аналогично определяются операции uk--, ++uk--uk. В общем случае указатель uk можно скаладывать с целым числом i. Оператор uk+=i передвигает ссылку на iэлементов относительно текущего значения. Эти конструкции подчиняются правилам адресной арифметики.

  А теперь вернемся к массивам. Пусть имеется описание int a[5]; Оно определяет массив размером 5 элементов, т.е. пять последовательно расположенных ячеек памяти a[0], a[1], a[2], a[3], a[4]. Адресс i-го элемента массива равен сумме адреса начального елемента массива и смещения этого элемента на единиц от начала массива. Это достигается индексированием: a[i]-i -й элемент массива. Но доступ к любому элементу массива может быть выполнен и с помощью указателей, причем, более эффективно. Если uk -указатель на целое, описанный как int *uk, то ukпосле выполнения операции uk=&a[0] содержит адресс a[0], а uk+i  указывает на i -й элемент массива. Таким образом, uk+i является адрессом a[i], а *(uk=1) - содержимым i- го элемента(операции и & более приоритетны, чем арифметические операции). Так как имя массива в программе отождествляется с адресом его первого элемента, то выражение uk=&a[0] эквивалентно такому: uk=a. Поэтому значение a[i] можно записать как *(a+i). Применив к этим двум элементам операцию взятия адреса, получим, что &a[i] и a+i идеитичны.

         Раньше, в связи с использованием функции scanf, мы говорили, что применение указателей в качестве аргументов функции дает способ обхода защиты значений вызывающей функции от действий вызванной функции. На примере 5.4 приведен текст программы с функцией obmen(x,y), которая меняет местами значения двух целых величин. Так как x,y - адреса переменных и b, то *x и *y обеспечивают косвенный доступ значениям и b. К сказанному добавим, что использование указателей позволяет нам обходить ограничения языка Си, согласно которым функциям может возращать только одно значение.

   Если в качестве функции передается имя массива, то оно фактически определяет адрес начала массива, т.е. является указателем. В качестве илюстрации напишем очередную версию функции length, вычисляющей длину строки. Если имя массива передается функции, то в самой функции можно исходить из того, что она работает с массивом, а можно исходить из того, что она работает с указателем.

  Пример 5.4

/*обмен a и b */obmen(int *x,int *y){int t;t=*x;*x=*y;*y=t;}#include <stdio.h>main(){int a,b;a=3;b=7;obmen(a,b);printf("a=%d b=%d",a,b);}

  В определении функции формальные параметры char s[] и char *s совершенно идеитичны. Операция s++ (пример 5.5увеличение на единицу текущее значение указателя, первоначально указывающее на первый символ строки, а операция *s!='\0' сравнивает очередной символс признаком конца строки.

         Пример 5.5

/*длина строки*/length(s)char *s;{int i;for(i=0; *s!='\0';s++)i+++;return(i);}

  Кроме ранее расмотренных операций адресной арифметики, к указателям можно применить операции сравнения ==и !=. Даже операции отношения Б <,>= и т.п. работают правильно, если указатели ссылаются на элементы одного и того же  массива. В последнем случае возможно даже вычитание ссылок: если и ссылаются на элементы одного массива, то u-s есть число элементов между и s. Используем этот факт для составления еще одной версии функцииlength (пример 5.6). Cначала указывает на первый символ строки (char *u =s). Затем в цикле по очереди проверяется каждый символ, пока в конце концов не будет обнаружен "\0". Разность u-s дает как раз длину строки.

        Пример 5.6

/*длина строки*/length(s)char *s;{char *u=s;while(*u!='\0')u++;return(u-s);}

Для илюстрации основных аспектов применения указателей в СИ рассмотрим функцию копирования строки s1 в строку s2. Сначала приведем версию, основанную на работе с массивами(пример 5.7). Для сравнения рядом помещена версия с использованием указателей(пример 5.8).

            Пример 5.7

/*копия строки*/copy(s1,s2)char s1[],s2[];{int i=0;while((s2[i]=s1[i])!='\0')i++;}

   Пример 5.8

/*копия строки*/copy(s1,s2)char *s1,*s2;{while((*s2=*s1)!='\0'){s2++;s1++;}}

   Здесь операция копирования помещена непосредственно в условие, определяющее момент цикла:while((*s2=*s1)!='\0'). Продвижение вдоль массивов вплоть до тех пор, пока не встретится "\0", обеспечивают операторы s2++ и s1++. Их, однако, тоже можно поместить в проверку (пример 5.9).

      Пример 5.9

/*копия строки*/copy(s1,s2)char *s1,*s2;{while((*s2++=*s1++)!='\0');}

Еще раз напомним, что унарные операции типа и ++ выполняются справа налево. Значение *s++ cесть символ, на который указывает до его увеличения. Так как значение "\0" есть нуль, а цикл while проверет, не нуль ли выражение в скобках, то это позволяет опустить явное сравнение с нулем(пример 6.0) . Так постепенно функция копирования становится все более компактной и ... все менее понятной. Но  в системном программировании предпостение чаще отдают именно компактным и, следовательно, более эффективным по быстродействиб программам.

        Пример 6.0

/*копия строки*/copy(s1,s2)char *s1,*s2;{while(*s2++=*s1++);}

   В языке Си, что некоторая литерная строка, выраженная как "строка" , фактически рассматривается как указатель на нулевой элемент массива " строка". Допускается, например, такая интересная запись:

            char *uk; uk="ИНФОРМАТИКА";

   Последний оператор присваивает указателю адрес нулевого элемента строки, т.е. символа "И". Возникает вопрос, где находится массив, содержащий символы "ИНФОРМАТИКА"? Мы его нигде не описывали. Ответ такой: эта строка - константа; она является частью функции, в которой встречается, точно также, как целая константа или символьная константа "А" в операторах i=4; c="A";. Более детально пояснит сказанное программа на пример 6.1, которая печатает строку символов в обратном порядке.

       Пример 6.1

#include <stdio.h>main(){char *uk1,*uk2;uk1=uk2="ИНФОРМАТИКА";while(*uk2!='\0')  putchar(*uk2++);putchar('\n');while(--uk2 >= uk1)putchar(*uk2);putchar('\n');}

В самом начале указателям uk1 и uk2 присваивается начальный адресс строки "ИНФОРМАТИКА". Затем строка посимвольно печатается и одновременно указатель uk2 смещается вдоль строки. В конце вывода uk2 указывает на последний символ исходной строки. Во втором цикле while все тот же указатель uk2 начинает изменяться в обратном направлении, уменьнаясь до тех пор, пока он не будет указывать на нулевой элемент массива, обеспечивая выдачу строки в обратном порядке.

15. указатели и массивы. Адресная арифметика

Язык Си имеет средства работы непосредственно с областями оперативной памяти ЭВМ, задаваемыми их адресами (указателями). В языке C указатели строго типизированы, т. е. различают указатели (адреса) символьных, целых, вещественных величин, а также типов данных, создаваемых программистом.

Для описания указателя на какой-либо тип данных перед именем переменной ставится *. Например в строке

  1. int *a, *b, c, d;    

описываются два адреса и две переменные целого типа. В строке

  1. double *bc;  

описан адрес переменной вещественного типа. Никогда не следует писать знак * слитно с типом данных, например как в следующей строке:

  1. int* a, b;  

В этой строке создается ложное впечатление о том, что описаны два указателя на тип int, в то время как на самом деле описан один указатель на int, а именно a, и одна переменная типа int.

Описание переменных заставляет компилятор выделять память для хранения этих переменных. Описание указателя выделяет память лишь для хранения адреса. В этом смысле указатель на целое данное и на тип double будут занимать в ЭВМ одинаковое количество байт памяти, зависящее от модели памяти, на которую настроен компилятор. Например, в 16-ти разрядной Small модели длина указателя равна двум байтам, а в 16-ти разрядной Large модели - четырем.

При описании указателей в качестве имени типа данных можно использовать ключевое слово void, например

  1. void *vd;  

При таком описании с указателем не связывается никакой тип данных, то есть получаем указатель на данное произвольного типа.

Для указателей одного и того же типа допустимой является операция присваивания, кроме того указателю типа void может быть присвоено значение адреса данного любого типа, но не наоборот, например

  1. int *a, *b;   

  2. double *d;   

  3. void *v;  

  4. ...  

  5. a = b; /* Правильно */  

  6. v = a; /* Правильно */  

  7. v = d; /* Правильно */  

  8. b = v; /* Неправильно */  

  9. d = a; /* Неправильно */   

В случае неправильного присваивания указателей компиляторы обычно выдают предупреждающие сообщения, которыми никогда не следует пренебрегать. Например, компилятор фирмы Borland выдает сообщение: "Suspicious pointer conversion", которое переводится как "Подозрительное преобразование указателей".

Если по какой-либо причине необходимо выполнить операцию присваивания между указателями разного типа, то следует использовать явное преобразование типов, например для указателей из предыдущего примера

  1. b = (int *) v;  

  2. d = (double *) a;   

При этом ответственность за корректность подобных операций целиком ложится на программиста. Действительно, в предыдущем примере a является указателем на ячейку памяти для хранения величины типа int. Обычно это ячейка размером 2 байта. После присваивания указателей с явным преобразования типов, делается возможным обращение к этой ячейке посредством указателя d, как к ячейке с величиной типа double. Размер этого типа обычно 8 байт, да и внутреннее представление данных в корне отличается от типа int. Никакого преобразования самих данных не делается, ведь речь идет только об указателях. Дальнейшая работа с указателем d скорее всего заденет байты, соседние с байтами на которые указывает a. Результат интерпретации этих байт будет тоже неверным.

Для поддержки адресной арифметики в языке Си имеются две специальные операции - операция взятия адреса & и операция получения значения по заданному адресу * (операция разадресации).

Операция & может применяться к любому объекту программы, адрес которого в принципе может быть определен. Результатом операции является указатель того же типа, что и тип объекта, имеющий значение адреса объекта. Если эту операцию применить к указателю, то результатом будет адрес ячейки памяти, в которой хранится значение указателя. Результат операции & можно использовать в любом выражении, где допускается использование указателя соответствующего типа.

Операция * может применяться только к указателям и только в том случае когда они типизированы. При необходимости применить эту операцию к указателю типа void следует использовать явное преобразование типов. Результатом операции * является значение того объекта, к адресу которого применялась операция *, тип результата совпадает с типом объекта. Результат операции * можно использовать в любом выражении, где допускается использование объекта соответствующего типа.

Рассмотрим работу вышеописанных операций на следующем примере

  1. int *p, a, b;   

  2. double d;   

  3. void *pd;  

  4. p = &a;  

  5. *p = 12;  

  6. p = &b;  

  7. *p = 20;  

  8. /* Здесь a содержит число 12, b - число 20 */  

  9. pd = &d;  

  10. *( (double *) pd ) = a;  

  11. /* Здесь d содержит число 12.0 */   

Поясним первые четыре присваивания рисунком, в котором прямоугольники изображают ячейки памяти для хранения величин типа int и указателей, внутри которых проставлены значения величин, а над ними записаны их названия и адреса.

Состояние ячеек до первого присваивания

P, адрес 1000

a, адрес 2000

b, адрес 4000

мусор

мусор

мусор

Состояние ячеек после присваивания p = &a

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

мусор

мусор

Состояние ячеек после присваивания *p = 12

p, адрес 1000

a, адрес 2000

b, адрес 4000

2000

12

мусор

Состояние ячеек после присваивания p = &b

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

мусор

Состояние ячеек после присваивания *p = 20

p, адрес 1000

a, адрес 2000

b, адрес 4000

4000

12

20

Описание указателя не является требованием на выделение памяти для хранения данных. Память выделяется только для хранения адреса. Поэтому прежде, чем использовать указатель, ему нужно присвоить значение адреса реально существующего объекта. В противном случае результат работы программы непредсказуем. Рассмотрим, например, следующую последовательность строк

  1. double *a, b;  

  2. b = *a;  

  3. *a = 135.7;    

В этой последовательности используется указатель, которому предварительно не присвоено никакого значения. В ячейке a находится произвольное значение, возможно оставшееся от работы предыдущей программы. Первая операция присваивания приведет к тому, что переменная b получит значение из ячейки памяти с непредсказуемым адресом. Вторая - к тому, что по непредсказуемому адресу будут записаны 8 байт, являющиеся двоичным представлением числа 135.7. Если эти байты попадут на область данных программы, то программа скорее всего выдаст неправильный результат. Если они попадут на область кода программы или на системную область MS DOS, то в лучшем случае программа аварийно завершится, а в худшем компьютер полностью зависнет.

Если делается попытка присвоить какое-либо значение по адресу указателя, значение которого равно нулю, то многие компиляторы выдают сообщение Null pointer assingment

К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться. Для компилятора фирмы Borland легко можно отследить момент некорректного обращения к памяти. Для этого нужно в окно просмотра значений выражений поместить выражение (char *) 4, затем пошагово выполнять программу до тех пор, пока строка-подпись фирмы Borland в окне просмотра не изменится. Если программа имеет большой размер, то более целесообразно выполнять ее от одной точки останова до другой.

Следует также опасаться случая, когда указатель содержит адрес объекта программы, завершившего свое существование. Например, результат работы следующей программы неверен и непредсказуем:

  1.     

  2. #include <stdio.h>  

  3. #include <math.h>   

  4. double * Cube(double x)  

  5. {  

  6.   double cube_val;  

  7.   cube_val = x*x*x;  

  8.   return &cube_val;  

  9. }   

  10. void main(void)  

  11. {  

  12.   double *py;  

  13.   py = Cube(5);  

  14.   printf("y1 = %lf\n", *py);  

  15.   sin(0.7);  

  16.   printf("y1 = %lf\n", *py);  

  17. }  

Это происходит потому, что функция Cube() возвращает указатель на локальную переменную cube_val, которая существует только в пределах функции Cube(). После возврата из функции память, ранее распределенная под переменную cube_val, освобождается и может быть использована компилятором для других целей несмотря на то, что значение возвращенного указателя по-прежнему содержит ее адрес.