Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Програм-е на ЯВУ / Программирование на языках выского уровня, алгоритмические языки.doc
Скачиваний:
63
Добавлен:
11.04.2014
Размер:
1.74 Mб
Скачать

УказатеЛи

Понятие указателя

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

Ниже приведен фрагмент памяти, состоящий из 6 ячеек (содержимое памяти и адреса памяти представлены в шестнадцатеричном формате): 4 ячейки занимает переменная A, объявленная как:

unsigned int A= 333;

Переменные

A

Содержимое памяти

...

0x4D

0x01

0x00

0x00

...

Адреса памяти

0x0012FF5B

0x0012FF5C

0x0012FF5D

0x0012FF5E

0x0012FF5F

0x0012FF60

Адресом переменной A является адрес ее первой ячейки, т.е. 0x0012FF5C (обратите внимание, что в языке Си значения хранятся в перевернутом виде: сначала – младший байт, затем – старшие байты).

Для обозначения типа данных указатель используется следующая конструкция:

<базовый тип данных> *

где базовый тип данных означает void, int, float и т.д. Другими словами, указатель не является самостоятельным типом, он всегда связан с каким-либо базовым типом данных. Базовый тип определяет тип того значения, на которое «ссылается» указатель. Рассмотрим примеры объявления переменных, способных хранить адреса памяти:

int *pInt; // указатель на значение целого типа

char *pChar; // указатель на значение символьного типа

void *pVoid; // указатель неопределенного типа (может указывать на

// значение любого типа)

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

Часто сами переменные типа указатель называют указателями. Над указателями-переменными возможны следующие операции:

– операции инициализации, присваивания (=), взятия адреса (&);

– операция разыменовывания (разадресации) указателя или взятия значения переменной по указателю (*);

– арифметические операции (+, ++, -, --);

– операции сравнения (==, !=, >, <, >=, <=);

– операция явного приведения типов.

Инициализация указателя, операция присваивания, операция взятия адреса

Инициализация указателя (присваивание указателю значения) может осуществляться константой:

int *pInt= (int *)0x00120000; // инициализация константой

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

Другой способ инициализации указателя – инициализация адресом переменной:

int A; // переменная располагается по адресу 0x0012FF5C

int *pInt= &A; // инициализация адресом переменной A: pInt= 0x0012FF5C

Как результат получается следующая карта памяти:

Переменные

A

Содержимое памяти

...

0x4D

0x01

0x00

0x00

...

Адреса памяти

0x0012FF5B

0x0012FF5C

0x0012FF5D

0x0012FF5E

0x0012FF5F

0x0012FF60

Переменные

pInt

Содержимое памяти

...

0x5C

0xFF

0x12

0x00

...

Адреса памяти

0x00707070

0x00707071

0x00707072

0x00707073

0x00707074

0x00707075

В этом случае используется специальная операция взятия адреса &, которая возвращает адрес указанной переменной. Данная операция унарная, не путайте ее с похожей по написанию бинарной операцией побитовое И.

Операция инициализации указателя адресом переменной логически представляется следующим образом:

pInt

333

A

Разыменовывание указателя

Операция разыменовывания указателя * позволяет обратиться к значению, хранящемуся по заданному адресу. Ниже приведен пример обращения к переменной A через указатель pInt (который хранит адрес этой переменной).

int A;

int * pInt= &A;

int A= 5; // переменной A присваивается значение 5

*pInt= 5; // в ячейку памяти с адресом pInt записывается значение 5

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

printf(“%d”, A); // распечатываем значение переменной A

printf(“%d”, *pInt); // распечатываем значение, хранящееся по адресу pInt

Замечание. В языке Си символ * имеет многозначный смысл. В зависимости от контекста этот символ воспринимается как операция умножения, операция разыменовывания или объявление переменной типа указатель.

Сравнение указателей

К указателям применимы все шесть операций сравнения: ==, !=, >, <, >=, <=. Сравнение pInt1 < pInt2 означает, что адрес, находящийся в pInt1, меньше адреса, находящегося в pInt2. Если pInt1 и pInt2 указывают на элементы одного массива, то индекс элемента, на который указывает pInt1, меньше индекса того элемента, на который указывает pInt2.

Приведение типов указателей

Арифметические операции (+, ++, -, --) и операция разыменовывания (*) не применимы к указателю типа void, т.к. неизвестно на значения какого типа он указывает. В указателе типа void можно лишь хранить адрес памяти. Для того чтобы воспользоваться этим указателем его необходимо привести к одному из типизированных указателей, используя явное приведение типа. Например, так:

int Mass[5];

void *pVoid;

pVoid= &Mass[0];

printf(“%d”, *pVoid); // синтаксическая ошибка!!!

printf(“%d”, *(int *)pVoid); // печать первого элемента массива

Базовый тип данных играет важную роль

short int Mass[2]= {1, 2}; // исходный массив

short int *pSInt= &Mass[0]; // рассматриваем Mass как массив коротких целых чисел

char *pChar= (char *)&Mass[0]; // рассматриваем Mass как массив символов

long int *pLInt= (long int *)&Mass[0]; // рассматриваем Mass как массив длинных целых чисел

int Count; // кол-во элементов массива

int i;

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

printf("\n");

Count= sizeof(Mass)/sizeof(short int);

for(i= 0; i < Count; i++)

{ printf("%d ", *(pSInt + i)); } // распечатывается "1 2"

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

printf("\n");

Count= sizeof(Mass)/sizeof(char);

for(i= 0; i < Count; i++)

{ printf("%d ", *(pChar + i)); } // распечатывается "1 0 2 0"

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

