Указатели в языках C и C++
Выбери формат для чтения
Загружаем конспект в формате doc
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Лекция 4
Указатели в языках C и C++
Указатель – это переменная, содержащая адрес другой переменной. Программистами они используются достаточно широко, так как, во-первых, они порой дают единственную возможность выразить нужное действие, а во-вторых, ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.
Указатели и адреса
Предположим, что х – переменная, например, типа int, а рх – указатель. Унарная операция & (знак называется «амперсанд») выдаёт адрес объекта, так что оператор
рх = &х;
присваивает адрес х переменной рх; говорят, что «рх указывает на х». Операция & применима только к переменным и элементам массива; использование конструкций типа &(x-1) и &3 недопустимо.
Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если y тоже имеет тип int, то строка
y = *рх;
присваивает содержимое того, на что указывает рх. Так, последовательность
рх = &х;
y = *рх;
присваивает y то же самое значение, что и оператор
y = x;
Переменные, участвующие в этом, можно объявить таким образом:
int x, y;
int *px;
Описание указателя (инструкция int *px) говорит, что комбинация *px имеет тип int. Это означает, что если px появляется в контексте *px, то это эквивалентно переменной типа int.
Другое объявление,
double atof(), *dp;
говорит, что atof() и *dp принимают значения типа double.
Также из описания следует, что переменная-указатель может указывать только на определённый тип объектов.
Указатели могут входить в выражения. Например, если px указывает на целое x, то *px может появляться в любом контексте, где может встретиться x. Так, оператор
y = *px + 1;
присваивает y значение, на 1 большее значения x,
printf(“%d\n”, *px);
печатает текущее значение x,
D = sqrt((double) *px);
получает в D квадратный корень из x, причем до передачи функции sqrt значение x преобразуется к типу double.
Ссылки на указатели могут появляться и в левой части присваиваний.
Если px указывает на x, то
*px = 0;
означает, что x равно нулю, а
*px += 1;
увеличивает его на единицу, как и выражение
(*px)++;
Круглые скобки в последнем примере необходимы; если их опустить, то
поскольку унарные операции, подобные * и ++, выполняются справа налево, это выражение увеличит px, а не ту переменную, на которую он указывает.
Поскольку указатели являются переменными, то с ними можно обращаться таким же образом, как и с остальными переменными. Если py – другой указатель на переменную типа int, то присваивание
py = px;
копирует содержимое px в py, в результате чего py указывает на то же, что и px.
Указатели и аргументы функций
В Си передача аргументов функциям осуществляется “по значению”, поэтому вызванная функция не имеет непосредственной возможности изменить переменную из вызывающей программы. Что же делать, если действительно надо изменить аргумент? Например, программа сортировки захотела бы поменять два нарушающих порядок элемента с помощью функции с именем swap. Для этого недостаточно написать
swap(a, b);
определив функцию swap при этом следующим образом:
void swap(int x, int y) /* WRONG */
{
int temp;
temp = x;
x = y;
y = temp;
………………………
return;
}
Из-за вызова по значению swap не может воздействовать на аргументы a и b в вызывающей функции. Выход из положения есть – вызывающая программа передает указатели подлежащих изменению значений:
swap(&a, &b);
так как операция & выдает адрес переменной, то &a фактически является указателем на a.
В самой функции swap аргументы описываются как указатели и доступ к фактическим аргументам функции осуществляется через них:
void swap (int *px, int *py)
{
int temp;
temp = *px;
*px = *py;
*py = temp;
………………………
return;
}
Графически работа с указателями в данном случае выглядит следующим образом:
Рис. 1. Доступ к аргументам функции с помощью указателей.
Приведём другой пример работы с аргументами-указателями. Пусть функция getint осуществляет ввод в свободном формате одного целого числа и его перевод из текстового представления в значение типа int.
Функция getint должна возвращать значение полученного числа или сигнализировать значением EOF о конце файла, если входной поток исчерпан.
В предлагаемом ниже варианте функция getint возвращает EOF по концу файла; нуль, если следующие вводимые символы не представляют собою числа; и положительное значение, если введенные символы представляют собой число.
int getint(int *pn)
{
int c, sign;
while (isspace(c = getch()))
if (!isdigit(c) && с != EOF && c ! = '+' && с != '-' )
{
ungetch(c);
return 0;
}
sign = (с == '-' ) ? -1 : 1;
if (с == '+' || с == '-')
с = getch();
for (*pn = 0; isdigit(c); c = getch())
{
*pn = 10 * *pn;
(*pn)++;
}
*pn *= sign;
if (c != EOF)
ungetch(c);
return c;
}
Указатели и массивы. Массивы указателей
Любой доступ к элементу массива, осуществляемый операцией индексирования, может быть выполнен с помощью указателя. Вариант с указателями в общем случае работает быстрее, но разобраться в нем довольно трудно.
Объявление
int a[10];
определяет массив а размера 10, т. е. блок из 10 последовательных объектов с именами а[0], а[1],..., а[9].
Рис. 2. Целочисленный массив a[10]
Запись а[i] отсылает нас к i-му элементу массива. Если pa – указатель на int, т. е. объявлен как
int *pa;
то в результате присваивания
ра = &а[0];
pa будет указывать на нулевой элемент а, т. е., pa будет содержать адрес элемента а[0].
Рис. 3. Переменная pa указывает на элемент массива a с нулевым индексом.
Теперь присваивание
х = *ра;
будет копировать содержимое a[0] в x.
Если ра указывает на некоторый элемент массива, то ра+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после ра, a pa-i — на i-й элемент перед ра.
Таким образом, если ра указывает на а[0], то
*(pa+1)
представляет собой содержимое а[1], pa+i — адрес a[i], а *(pa+i) — содержимое a[i].
Рис. 4. Обращение к элементам массива через указатель
Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив является адресом нулевого элемента массива. Поэтому присваивание
ра = &а[0];
можно также записать в следующем виде:
ра = а;
Кроме того, а[i] можно записать как *(а+i).
Если имя массива передается функции, то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Приведём в качестве примера альтернативную версию функции strlen, вычисляющей длину строки:
int strlen(char *s)
{
int n;
for (n = 0; *s != '\0' ; s++)
n++;
return n;
}
Так как s является указателем, к переменной применима операция ++. В результате выполнения операции увеличивается на 1 некоторая копия
указателя, находящаяся в пользовании функции strlen. Следовательно, такие вызовы, как
strlen(array); /* char array[100]; */
strlen(ptr); /* char *ptr; */
правомерны.
Формальные параметры char s[] и char *s в определении функции эквивалентны. Последний вариант более предпочтителен, так как явно сообщает, что s – указатель.
Как и любые другие переменные, указатели можно группировать в массивы. Это удобно, например, при сортировке текстовых строк в алфавитном порядке. Если объявить массив указателей на элементы типа char, например, char *lineptr[], то каждый отдельный указатель будет содержать адрес первого символа определённой строки:
Рис. 5. Массив указателей, содержащих адреса первых символов строк