Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
лекция 17-18.docx
Скачиваний:
13
Добавлен:
23.03.2015
Размер:
97.73 Кб
Скачать

Указатели

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

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

Что такое указатели

Указатель — это переменная, значением которой является адрес некоторого объекта (обычно другой переменной) в памяти компьютера. Например, если одна переменная содержит адрес другой переменной, то говорят, что первая переменнаяуказывает (ссылается) на вторую. Это иллюстрируется с помощью рис. 5.2.

Указательные переменные

Как известно, переменную, являющуюся указателем, нужно соответствующим образом объявить. Объявление указателя состоит из имени базового типа, символа * и имени переменной. Общая форма объявления указателя следующая:

тип *имя;

Здесь тип — это базовый тип указателя, им может быть любой правильный тип. Имя определяет имя переменной-указателя.

Базовый тип указателя определяет тип объекта, на который указатель будет ссылаться. Фактически указатель любого типа может ссылаться на любое место в памяти. Однако выполняемые с указателем операции существенно зависят от его типа. Например, если объявлен указатель типа int *, компилятор предполагает, что любой адрес, на который он ссылается, содержит переменную типа int, хоть это может быть и не так. Следовательно, объявляя указатель, необходимо убедиться, что его тип совместим с типом объекта, на который он будет ссылаться.

Адресная арифметика языка Си

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

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

int *a, *b, c, d;

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

double *bc;

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

int* a, b;

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

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

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

void *vd;

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

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

int *a, *b;

double *d;

void *v;

...

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

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

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

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

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

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

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

b = (int *) v;

d = (double *) a;

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

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

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

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

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

int *p, a, b;

double d;

void *pd;

p = &a;

*p = 12;

p = &b;

*p = 20;

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

pd = &d;

*( (double *) pd ) = a;

/* Здесь 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

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

double *a, b;

b = *a;

*a = 135.7;

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

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

Null pointer assingment

К сожалению, это сообщение выдается уже после того, как программа завершилась, если она смогла завершиться.

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

#include <stdio.h>

#include <math.h>

double * Cube(double x)

{

double cube_val;

cube_val = x*x*x;

return &cube_val;

}

void main(void)

{

double *py;

py = Cube(5);

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

sin(0.7);

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

}

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

Операция получения адреса (&) и раскрытия ссылки (*)

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

Первый из них — оператор &, это унарный оператор, возвращающий адрес операнда в памяти. Например, оператор

m = &count;

записывает в переменную m адрес переменной count. Этот адрес представляет собой адрес ячейки памяти компьютера, в которой размещена переменная. Адрес и значение переменной — совершенно разные понятия. Выражение "&переменная" означает "адрес переменной". Следовательно, инструкция m = &scount; означает: "Переменной m присвоить адрес, по которому расположена переменная count;".

Допустим, переменная count расположена в памяти в ячейке с адресом 2000, а ее значение равно 100. Тогда в предыдущем примере переменной m будет присвоено значение 2000.

Второй рассматриваемый оператор * является двойственным (дополняющим) по отношению к &. Оператор * является унарным оператором, он возвращает значение объекта, расположенного по указанному адресу. Операндом для * служит адрес объекта (переменной). Например, если переменная m содержит адрес переменной count, то оператор

q = *m;

записывает значение переменной count в переменную q. В нашем примере переменная q получит значение 100, потому что по адресу 2000 записано число 100, причем этот адрес записан в переменной m. Выражение "* адрес" означает "по адресу". Наш фрагмент программы можно прочесть как "q получает значение, расположенное по адресу m".

К сожалению, символ операции раскрытия ссылки совпадает с символом операции умножения, а символ операции получения адреса — с символом операции поразрядного И. Необходимо помнить, что эти операторы не имеют никакого отношения друг к другу. Операторы * и & имеют более высокий приоритет, чем любая арифметическая операция, кроме унарного минуса, имеющего такой же приоритет.

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

char *ch;

Необходимо понимать, что ch — это не переменная типа char, а указатель на переменную данного типа, это совершенно разные вещи. Тип данных, на который указывает указатель (в данном случае это char), называется базовым типом указателя. Сам указатель является переменной, содержащей адрес объекта базового типа. Компилятор учтет размер указателя в архитектуре компьютера и выделит для него необходимое количество байтов, чтобы в указатель поместился адрес. Базовый тип указателя определяет тип объекта, хранящегося по этому адресу.

В одном операторе объявления можно одновременно объявить и указатель, и переменную, не являющуюся указателем. Например, оператор

int x, *y, count;

объявляет х и count как переменные целого типа, а у — как указатель на переменную целого типа.

В следующей программе операторы * и & используются для записи значения 10 в переменную target. Программа выведет значение 10 на экран.

#include <stdio.h>

int main(void)

{

int target, source;

int *m;

source = 10;

m = &source;

target = *m;

printf("%d", target);

return 0;

}

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]