printf("\n");

Count= sizeof(Mass)/sizeof(long int);

for(i= 0; i < Count; i++)

{ printf("%d ", *(pLInt + i)); } // распечатывается "131073"

Окончание занятия №11 (лекция)

Арифметика указателя

Арифметические операции (+, ++, -, --) над указателем используются для доступа к данным, которые в памяти размещаются последовательно. Они позволяют, зная адрес некоторой ячейки памяти, вычислить адреса смежных ячеек.

Рассмотрим массив символов char String[4], заканчивающийся нуль-символом. Его элементы располагаются в памяти последовательно друг за другом (см. карту памяти).

Переменные

String

Содержимое памяти

...

0x01

0x02

0x03

0x00

...

Адреса памяти

0x00121000

0x00121001

0x00121002

0x00121003

0x00121004

0x00121005

Получив адрес первого элемента массива

char *pChar= &String[0]; // указатель на первый элемент массива

можно обратиться ко всем его элементам, используя арифметику указателя и операцию разадресации указателя:

// Посимвольная печать строки

i= 0;

while(*(pChar + i) != ‘\0’)

{ printf(“%c”, *(pChar + i)); }

Выражение *(pChar + i) означает, что сначала вычисляется адрес i-го элемента массива: (pChar + i), а затем происходит обращение к ячейке памяти с полученным адресом: *(<адрес ячейки>). Ниже приводится таблица с результатами выражения (pChar + i).

pChar

i

(pChar + i)

0x00121001

0

0x00121001

0x00121001

1

0x00121002

0x00121001

2

0x00121003

0x00121001

3

0x00121004

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

short int Mass[2];

short int *pInt= &Mass[0]; // указатель на первый элемент массива

// Печать массива

for(i= 0; i < 2; i++)

{ printf(“%d”, *(pInt + i)); }

Переменные

Mass

Содержимое памяти

...

0x01

0x02

0x03

0x00

...

Адреса памяти

0x00121000

0x00121001

0x00121002

0x00121003

0x00121004

0x00121005

pInt

i

(pInt + i)

0x00121001

0

0x00121001

0x00121001

1

0x00121003

Полученные результаты объясняются тем, что адреса ячеек памяти определяются с учетом базового типа данных, т.е. в общем случае выражение (<адрес ячейки> + i) вычисляется по следующей формуле:

(<адрес ячейки> + i) = <адрес ячейки> +

i*<кол-во байт памяти базового типа данных>

Связь массивов и указателей

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

Если объявлен массив

int Mass[5]= {1, 2, 3, 4, 5};

то связь массива и его имени может быть отражена следующим образом:

0

1

2

3

4

Mass

1

2

3

4

5

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

int Mass[5]= {1, 2, 3, 4, 5};

int *pMass= Mass; // записываем в pMass адрес первого элемента массива

// Mass имеет тип const int *

// Две идентичные по смыслу записи

printf(“%d”, *(pMass+1)); // печатаем второй элемент массива

printf(“%d”, *(Mass+1)); // печатаем второй элемент массива

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

int Mass[5]= {1, 2, 3, 4, 5};

int *pMass= Mass; // записываем в pMass адрес первого элемента массива

printf(“%d”, *(++pMass)); // вычисляем адрес второго элемента массива и

// печатаем его

printf(“%d”, *(++Mass)); // синтаксическая ошибка!!!

Тесная связь массивов и указателей позволяет заменить выражение *(<адрес ячейки> + i) на операцию []. Рассмотрим пример такой замены:

int Mass[5]= {1, 2, 3, 4, 5};

int *pMass= Mass; // записываем в pMass адрес первого элемента массива

printf(“%d”, pMass[1]); // печатаем второй элемент массива

Cвязь массивов с указателями позволяет работать с многомерными массивами как с одномерными. Как известно, многомерный массив располагается в памяти линейно:

Mass

0

1

2

0

1

1

1

Логическое представление

1

2

2

2

Mass

0

1

2

3

4

5

1

1

1

2

2

2

Физическое представление

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

#define M 2

#define N 3

int Mass[M][N]= {{1, 1, 1}, {2, 2, 2}};

int *Element;

// Печать всех элементов массива

Element= (int *)Mass; // Mass имеет тип const int * *

for(i= 0; i <= M*N-1; i++)

{ printf(“%d”, Element[i]); }

Замечание. В рассмотренном выше примере переменная Mass (с точки зрения механизма указателей) имеет тип const int **. Это объясняется следующим логическим представлением двумерного массива с точки зрения механизма указателей.

Mass

0

1

2

0

1

1

1

1

2

2

2

массив

указателей

Окончание занятия №12 (практика)

Пример

Массивы указателей

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

char *Errors[3]= {“Cannot open file”,

“Cannot close file”,

“System error”};

При таком объявлении строковые константы будут занесены в раздел констант памяти, а массив указателей Errors будет состоять из трех элементов и содержать адреса этих строковых констант.

Errors

0

C

a

n

n

o

t

o

p

e

n

f

i

l

e

\0

1

C

a

n

n

o

t

o

p

e

n

f

i

l

e

\0

2

S

y

s

t

e

m

e

r

r

o

r

\0

экономия

Как видно из рисунка при таком подходе экономится определенное количество памяти по сравнению с представлением набора строк двухмерным массивом символов.

Области применения указателей

В языке Си указатели используются для:

– передачи параметров функции по адресу;

– организации динамических структур данных;

– организации ссылок в сложных структурах данных.

Более подробно применение указателей будет рассмотрено в соответствующих разделах.

Окончание занятия №13 (лекция)