
- •Операции с указателями
- •Указатели и константы.
- •Связь между указателями и массивами
- •Указатели на указатели
- •Бестиповый (нетипизированный) указатель
- •Нулевой указатель
- •Преобразование типа указателя
- •Практическая часть
- •Контрольные вопросы
- •Задание.
- •Выполнить свой вариант лабораторной работы №8 с использованием указателей.
Связь между указателями и массивами
Одной из наиболее распространенных конструкций с использованием указателей являются массивы. Результатом использования указателей для массивов является меньшее количество используемой памяти и высокая производительность.
В языке С массивы – это упорядоченные данные (элементы) одного типа. Компилятор языка С рассматривает имя массива как адрес его первого элемента (в языке С нумерация элементов массива начинается с нуля). Например, если имя массива Arr с десятью элементами, то i -й элемент (0 ≤ i < 10) компилятор преобразует его по правилам работы с указателями с операцией разыменования: *(Arr + i). Здесь Arr как бы указатель, а i – целочисленная переменная. Сумма (Arr + i) указывает на i -й элемент массива, а операция разыменования (оператор раскрытия ссылки * ) дает значение самого элемента, т.е. Arr[i] идентично *(Arr+i).
Имя массива без индекса образует указатель на начало этого массива.
Имеется одно различие между именем массива и указателем, которое необходимо иметь в виду. Указатель является переменной, так что операции ptr1=Arr и ptr1++ имеют смысл. Но имя массива является константой, а не переменной: конструкции типа Arr=ptr1 или Arr++, или Arr=&x будут незаконными.
Т.е. имя массива всегда указывает на одно и то же место в памяти – на нулевой элемент.
Пусть, например, массив Arr содержит 10 целочисленных переменных:
int Arr[10];
Тогда можно объявить указатель ptr, который будет указывать на элементы массива Arr:
int *ptr;
Тип указателя (в примере это int ) должен соответствовать типу объявленного массива.
Для того, чтобы указатель ptr ссылался на первый элемент (с нулевым индексом) массива Arr, можно использовать утверждение:
ptr = Arr;
В то же время можно использовать прямую адресацию:
ptr = &Arr[0];
Обе формы записи эквивалентны.
Аналогичные утверждения будут справедливы для других типов массивов: char, float, double и пр.
Если указатель ptr указывал на первый элемент (с нулевым индексом) массива Arr, то для обращения к следующему элементу массива допустимы следующие формы утверждений:
ptr = &Arr[1];
ptr += 1;
Соответственно, выражение *(ptr+1) будет ссылаться на значение, содержащееся в элементе Arr[1].
Утверждение
ptr += n;
заставит указатель *ptr ссылаться на элемент массива, находящийся на расстоянии n от того, на который ранее ссылался указатель, независимо от типа элементов, содержащихся в массиве. Разумеется, значение n должно быть в допустимых пределах для данного объема массива.
При работе с указателями и массивами особенно удобны операторы инкремента "++" и декремента "––" . Использование оператора инкремента с указателем аналогично операции суммирования с единицей, а операция декремента имеет тот же эффект, что и вычитание единицы из указателя.
В языке программирования С вполне корректной операцией является сравнение указателей. К указателям применяются операции сравнения ">", ">=", "!=", "==", "<=", "<". Сравнивать указатели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса. Константа NULL – это особое значение переменной-указателя, присваиваемое ей в том случае, когда она не должна иметь никакого значения. Его можно присвоить переменной-указателю любого типа. Оно представляет собой целое число нуль.
Особое значение имеет сравнение двух указателей, которые связаны с одним и тем же массивом данных.
Рассмотрим инициализацию указателей типа char:
char *ptr = "hello, world";
Переменная *ptr является указателем, а не массивом. Поэтому строковая константа "hello, world" не может храниться в указателе *ptr. Тогда возникает вопрос, где хранится строковая константа. Для этого следует знать, что происходит, когда компилятор встречает строковую константу. Компилятор создает так называемую таблицу строк. В ней он сохраняет строковые константы, которые встречаются ему по ходу чтения текста программы. Следовательно, когда встречается объявление с инициализацией, то компилятор сохраняет "hello, world" в таблице строк, а указатель *ptr записывает ее адрес.
Поскольку указатели сами по себе являются переменными, их можно хранить в массивах, как и переменные других типов. Получается массив указателей.
Массив указателей фиксированных размеров вводится одним из следующих определений:
тип *имя_массива [размер];
тип *имя_массива [ ] = инициализатор;
тип *имя_массива [размер] = инициализатор;
В данной инструкции тип может быть как одним из базовых типов, так и производным типом;
имя_массива – идентификатор, определяемый пользователем по правилам языка С ;
размер – константное выражение, вычисляемое в процессе трансляции программы;
инициализатор – список в фигурных скобках значений элементов заданного типа (т.е. тип ).
Рассмотрим примеры:
int data[7]; // обычный массив
int *pd[7]; // массив указателей
int *pi[ ] = { &data[0], &data[4], &data[2] };
В приведенных примерах каждый элемент массивов pd и pi является указателем на объекты типа int.
Значением каждого элемента pd[j] и pi[k] может быть адрес объекта типа int. Все 6 элементов массива pd указателей не инициализированы.В массиве pi три элемента, и они инициализированы адресами конкретных элементов массива data.
В случае обработки строк текста они, как правило, имеют различную длину, и их нельзя сравнить или переместить одной элементарной операцией в отличие от целых чисел. В этом случае эффективным средством является массив указателей. Например, если сортируемые строки располагаются в одном длинном символьном массиве вплотную — начало одной к концу другой, то к каждой строке можно обращаться по указателю на ее первый символ. Сами же указатели можно поместить в массив, т.е. создать массив указателей. Две строки можно сравнить, рассмотрев указатели на них.
Массивы указателей часто используются при работе со строками. Можно привести пример массива строк о студенте, задаваемый с помощью массива указателей.
char *ptr[ ] = {
"Surname", //Фамилия
"Name", // Имя
"group", // группа
"ACOUY" // специальность
};
С помощью массива указателей можно инициализировать строки различной длины. Каждый из указателей массива указателей указывает на одномерный массив символов (строку) независимо от других указателей.
В языке программирования С предусматриваются ситуации, когда указатели указывают на указатели. Такие ситуации называются многоуровневой адресацией. Пример объявления указателя на указатель:
int **ptr2;
В приведенном объявлении **ptr2 – это указатель на указатель на число типа int. При этом наличие двух звездочек свидетельствует о том, что имеется двухуровневая адресация. Для получения значения конкретного числа следует выполнить следующие действия:
int x = 88, *ptr, **ptr2;
ptr = &x;
ptr2 = &ptr;
printf("%d", **ptr2);
В результате в выходной поток (на дисплей пользователя) будет выведено число 88. В приведенном фрагменте переменная *ptr объявлена как указатель на целое число, а **ptr2 – как указатель на указатель на целое. Значение, выводимое в выходной поток (число 88), осуществляется операцией разыменования указателя **ptr2.
Для многомерных массивов указатели указывают на адреса элементов массива построчно. Рассмотрим пример двухмерного целочисленного массива М размера 3 5, т.е. состоящего из 3 строк и 5 столбцов, и определим указатель:
int M[3][5]= {{1,2,3,4,5},{–6,–7,–8,–9,–10},{11,12,13,14,15}};
int *ptr;
Элементы массива (по индексам) располагаются в ячейках памяти по строкам в следующем порядке:
M[0][0], M[0][1], M[0][2], M[0][3], M[0][4], M[1][0], M[1][1], M[1][2], M[1][3], M[1][4], M[2][0], M[2][1], M[2][2], M[2][3], M[2][4].
Сначала запоминается первая строка, затем вторая, затем третья. В данном случае двухмерный массив – это массив трех одномерных массивов, состоящих из 5 элементов.
Указатель указывает на адреса элементов в порядке расположения их в памяти. Поэтому тождественны равенства:
ptr == &M[0][0]; //?1-я строка, 1-й столбец
ptr + 1 == &M[0][1]; // 1-я строка, 2-й столбец
ptr + 2 == &M[0][2]; // 1-я строка, 3-й столбец
ptr + 3 == &M[0][3]; // 1-я строка, 4-й столбец
ptr + 4 == &M[0][4]; // 1-я строка, 5-й столбец
ptr + 5 == &M[1][0]; // 2-я строка, 1-й столбец
ptr + 6 == &M[1][1]; // 2-я строка, 2-й столбец
ptr + 7 == &M[1][2]; // 2-я строка, 3-й столбец
ptr + 8 == &M[1][3]; // 2-я строка, 4-й столбец
ptr + 9 == &M[1][4]; // 2-я строка, 5-й столбец
ptr + 10 == &M[2][0]; // 3-я строка, 1-й столбец
ptr + 11 == &M[2][1]; // 3-я строка, 2-й столбец
ptr + 12 == &M[2][2]; // 3-я строка, 3-й столбец
ptr + 13 == &M[2][3]; // 3-я строка, 4-й столбец
ptr + 14 == &M[2][4]; // 3-я строка, 5-й столбец
Практически следует произвести инициализацию указателя, например, взяв адрес первого элемента матрицы, а затем – обращение к элементам матрицы, можно производить через указатель:
ptr = &M[0][0];
*(ptr + i*n + j);
где i – номер строки заданной матрицы, j – номер столбца, n – число столбцов в матрице.