<< Предыдущая Оглавление Следующая >>


4.1. Указатели

При объявлении переменных, структур, объединений и т.п. операционная система выделяет необходимый объем памяти для хранения данных программы. Например, задавая целочисленную переменную

int a = 10;

в памяти ЭВМ выделяется либо 2, либо 4 байта (в зависимости от стандарта языка С), которые расположены друг за другом, начиная с определенного адреса. Здесь под адресом следует понимать номер байта в памяти, который показывает, где начинается область хранения той или иной переменной или каких-либо произвольных данных. Условно память ЭВМ можно представить в виде последовательности байт (рис. 4.1).

Рис. 4.1. Условное представление памяти ЭВМ с расположением переменной а

На рис. 4.1 переменная а расположена в 100 и 101 ячейках и занимает соответственно два байта. Адрес этой переменной равен 100. Учитывая, что значение переменной а равно 10, то в ячейке под номером 100 будет записано число 10, а в ячейке под номером 101 – ноль. Аналогичная картина остается справедливой и при объявлении произвольных переменных и структур, только в этом случае расходуется разный объем памяти в зависимости от типа переменной.

В языке С++ имеется механизм работы с переменными через их адрес. Для этого необходимо объявить указатель соответствующего типа. Указатель объявляется также как и переменная, но перед его именем ставится символ ‘*’:

int *ptr_a;
char *ptr_ch, *ptr_var;

Для того чтобы с помощью указателя ptr_a работать с переменной a он должен указывать на адрес этой переменной. Это значит, что значение указателя ptr_a должно быть равно адресу переменной a. Здесь возникает две задачи: во-первых, необходимо определить адрес переменной, и, во-вторых, присвоить этот адрес указателю. Для определения адреса в языке С++ используется символ ‘&’ как показано ниже:

unsigned long ptr = &a;

В результате переменной ptr будет присвоен адрес переменной a. Аналогичным образом можно выполнить инициализацию указателя ptr_a:

ptr_a = &a; //инициализация указателя

По существу получается, что указатель это переменная, которая хранит адрес на заданную область памяти. Но в отличие от обычной переменной позволяет еще, и работать с данной областью, т.е. записывать в нее значения и считывать их. Допустим, что переменная a содержит число 10, а указатель ptr_a указывает на эту переменную. Тогда для того чтобы считывать и записывать значения переменной a с помощью указателя ptr_a используется следующая конструкция языка С:

int b = *ptr_a; //считывание значения переменной а
*ptr_a = 20; //запись числа 20 в переменную а

Здесь переменной b присваивается значение переменной a через указатель ptr_a, а, затем, переменной a присваивается значение 20. Таким образом, для записи и считывания значений с помощью указателя необходимо перед его именем ставить символ ‘*’ и использовать оператор присваивания.

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

Листинг 4.1. Пример использования указателей.

void interchange(int* arg1, int* arg2);
int main()
{
int arg1 = 10, arg2 = 20;
interchange(&arg1,&arg2);
return 0;
}
void interchange (int* arg1, int* arg2)
{
int temp = *arg1;
*arg1 = *arg2;
*arg2 = temp;
}

Здесь реализована функция, которая в качестве аргументов принимает два указателя на переменные arg1 и arg2. Благодаря такому подходу значения локальных переменных, объявленных в функции main() можно менять через указатели в функции interchange(). Реализовать проще или также данную задачу с помощью обычных переменных (без использования указателей) невозможно. Учитывая, что в основе работы компьютера лежит работа с адресами той или иной области памяти, считывания от туда данных и записи новых, то указатели позволяют программисту добиться более эффективного исполнения программ.

Каждый раз при работе с указателями необходимо выполнять их инициализацию, т.е. задавать адрес на выделенную область памяти. Сложность работы с указателями заключается в том, что при их объявлении они указывают на произвольную область памяти, с которой можно работать как с обычной переменной. Приведем такой пример

int* ptr;
*ptr = 10;

В результате в произвольную область памяти будет записано два байта со значениями 10 и 0. Это может привести к необратимым последствиям в работе программы и к ее ошибочному завершению. Поэтому перед использованием указателей всегда нужно быть уверенным, что они предварительно были инициализированы.

Рассмотрим возможность использования указателей при работе с массивами. Допустим, что объявлен массив целочисленного типа int размерностью в 20 элементов:

int array[20];

Элементы массивов всегда располагаются друг за другом в памяти ЭВМ, начиная с первого, индекс которого равен 0 (рис. 4.2).

Рис. 4.2. Условное расположение массива int array[20] в памяти ЭВМ

Из рис. 4.2 видно, что для получения адреса массива array достаточно знать адрес его первого элемента array[0], который можно определить как адрес переменной и присвоить его указателю:

int* ptr_ar = &array[0];

Однако в языке С++ предусмотрена более простая конструкция для определения адреса массивов, которая записывается следующим образом:

int* ptr_ar = array;

т.е. имя массива задает его адрес. Следует отметить, что величины &array[0] и array являются константами, т.е. не могут менять своего значения. Это означает, что массив (как и переменная) не меняют своего адреса, пока существуют в зоне своей видимости.

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

int a = *ptr_ar;
*ptr_ar = 20;

Для того чтобы перейти к следующему элементу массива, достаточно выполнить операцию

ptr_ar += 1;

или

ptr_ar++;

Особенностью применения данной операции является то, что адрес, т.е. значение указателя ptr_ar изменится не на единицу, а на четыре, ровно на столько, сколько занимает один элемент в памяти ЭВМ, в данном случае четыре байта. В результате указатель ptr_ar будет указывать на следующий элемент массива, с которым можно работать также как и с предыдущим.

