/* |
// указателя |
.............................*/ |
Вязыке Си указатели наделены большими возможностями. Об этом свидетельствует размер перечня тех операций, которые можно выполнять с указателями:
∙Разыменование,
∙Взятие адреса,
∙Присваивание,
∙Индексирование указателей
∙Арифметические действия с указателями,
∙Сравнение указателей.
Как отмечалось выше, разыменование – операция позволяющая получить доступ к объекту, на который установлен указатель. Однако эта операция допустима не ко всем указателям. Остановимся на тех категориях указателей,
ккоторым нельзя применять разыменование:
∙Недействительный указатель.
∙Нетипизированный указатель.
∙Нулевой указатель
Приведем примеры недопустимых разыменований.
/* Примеры недопустимых разыменований указателей */
int* p = NULL; |
/* Недопустимо разыменовывать нулевой указатель*/ |
|
*p = 10; |
||
int n = 5; |
|
|
void* |
p3 = &n; |
/* Недопустимо разыменовывать нетипизированный |
*p3 = 10; |
||
|
|
указатель p3*/ |
char* p4; |
/* Недопустимо разыменовывать |
|
*p4 = ’A’; |
||
/* |
|
неинициализированный указатель p4 */ |
. . . . . . . . . . . . . . . . . . . . . . . . . . . */ |
||
Заметим, что к категории операторов разыменования относятся еще два оператора:
∙Оператор [], который используется при работе с массивами.
∙Оператор ->, который используется при работе со структурами.
Одному указателю можно присвоить значение другого указателя. Однако некоторые присваивания, которые допускает язык Си, могут привести к
96
некорректной работе программы. Остановимся на этом вопросе подробнее. Рассмотрим ряд частных случаев. Начнем с присваивания типизированных указателей. Корректность такой операции не вызывает сомнений только в том случае, когда совпадают базовые типы обоих указателей. В противном случае такую операцию следует считать недопустимой. Приведем пример программного кода, в котором будут встречаться оба вида присваиваний.
/* |
Корректные и некорректные присваивания типизированных |
||
int n = 10; |
указателей |
*/ |
|
|
|
||
double x = 20.3; |
|
||
int *p1, p2; |
|
|
|
double* p3; |
|
|
|
p1 |
= &n; |
/* допустимое присваивание |
*/ |
p2 |
= p1; |
/* допустимое присваивание |
*/ |
p3 |
= p2; |
/* недопустимое присваивание, т. к. не |
|
совпадают базовые типы указателей |
*/ |
|
/* конец программного кода примера |
*/ |
|
Отметим, что, несмотря на |
семантическую недопустимость |
|
присваивания p3 = p1 в приведенном выше примере компилятор языка Си ограничивается выводом предупреждения (Компилятор языка С++ в этой ситуации выводит сообщение об ошибке). Работа языка Си с указателями свидетельствует о его пониженной типизации. Это обстоятельство следует рассматривать как крайне опасным последствиям. Одна из рекомендаций по преодолению этих неприятностей заключается в предварительной компиляции программного кода компилятором языка С++.
Одному указателю можно присвоить значение другого указателя. Однако некоторые присваивания, которые допускает язык Си, могут привести к некорректной работе программы. Остановимся на этом вопросе подробнее. Рассмотрим ряд частных случаев. Начнем с присваивания типизированных указателей. Корректность такой операции не вызывает сомнений только в том случае, когда совпадают базовые типы обоих указателей. В противном случае такую операцию следует считать недопустимой. Приведем пример программного кода, в котором будут встречаться оба вида присваиваний.
/* |
Корректные и некорректные присваивания типизированных |
||
int n = 10; |
указателей |
*/ |
|
|
|
||
double x = 20.3; |
|
||
int *p1, p2; |
|
|
|
double* p3; |
|
|
|
p1 |
= &n; |
/* допустимое присваивание |
*/ |
p2 |
= p1; |
/* допустимое присваивание |
*/ |
p3 |
= p2; |
/* недопустимое присваивание, т. к. не совпадают |
|
|
|
базовые типы указателей |
*/ |
/* конец программного кода примера |
*/ |
||
97
Отметим, что, несмотря на недопустимость присваивания p3 = p1 в приведенном выше примере компилятор языка Си ограничивается выводом предупреждения (Компилятор языка С++ в этой ситуации выводит сообщение об ошибке).
Пусть имеется следующий фрагмент программы
//..
int n = 5; int *p = &n; int **pp = &p;
Переменная pp является указателем на указатель или двухуровневым указателем. Эта переменная получена применением оператора взятия адреса & к указателю p. Указатели на указателиприменяются на практике. Например, в качестве параметров функций (см. прототип функции strtok).
Рассмотрим следующий фрагмент программного кода:
#define MSIZE 5 #include <stdio.h> int main(void)
{
int x[MSIZE] = {1, 3, 5, 7, 9}; int *p = &x[1];
printf(“p[1] = %d”, p[1]); // ..
}
В рассматриваемом фрагменте программы объявлен указатель p, который инициализирован адресом второго элемента массива x. Затем функции printf() к указателю p применен оператор индексные скобки []. В результате выполнения этого фрагмента программы на экране получим p[1] = 5.
Арифметические операции применяются к укзателям, которые работают с массивами. К числу допустимых арифметических операций к таким указателям относятся:
∙++ (инкремент),
∙--(декремент),
∙сложение с целым числом,
∙вычитание целого числа.
∙вычисление разности двух указателей. Рассмотрим слеющий программный код:
#define MSIZE 5 #include <stdio.h>
98
int main(void)
{
int x[MSIZE] = {1, 3, 5, 7, 9}; int *p1 = &x[1];
int *p2 = &x[3]; // ..
}
Вэтом программном коде объявлены указатели p1 и p2, инциализированныt адресами элементов массива x. Допустимы следующие выражения с этими указателями:
∙p1++ и ++p1,
∙p1-- и p1--,
∙p1 + 2 и p2 – 2
∙p2 – p1.
Врезультате вычисления всех выражений, исключая последнее выражение (p2 – p1), получается адрес. Результатом вычисления разности p2
–p1 будет целое число. Рассмотрим выражение p1++. При вычислении этого выражения следует учитывать возвращаемое значение и побочный эффект. Возращаемым значения выражения p++ является исходное значение указателя p. Побочный эффект при вычислении выражения p++ состоит в увеличении операнда на единицу. Однако адрес, хранящийся в указателе p, увеличивается таким образом, чтобы указатель был установлен на следующий элемент массива. При работе в 32 разрядной среды адрес, хранящийся в указатель, увеличиться на 4 байта. Таким образом, арифметические операции, выполняемые с указателями, оказываются масштабированными. В качесте масштаба используется объем памяти, которую занимает один элемент массива, с которым работает указатель.
Указатели можно сравнивать. Сравнение имеет смысл только для указателей на один и тот же массив. При соблюдении этого условия операции ==, !=, <, >, <=, >= будут выполняться корректно.
Как отмечалось выше, одной из основных областей применения указателей – работа с динамической памятью. Использование динамической памяти позволяет повысить эффективность использования оперативной памяти компьютера. При этом возрастают трудности программирования. В первую очередь это связано с тем, что программист должен следить за выделением и освобождением память самостоятельно.
В стандарте языка Си определены четыре функции, предназначенные для выделения и освобождения динамической памяти. К ним относятся следующие функции:
∙malloc().
∙calloc(),
∙realloc(),
99
∙free().
Первые три функции служат для выделения динамической памяти. Последняя функция предназначена для освобождения памяти. На начальном этапе знакомство с вопросами, связанными с работой с динамической памятью, достаточно ограничиться функциями malloc() и free().
#include<stdlib.h>
void* malloc(size_t size);
Эта функция предпринимает попытку динамически выделить непрерывный блок памяти, размер которого равен size_t байт. В случае успеха функция возвращает указатель на начало выделенной памяти. В случае неуспеха функция вернет указатель NULL. Следует учитывать, что после завершения работы с памятью, выделенной функцией malloc(), ее следует освободить вызовом функции free().
В приведенном ниже примере делается попытка динамически выделить память для строкового буфера размером в MAXSIZE байт. Если попытка закончится неудачей, то будет вызвана функция пользователя error().
#include<stdlib.h> #define MAXSIZE 129 int main(void)
{
char* buf;
if(NULL ==(buf = malloc(MAXSIZE)))
|
error(); |
/* |
..................*/ |
}
Объявление этой функции имеет следующий вид
#include<stdlib.h>
void* calloc(size_t nmemb, size_t size);
Делает попытку выделить память для массива из nmemb объектов, размер каждого из которых равен size байт. В случае успеха функция calloc() возвращает указатель на начало выделенной памяти. В случае неуспеха возвращается указатель NULL. Выделенная память заполняется двоичными нулями.
#include<stdlib.h> #define MAXSIZE 129 int main(void)
{
double* arr; int n;
if(NULL ==(arr = сalloc(MAXSIZE))) error();
100