
- •Программирование на языках высокого уровня. Алгоритмические языки
- •Содержание
- •Разработка (проектирование) одномодульных программ
- •Представление информации в языке Си
- •Массивы
- •УказатеЛи
- •Выражения и операции
- •Управляющие операторы
- •Функции
- •Типы данных, определяемые пользователем
- •Приемы программирования
- •Приложение 2. Арифметические основы компьютеров
- •Литература
УказатеЛи
Понятие указателя
В отличие от рассмотренных ранее типов данных, указатель является служебным типом данных, т.к. используется не для хранения числовых или символьных значений, а для хранения адресов памяти. В первом приближении адрес памяти может рассматриваться как номер ячейки памяти.
Ниже приведен фрагмент памяти, состоящий из 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 (лекция) |