Пример изменения адреса указателя ptr_ar показывает необходимость правильно задавать его тип, в данном случае int. Если бы тип указателя ptr_ar был char, то при использовании оператора инкремента ++, его значение увеличилось бы на единицу, а не на четыре и переход к следующему элементу массива осуществлялся бы некорректно.

Приведем пример использования указателя на массив, показывающий особенности применения указателей.

Листинг 4.2. Работа с элементами массива через указатели.

#include
#define SIZE 5
int main()
{
int array[SIZE] = {10,20,30,40,50};
int *ptr_ar = array;
for(int i = 0;i < SIZE;i++)
printf(“Значение элемента %d, адрес элемента %p\n”,*(ptr_ar+i), ptr_ar+i);
return 0;
}

В данном примере используется указатель ptr_ar, который всегда указывает на первый элемент массива array[0]. Для перехода к следующим элементам прибавляется значение i и выводится результат на экран.

В языке С++ при работе с массивами через указатель допускается более простая форма чем рассмотренная ранее. Допустим, что ptr_ar указывает на первый элемент массива array. Тогда для работы с элементами массива можно пользоваться следующей записью:

int *ptr_ar = array;
ptr_ar[0] = 10;
ptr_ar[1] = 20;

т.е. доступ к элементам массива осуществляется по его индексу.

Массивы удобно передавать функциям через указатели. Пусть имеется функция вычисления суммы элементов массива:

int sum(int* ar, int n);

и массив элементов

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

Тогда для передачи массива функции sum следует использовать такую запись:

int s = sum(array,5);

т.е. указатель ar инициализируется по имени массива array и будет указывать на его первый элемент.

Следует отметить, что все возможные изменения, выполненные с массивом внутри функции sum(), сохраняются в массиве array. Это свойство можно использовать для модификации элементов массива внутри функций. Например, рассмотренная ранее функция strcpy(char *dest, char* src), выполняет изменение массива, на который указывает указатель dest. Для того чтобы «защитить» массив от изменений следует использовать ключевое слово const либо при объявлении массива, либо в объявлении аргумента функции как показано ниже.

char* strcpy(char* dest, const char* src)
{
while(*src != ‘\0’) *dest++ = *src++;
*dest = *src;
return dest;
}

В этом объявлении указатель src не может вносить изменения в переданный массив при вызове данной функции, а может лишь передавать значения указателю dest. В приведенном примере следует обратить внимание на использование конструкции *dest++ и *src++. Дело в том, что приоритет операции ++ выше приоритета операции *, поэтому эти выражения аналогичны выражениям *(dest++) и *(src++). Таким образом, в строке

*dest++ = *src++;

сначала выполняется присваивание значений соответствующих элементов, на которые указывают dest и src, а затем происходит увеличение значений указателей для перехода к следующим элементам массивов. Благодаря такому подходу осуществляется копирование элементов одного массива в другой. Последняя строка примера *dest = *src, присваивает символ конца строки ‘\0’ массиву dest.

В общем случае можно выполнять следующие операции над указателями:

pt1 = pt2; //Присвоение значения одного указателя другому
pt1 += *pt2; //Увеличение значения первого указателя на величину *pt2
pt1 -= *pt2; //Уменьшение адреса указателя на величину *pt2
pt1-pt2; //Вычитание значений адресов первого и второго указателей
pt1++; и ++pt1; //Увеличение адреса на единицу информации
pt1--; и --pt1; //Уменьшение адреса на единицу информации

Если указатели pt1 и pt2 имеют разные типы, то операция присваивания должна осуществляться с приведением типов, например:

int* pt1;
double* pt2;
pt1 = (int *)pt2;

Язык С++ допускает инициализацию указателя строкой, т.е. будет верна следующая запись:

char* str = “Лекция”;

Здесь задается массив символов, содержащих строку «Лекция» и адрес этого массива передается указателю str. Таким образом, получается, что есть массив, но нет его имени. Есть только указатель на его адрес. Подобный подход является удобным, когда необходимо задать массив строк. В этом случае возможна такая запись:

char* text[] = {«Язык С++ имеет»,
«удобный механизм»,
«для работы с памятью.»};

При таком подходе задается массив указателей, каждый из которых указывает на начало соответствующей строки. Например, значение *text[0] будет равно символу ‘Я’, значение *text[1] – символу ‘у’ и значение *text[2] – символу ‘д’. Особенность такого подхода состоит в том, что здесь задаются три отдельных одномерных массива символов никак не связанных друг с другом. Каждый массив – это отдельная строка. В результате не расходуется лишний объем памяти характерный для двумерного массива символов, который условно можно представить в виде таблицы (рис. 4.3).

char text_array[][] = {«Язык С++ имеет»,
«удобный механизм»,
«для работы с памятью.»};

Рис. 4.3. Условное представление текста в двумерном массиве text_array

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

char* name;
scanf(“%s”,name);

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

char buff[100];
char* name = buff;
scanf(“%s”,name);

Язык С++ также позволяет инициализировать указатели на структуры. Допусти, что имеется структура

struct tag_person {
char name[100];
int old;
} person;

и инициализируется следующий указатель:

struct tag_person* pt_man = &person;

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

(*pt_man).name;
pt_man->name;

Последний вариант показывает особенность использования указателя на структуры, в котором для доступа к элементу используется операция ->. Данная операция наиболее распространена по сравнению с первым вариантом и является предпочтительной.


<< Предыдущая Оглавление Следующая >>