Если вы еще не имеете четкого представления о работе указателей, то самое время подробнее познакомиться с ними. Суть концепции указателей состоит в том, что вы работаете с адресом ячейки памяти, получая лишь косвенный доступ к ее содержимому, благодаря чему появляется возможность динамически создавать и уничтожать переменные. Хотя с помощью указателей можно создавать чрезвычайно эффективные алгоритмы, сложность программы, в которой используются указатели, значительно возрастает.
В языках C/C++ вопросы, связанные с указателями, массивами и строками, тесно связаны друг с другом. Поэтому данную главу можно рассматривать как непосредственное продолжение предыдущей главы. Для начинающего программиста глава может показаться чересчур сложной, но лишь в полной мере изучив тему указателей, можно начать создавать действительно профессиональные приложения.
Указатели как особый тип переменных
Начинающие программисты, как правило, работают только с обычными переменными. Для таких переменных память выделяется автоматически при запуске программы или функции, в которой они объявлены, и удаляется также автоматически при завершении программы или функции. Доступ к ним организуется достаточно просто — по имени:
imemorycell_contents+= 10;
Другой способ получения доступа к значению переменной заключается в использовании другой переменной, которая содержит ее адрес. Предположим, имеется переменная типа intс именем imemorycell_contents и переменная pimemory_cell_address, являющаяся указателем на нее. Как вы уже знаете, в C/C++ есть оператор взятия адреса &, который возвращает адрес своего операнда. Поэтому вам будет нетрудно разобраться в синтаксисе присвоения одной переменной адреса другой переменной:
pimemorycell_address = &imemorycell_contents;
Переменные, которые хранят адреса других переменных, называются указателями.
На рис. 9.1 схематично показана взаимосвязь между переменной и указателем
на нее. Переменная imemorycell_contents представлена в памяти компьютера ячейкой
с адресом 7751. После выполнения показанной выше строки программы адрес этой
переменной будет присвоен указателю pimemorycell_address.
Рис. 9.1. Взаимосвязь между переменной и её указателями
Обращение к переменной, чей адрес хранится в другой переменной, осуществляется путем помещения перед указателем оператора *: *pimemorycell_address. Такая запись означает, что будет произведен косвенный доступ к ячейке памяти через имя указателя, содержащего адрес ячейки. Например, если выполнить две показанные ниже строки, то переменная imemorycell_contentsпримет значение 20:
pimemorycell_address = &imemorycell_contents;
*pimemorycell_address = 20; :
С учетом того, что указатель pimemorycell_address хранит адрес переменной imemorycell_contents, обе следующие строки приведут к одному и тому же результату: присвоению переменной imemorycell_contents значения 20.
imemorycell_contents = 20;
*pimemorycell_address = 20;
Объявление указателей
В языках C/C++ все переменные должны быть предварительно объявлены. Объявление указателя pimemorycell_address выглядит следующим образом:
int *pimemorycell_address;
Символ * говорит о том, что создается указатель. Этот указатель будет адресовать переменную типа int. Следует подчеркнуть, что в C/C++ указатели могут хранить адреса только переменных конкретного типа. Если делается попытка присвоить указателю одного типа адрес переменной другого типа, возникнет ошибка либо во время компиляции, либо во время выполнения программы.
int *pi
float real_value = 98.26;
pi = &real_value;
В данном случае переменная pi объявлена как указатель типа int. Но в третьей строке делается попытка присвоить этому указателю адрес переменной real_value, имеющей тип float. В результате компилятор выдаст предупреждение вида "несовместимые операнды в операции присваивания", а программа, использующая указатель pi, будет работать неправильно.
Использование указателей
В следующем фрагменте программы посредством указателей производится обмен значений между переменными iresult_a и iresult_b:
int iresialt_a = 15,
iresult_b = 37, itemporary;
int *piresult;
piresult = &iresult_a;
itemporary = *piresult;
*piresult = iresult_b;
iresult_b = itemporary;
Первая строка содержит традиционные объявления переменных. При этом в памяти
компьютера резервируются три ячейки для хранения целочисленных значений, каждой
ячейке присваивается имя, и две из них инициализируются начальными значениями.
Предположим, что переменная iresult_a хранит свои значения в ячейке с адресом
5328, переменная iresult_b связана с ячейкой 7916, a itemporary— с ячейкой 2385
(рис. 9.2).
Рис. 9.2. Резервирование и инициализация ячеек памяти
Во второй строке программы создается указатель piresult. При этом также происходит резервирование именованной ячейки памяти (скажем, с адресом 1920). Поскольку инициализация не производится, то в данный момент указатель содержит пустое значение. Если попытаться применить к нему оператор *, то компилятор не сообщит об ошибке, но и не возвратит никакого адреса.
В третьей строке происходит присваивание указателю piresult адреса переменной iresult_a(рис. 9.3).
В следующей строке в переменную itemporary записывается содержимое переменной iresult_a, извлекаемое с помощью выражения *piresult:
Рис. 9.3. Присваивание указателю piresult адреса переменной iresult_a
Таким образом, переменной itemporary присваивается значение 15 (рис. 9.4).
Если перед именем указателя piresult не поставить символ *, то в результате
в переменную itemporary будет ошибочно записано содержимое самого указателя,
т.е. 5328. Это очень коварная ошибка, поскольку многие компиляторы не выдают
в подобных ситуациях предупреждений или сообщений об ошибке. Компилятор VisualC++
отобразит предупреждение вида "разные уровни косвенной адресации в операции
присваивания".
Рис. 9.4. Запись в переменную ietemporary значения переменной iresult_a
В пятой строке содержимое переменной iresult_b копируется в ячейку памяти, адресуемую указателем piresult(рис. 9.5):
*piresult = iresult_b;
Рис. 9.5. В переменную iresult_a записывается значение переменной
iresult_b
В последней строке число, хранящееся в переменной itemporary, просто копируется в переменную iresult_b(рис. 9.6).
Рис. 9.6. Конечный результат
В следующем фрагменте программы демонстрируется возможность манипулирования адресами, хранящимися в указателях. В отличие от предыдущего примера, где переменные обменивались значениями, здесь осуществляется обмен адресами переменных.
char cswitchl = 'S', cswitch2 = "I" ;
char *pcswitchl, *pcswitch2, *pctemporary;
pcswitchl = scswitchl;
pcswitch2 = &cswitch2;
pctemporary = pcswitchl;
pcswitchl = pcswitch2;
pcswitch2 = pctemporary;
printf("%c%c",*pcswitchl, *pcswitch2);
На рис. 9.7 показана схема отношений между зарезервированными ячейками памяти
после выполнения первых четырех строк программы. В пятой строке содержимое указателя
pcswitchl копируется в переменную pctemporary, в результате чего оба указателя
адресуют одну переменную: cswitchl(рис. 9.8).
Рис. 9.7. Исходные отношения между переменными
Рис. 9.8. Указателю pctemporary присвоен адрес, хранящийся в указателе pcswitch1
В следующей строке содержимое указателя pcswitch2 копируется в указатель pcswitchl, после чего оба будут содержать адрес переменной cswitch2 (рис. 9.9):
pcswitchl = pcswitch2;
Рис. 9.9. Присвоение указателю pcswitch1 адреса, хранящегося в указателе
pcswitch2
Обратите внимание, что если бы содержимое указателя pcswitchl не было продублировано во временной переменной pctemporary, то в результате выполнения предыдущего выражения ссылка на адрес переменной cswitchl была бы утеряна.
В предпоследней строке происходит копирование адреса из указателя pctemporary в указатель pcswitch2 (рис. 9.10). В результате работы функции printf() получаем:
TS
Рис. 9.10. Передача адреса от указателя pctemporary к указателю pcswitch2
Заметьте: в ходе выполнения программы исходные значения переменных cswitchlи cswitch2 не изменялись. Описанный метод может пригодиться вам в дальнейшем, так как, в зависимости от размеров объектов, часто бывает проще копировать их адреса, чем перемещать содержимое.
Инициализация указателей
Указатели можно инициализировать при их объявлении, как и любые другие переменные. Например, в следующем фрагменте создаются две именованные ячейки памяти: iresult и piresult.
int iresult;
int *piresult = &iresult;
Идентификатор iresult представляет собой обычную целочисленную переменную, apiresult— указатель на переменную типа int. Одновременно с объявлением указателя piresult ему присваивается адрес переменной iresult. Будьте внимательны: здесь инициализируется содержимое самого указателя, т.е. хранящийся в нем адрес, но не содержимое ячейки памяти, на которую он указывает. Переменная iresult остается неинициализированной.
В приведенной ниже программе объявляется и инициализируется указатель на строку-палиндром, одинаково читаемую как слева направо, так и справа налево:
/*
* psz.c
* Эта программа на языке С содержит пример инициализации указателя.
*/
#include <stdio.h>
#include <string.h>
char *pszpalindrome = "Доммод";
int i;
for (i = strlen(pszpalindrome) - 1; i >= 0; i--)
printf("%c",pszpalindrome[i]);
printf("%s",pszpalindrome); }
В указателе pszpalindrome сохраняется адрес только первого символа строки.
Но это не значит, что оставшаяся часть строки пропадает: компилятор заносит
все строковые константы, обнаруженные им в программе, в специальную скрытую
таблицу, встраиваемую в программу. Таким образом, в указатель записывается адрес
ячейки таблицы, связанной с данной строкой.
Функция strlen(), объявленная в файле STRING.H, в качестве аргумента принимает
указатель на строку, заканчивающуюся нулевым символом, и возвращает число символов
в строке, не считая последнего. В нашем примере строка состоит из семи символов,
но счетчик цикла for инициализируется значением 6, поскольку строка в нем интерпретируется
как массив, содержащий элементы от нулевого до шестого. На первый взгляд, может
показаться странной взаимосвязь между строковыми указателями и массивами символов,
но если мы вспомним, что имя массива по сути своей является указателем на первый
элемент массива, то станет понятным, почему переход от имени указателя к имени
массива в программе не вызвал возражений компилятора.
Ограничения на использование оператора &
Оператор взятия адреса (&) можно применять далеко не с каждым выражением. Ниже иллюстрируются ситуации, когда оператор & используется неправильно:
/*с константами*/
pivariable = &48;
/*в выражениях с арифметическими операторами*/
int iresult = 5;
pivariable = &(iresult + 15);
/* с переменными класса памяти register*/
register int registerl; pivariable = ®isterl;
В первом случае делается недопустимая попытка получить адрес константного значения. Поскольку с константой 48 не связана ни одна ячейка памяти, операция не может быть выполнена.
Во втором случае программа пытается найти адрес выражения iresult+15. Поскольку результатом этого выражения является число, находящееся в программном стеке, его адрес не может быть получен.
В последнем примере объявляется регистровая переменная, смысл которой состоит в том, что предполагается частое ее использование, поэтому она должна располагаться не в памяти, а непосредственно в регистрах процессора. Компилятор может проигнорировать подобный запрос и разместить переменную в памяти, но в любом случае операция взятия адреса считается неприменимой по отношению к регистровым переменным.
Указатели на массивы
Как уже говорилось, указатели и массивы логически связаны друг с другом. Вспомните из предыдущей главы, что имя массива является константой, содержащей адрес первого элемента массива. В связи с этим значение имени массива не может быть изменено оператором присваивания или каким-нибудь другим оператором. Например, ниже создается массив типа floatс именем ftemperatures:
#define IMAXREADINGS 20
float ftemperatures[IMAXREADINGS]; float *pftemp;
В следующей строке объявленному выше указателю pftemp присваивается адрес первого элемента массива:
pftemp = ftemperatures;
Это же выражение можно записать следующим образом:
pftemp = &ftemperatures[0];
Тем не менее, даже если указатель описан для хранения адреса переменных типа float, все равно следующие выражения недопустимы:
ftemperatures = pftemp;
&ftemperatures[0]= pftemp;
Эти выражения невыполнимы, поскольку в них делается попытка изменить константу ftemperatures и эквивалентное ей выражение &ftemperatures[0] , что так же бессмысленно, как и строка
10 = pftemp;
Указатели на указатели
В C/C++ можно создавать указатели на другие указатели, которые, в свою очередь, содержат адреса реальных переменных. Смысл этого процесса проиллюстрирован на рис. 9.11, где ppiявляется указателем на указатель.
Рис. 9.11. Пример указателя на указатель
Чтобы объявить в программе указатель, который будет хранить адрес другого указателя, нужно просто удвоить число звездочек в объявлении:
int **ppi;
Каждый символ * читается как указатель на. Таким образом, количество указателей в цепочке, задающее уровень косвенной адресации, соответствует числу звездочек перед именем идентификатора. Уровень косвенной адресации определяет, сколько раз следует выполнить операцию раскрытия указателя, чтобы получить значение конечной переменной.
В следующем фрагменте создается ряд указателей с различными уровнями косвенной адресации (рис. 9.12).
int ivalue = 10;
int *pi;
int **ppi;
int ***pppi;
pi = &ivalue;
ppi = π
pppi= &ppi;
Рис. 9.12. Несколько уровней косвенной адресации
В первых четырех строках объявляются четыре переменные: ivalue типа int, указатель piна переменную типа int (первый уровень косвенной адресации), указатель ppi (второй уровень косвенной адресации) и указатель pppi (третий уровень косвенной адресации). Этот пример показывает, что при желании можно создать указатель любого уровня.
В пятой строке указателю первого уровня piприсваивается адрес переменной ivalue. Теперь значение переменной ivalue(10) может быть получено с помощью выражения *pi. В шестой строке в указатель второго уровня ppiзаписывается адрес (но не содержимое) указателя pi, который, в свою очередь, указывает на переменную ivalue. Обратиться к значению переменной можно будет посредством выражения **ppi. В последней строке аналогичным образом заполняется указатель третьего уровня.
В C/C++ указатели можно инициализировать сразу при объявлении, как и любые другие переменные. Например, указатель pppi можно было бы инициализировать следующей строкой:
int ***pppi = &ppi;
Строковые константы в действительности представляют собой символьные массивы с конечным нулевым символом (рис. 9.13). Указатель на строку можно объявить и инициализировать следующим образом:
char *psz= "Файл не
найден";
Рис. 9.13. Схема представления в памяти строки символов
Данное выражение создает указатель psz типа char и присваивает ему адрес первого
символа строки — 'Ф' (рис. 9.14). Сама строка заносится компилятором в специальную
служебную таблицу.
Рис. 9.14. Инициализация указателя на строку
Приведенную выше строку можно записать по-другому:
char *psz;
psz= "Файл не найден";
Следующий пример иллюстрирует одно часто встречающееся заблуждение, касающееся использования строковых указателей и символьных массивов:
char *psz= "Файл не найден";
char pszаrray[] = "Файл не найден";
Основное различие между этими двумя выражениями состоит в том, что значение указателя psz может быть изменено (поскольку указатель является разновидностью переменной), а значение имени массива pszarray не может быть изменено, так как это константа. По этой причине показанные ниже выражения ошибочны:
/* Ошибочный код */
char pszarray[15];
pszarray= "Файл не найден";
Хотя, на первый взгляд, все кажется логичным, в действительности в данном выражении
оператор присваивания пытается скопировать адрес первой ячейки строки "Файл
не найден" в объект pszarray.- Но поскольку имя массива pszarray является
константой, а не переменной-указателем, то будет выдано сообщение об ошибке.
Следующий фрагмент на языке C++ ошибочен по той причине, что указатель был объявлен,
но не инициализирован:
/* Ошибочный код */
char *psz; cin >> psz;
Чтобы решить проблему, нужно просто зарезервировать память для указателя:
char string [10];
char *psz = string;
cin.get (psz,10) ;
Поскольку имя string представляет собой адрес первой ячейки массива, то второе выражение не только резервирует память для указателя, но и инициализирует его, присваивая ему адрес первого элемента массива string. В данном случае метод cin.get ( ) (его назначение состоит в чтении из входного потока заданного количества символов) будет выполнен успешно, поскольку в функцию передается действительный адрес массива.
Арифметические операции над указателями
Языки C/C++ дают возможность выполнять различные операции над указателями. В предыдущих примерах мы наблюдали, как адрес, хранящийся в указателе, или содержимое переменной, адресуемой указателем, присваивались другим указателям аналогичного типа. Кроме того, в C/C++ над указателями можно производить два математических действия: сложение и вычитание. Проиллюстрируем сказанное примером.
//
// ptarith.cpp
// Эта программа на языке C++ содержит примеры
// арифметических выражений с участием указателей.
//
#include <iostream.h>
void main (){
int *pi;
float *pf;
int an_integer;
float a_real;
pi = &an_integer; pf = &a_real; pi++;
pf++; }
Предположим, что в данной системе для целых чисел отводится 2 байта, а для чисел с плавающей запятой — 4 байта. Допустим также, что переменная an_integer хранится в ячейке с адресом 2000, а переменная a_real— в ячейке с адресом 4000. После выполнения двух последних строк программы значение указателя pi становится 2002, а указателя pf— 4004. Но, подождите, разве мы не знаем, что оператор ++ увеличивает значение переменной на единицу? Это справедливо для обычных переменных, но не для указателей.
В главе 4 мы познакомились с понятием перегрузки операторов. Операторы инкремента (++) и декремента (--) как раз являются примерами перегруженных операторов. Они модифицируют значение операнда-указателя на число, соответствующее размеру занимаемой им ячейки памяти. Для указателей типа int это 2, для указателей типа float—4 байта. Этот же принцип справедлив для указателей любых других типов. Если взять указатель, связанный со структурой размером 20 байтов, то единичное приращение такого указателя также будет соответствовать 20-ти байтам.
Адрес, хранимый в указателе, можно также изменять путем прибавления или вычитания любых целых чисел, а не только единицы, как в случае операторов ++ и --. Например, чтобы сместить адрес на 4 ячейки, можно использовать такое выражение:
pf = pf + 4;
Рассмотрим следующую программу и попытаемся представить, каким будет результат ее выполнения.
//
// sizept.cpp
// Эта программа на языке C++ демонстрирует приращение
// указателя на значение, большее единицы.
//
#include <iostream.h>
void main() {
float fvalues[] = (15.38,12.34,91.88,11.11,22.22);
float *pf;
size_t fwidth;
pf = &fvalues [0];
fwidth = sizeof (float);
pf = pf + fwidth;
cout << *pf; }
Предположим, адрес переменной fvalue равен FFCA. Переменная fwidth принимает свое значение от оператора sizeof(float) (в нашем случае — 4). Что произойдет после выполнения предпоследней строки программы? Адрес в указателе pf поменяется на FFDA, а не на FFCE, как можно было предположить. Почему? Не забывайте, что при выполнении арифметических действий с указателями нужно учитывать размер объекта, адресуемого данным указателем. В нашем случае величина приращения будет: 4x4 (размерность типа данных float) = 16. В результате указатель pfсместится на 4 ячейки массива, т.е. на значение 22,22.
Применение арифметики указателей при работе с массивами
Если вы знакомы с программированием на языке ассемблера, то наверняка сталкивались с задачей вычисления физических адресов элементов, хранящихся в массивах. При использовании индексов массивов в C/C++ выполняются те же самые операции. Разница состоит только в том, что в последнем случае функцию непосредственного обращения к адресам памяти берет на себя компилятор.
В следующих двух программах создаются массивы из 10-ти символов. В обоих случаях программа получает от пользователя элементы массива и выводит их на экран в обратной последовательности. В первой программе применяется уже знакомый нам метод обращения к элементам по индексам. Вторая программа идентична первой, но значения элементов массива считываются по их адресам посредством указателей. Вот первая программа:
/*
* arrayind.c
* Эта программа на языке С демонстрирует
* обращение к элементам массива по индексам.
*/
#include <stdio.h>
#define ISIZE 10
void main () (
char string10[ISIZE];
int i;
for(i = 0; i < ISIZE; i++)
string10[i]= getchar();
for(i= ISIZE - 1; i >= 0; i--)
putchar(string10[i]); )
}
Теперь приведем текст второй программы:
/*
* arrayptr.c
* Эта программа на языке С демонстрирует обращение к
* элементам массива посредством указателей.
*/
#include <stdio.h>
#define ISIZE 10
void main ()
{
char string10[ISIZE];
char *pc;
int icount;
pc = string10;
for(icount = 0; icount < ISIZE; icount++) {
*pc = getchar();
pc++; }
рс = stringlO + (ISIZE - 1);
for(icount = 0; icount < ISIZE; icount++)
{ putchar(*pc); pc—; }
}
Первая программа достаточно проста и понятна, поэтому сосредоточим наше внимание на второй программе, в которой используются арифметические выражения с указателями. Переменная рс имеет тип char*, означающий, что перед нами указатель на символ. Поскольку массив string10 содержит символы, указатель рс может хранить адрес любого из них. В следующей строке программы в указатель рс записывается адрес первой ячейки массива:
рс = string10;
В цикле for программа считывает ряд символов, число которых определяется константой isize,и сохраняет их в массиве string10. Для занесения очередного символа по адресу, содержащемуся в указателе рс, применяется оператор *:
*рс = getchar() ;
В следующей строке значение указателя увеличивается на четыре с помощью операции
инкрементирования (++). Таким способом осуществляется переход к следующему элементу
массива.
Чтобы начать вывод символов массива в обратном порядке, следует в первую очередь
присвоить указателю рс адрес последнего элемента массива:
рс = string10 + (ISIZE - 1);
Чтобы переместить указатель на 10-й элемент, мы прибавляем к базовому адресу
массива значение 9, поскольку первый элемент имеет нулевое смещение.
Во втором цикле for указатель рс последовательно смещается от последнего к первому
элементу массива с помощью операции декрементирования (--). Функция putchar()
выводит на экран содержимое ячеек, адресуемых указателем.
Распространенная ошибка при использовании операторов ++ и --
Напомним, что следующие два выражения будут иметь разный результат:
*рс++ = getchar ();
*++рс = getchar ();
Первое выражение (с постинкрементом) присваивает символ, возвращаемый функцией getchar(), текущей ячейке, адресуемой указателем рс, после чего выполняется приращение указателя рс. Во втором выражении (с преинкрементом) сначала происходит приращение указателя рс, после чего в ячейку по обновленному адресу будет записан результат функции getchar(). Сказанное справедливо также для префиксной и постфиксной форм оператора — (декремент).
Применение квалификатора const совместно с указателями
В рассматриваемой нами теме указателей еще достаточно "подводных камней". Посмотрите на следующие два объявления указателей и попробуйте разобраться в различиях между ними:
const MYTYPE *pmytype_l;
MYTYPE * const pmytype_2 = &mytype;
В первом случае переменная pmytype_lобъявляется как указатель на константу
типа mytype. Во втором случае pmytype_2 — это константный указатель на переменную
mytype. Heпонятно?
Хорошо, давайте разбираться дальше. Идентификатор pmytype_1 является
указателем. Указателю, как и любой переменной, можно присваивать значения соответствующего
типа данных. В данном случае это должен быть адрес, по которому локализован
объект типа mytype. А ключевое слово const в объявлении означает
следующее: хотя указателю pmytype_1 может быть присвоен адрес любой ячейки
памяти, содержащей данные типа mytype, впоследствии содержимое этой ячейки
не может быть изменено путем обращения к указателю. Попробуем прояснить
ситуацию на примере:
pmytype_l = &mytypel; //
допустимо
pmytype_l = &mytype2; //
допустимо
*pmytype_l= (MYTYPE)float_value; // недопустимая попытка изменить
// значение защищенной ячейки памяти
Теперь рассмотрим переменную pmytype_2, которая объявлена как константный указатель. Она может содержать адрес ячейки памяти с данными типа mytype, но этот адрес недоступен для изменения. По этой причине инициализация константного указателя обязательно должна происходить одновременно с его объявлением (в приведенном выше примере указатель инициализируется адресом переменной mytype). С другой стороны, содержимое ячейки, на которую ссылается такой указатель, может быть свободно изменено:
pmytype_2 = &mytypel; // недопустимая попытка изменить
// защищенный адрес
*pmytype_2 = (MYTYPE)float_value; // допустимое изменение содержимого ячейки
// памяти
А теперь подумайте, какой из двух вариантов применения ключевого слова const соответствует объявлению массива? Ответ: второй. Напомним, что имя массива представляет собой неизменяемый адрес первого его элемента. С точки зрения компилятора, ничего не изменилось бы, если бы массив объявлялся следующим образом:
тип данных* const имя массива = адрес_первого_элемента;
Другие операции над указателями
Мы уже рассмотрели примеры, иллюстрирующие применение операторов ++ и -- к указателям, а также добавление целого значения к указателю. Ниже перечислены другие операции, которые могут быть выполнены над указателями:
В результате вычитания целого числа указатель будет ссылаться на элемент, смещенный
на указанную величину влево по отношению к текущей ячейке.
Результатом вычитания указателя из указателя будет целое число, соответствующее
числу элементов между ячейками, на которые они ссылались. Предполагается, что
оба указателя одного типа и связаны с одним и тем же массивом. Если в выражении
используются два разнотипных указателя или если они ссылаются на разные массивы,
то результат операции невозможно предсказать.
Независимо от того, в какой операции участвует.указателъ, компилятор не может проследить за тем, чтобы в результате операции адрес не выходил за пределы массива.
Указатели одного типа можно сравнивать друг с другом. Возвращаемые значения
true(! 0) или false(0) можно использовать в условных выражениях и присваивать
переменным типа intтак же, как результаты любых других логических операций.
Один указатель будет меньше другого, если он указывает на ячейку памяти с меньшим
индексом. При этом предполагается, что оба указателя связаны с одним и тем же
массивом.
Наконец, указатели можно сравнивать с нулем. В данном случае можно выяснить
только равенство/неравенство нулю, поскольку указатели не могут содержать отрицательные
значения. Нулевое значение указателя означает, что он не связан ни с каким объектом.
Ноль является единственным числовым значением, которое можно непосредственно
присвоить указателю независимо от его типа. Кроме того, указатель можно сравнить
с константным выражением, результатом которого является ноль, а также с другим
указателем типа void(в последнем случае указатель следует предварительно привести
к типу void).
Все остальные операции с указателями запрещены. Например, нельзя складывать
два указателя, а также умножать и делить их.
Физическая реализация указателей
В примерах данной главы адреса возвращались как целые числа. Исходя из этого, вы могли сделать заключение, что указатели являются переменными типа int. В действительности это не так. Указатель содержит адрес переменной определенного типа, но при этом свойства указателя не сводятся к свойствам рядовых переменных типа int или float. В некоторых системах значение указателя может быть скопировано в переменную типа int и наоборот, хотя стандарты языков C/C++ не гарантируют подобную возможность. Чтобы обеспечить правильную работу программы в разных системах, такую практику следует исключить.
Во всех рассмотренных до сих пор примерах указатели использовались для обращения
к определенным значениям, содержащимся в памяти компьютера. Теперь мы познакомимся
с указателями, связанными не с данными, а с программными кодами — функциями.
Указатели на функции обеспечивают возможность косвенного вызова функций, точно
так же как указатели на данные предоставляют косвенный доступ к ячейкам памяти.
С помощью указателей на функции можно решать многие важные задачи. Рассмотрим,
к примеру, функцию qsort(), осуществляющую сортировку массива. В числе ее параметров
должен быть задан указатель на функцию, выполняющую сравнение двух элементов
массива. Необходимость в функции-аргументе возникает в связи с тем, что алгоритм
сравнения может быть достаточно сложным и многофакторным, работающим по-разному
в зависимости от типа элементов. Код одной функции не может быть передан в другую
функцию как значение аргумента, но в C/C++ допускается косвенное обращение к
функции с помощью указателя.
Концепция использования указателей на функции часто иллюстрируется на примере
стандартной функции qsort(},прототип которой находится в файле STDLIB.H. В приводимом
ниже примере программы на языке С мы создаем собственную функцию сравнения icompare_funct
() и передаем указатель на нее в функцию qsort().
/*
* qsort.с
* Эта программа на языке С демонстрирует передачу указателя на функцию
* в качестве аргумента функции qsort().
*/
#include <stdio.h>
#include <stdlib.h>
#define IMAXVALUES 10
int icompare_funct(const void *iresult_a,
const void *iresult_b);
int (*ifunct_ptr) (const void *, const void *);
void main ()
{
int i ;
int iarray[IMAXVALUES] ={0, 5, 3, 2, 8, 7, 9, 1, 4, 6);
ifunct_ptr = icompare_funct;
qsort(iarray,IMAXVALUES, sizeof(int),ifunct_ptr);
for(i=0;i < IMAXVALUES; i++)
printf("%d", iarray[i]); }
int icompare_funct(const void *iresult_a, const void *iresult_b) f
return({* (int*)iresult_a) - (*(int*)iresult_b)); }
Функция icompare_funct() (которую будем называть адресуемой) соответствует требованиям, накладываемым на нее функцией qsort() (которую будем называть вызывающей): она принимает два аргумента типа void* и возвращает целочисленное значение. Напомним, что ключевое слово const в списке аргументов накладывает запрет на изменение данных, на которые указывают аргументы. Благодаря этому вызывающая функция в худшем случае неправильно отсортирует данные, но она не сможет их изменить! Теперь, когда синтаксис использования функции icompare_funct() стал понятен, уделим внимание ее телу.
Если адресуемая функция возвращает отрицательное число, значит, первый ее аргумент меньше второго. Ноль означает равенство аргументов, а положительное значение — что первый аргумент больше второго. Все эти вычисления реализуются единственной строкой, составляющей тело функции icompare_funct () :
return((*(int *)iresult_a) - (*(int *)iresult_b));
Поскольку оба указателя переданы в функцию как void*, они приводятся к соответствующему им типу int и раскрываются (*). Результат вычитания содержимого второго указателя из содержимого первого возвращается в функцию qsort() в качестве критерия сортировки.
Важная часть программы — объявление указателя на адресуемую функцию, расположенное сразу вслед за ее прототипом:
int (*ifunct_ptr)(const void *, const void *);
Это выражение определяет указатель ifunct_ptr на некую функцию, принимающую два константных аргумента типа void* и возвращающую значение типа int. Обратите особое внимание на скобки вокруг имени указателя. Выражение вида
int *ifunct_ptr(const void *, const void *);
воспринимается не как объявление указателя, а как прототип функции, поскольку
оператор вызова функции, (), имеет более высокий приоритет, чем оператор раскрытия
указателя *. В результате последний будет отнесен к спецификатору типа, а не
к идентификатору ifunct_ptr.
Функция qsort() принимает следующие параметры: адрес массива, который нужно
отсортировать (массив iarray), число элементов массива (константа imaxvalues),
размер в байтах элемента таблицы (sizeof(int)) и указатель на функцию сравнения
(ifunct_ptr() )
Рассмотрим еще несколько интересных примеров:
int * (* (*ifunct__ptr) (int) ) [5];
float (*(*ffunct_ptr)(int,int))(float);
typedef double (* (* (*dfunct_ptr) () ) [5]) () ;
dfunct_ptr A_dfunct_ptr;
(* (*function_array_ptrs () ) [5]) () ;
Первая строка описывает указатель ifunct_ptr на функцию, принимающую один целочисленный
аргумент и возвращающую указатель на массив из пяти указателей типа int.
Вторая строка описывает указатель ffunct_ptr на функцию, принимающую два целочисленных
аргумента и возвращающую указатель на другую функцию с одним аргументом типа
float и таким же типом результата.
Создавая с помощью ключевого слова typedef новый тип данных (более подробно
эта тема раскрывается в главе "Дополнительные типы данных"), можно
избежать повторения сложных деклараций. Третья строка читается следующим образом:
тип dfunct_ptr определен как указатель на функцию без аргументов, возвращающую
указатель на массив из пяти указателей, которые, в свою очередь, ссылаются на
функции без аргументов, возвращающие значения типа double. В четвертой строке
создается экземпляр такого указателя.
Последняя строка содержит объявление функции, а не переменной. Создаваемая функция
function_array_ptrs() не имеет параметров и возвращает указатель на массив из
пяти указателей, которые ссылаются на функции без аргументов, возвращающие значения
типа int(последнее подразумевается по умолчанию, если не указано иное).
Во время компиляции программ на языках C/C++ память компьютера разделяется
на четыре области: программного кода, глобальных данных, стек и динамическую
область ("куча"). Последняя отводится для хранения временных данных
и управляется функциями распределения памяти, такими как malloc() и free().
Функция mallос () резервирует непрерывный блок ячеек для хранения указанного
объекта и возвращает указатель на первую ячейку этого блока. Функция free()
освобождает ранее зарезервированный блок и возвращает эти ячейки в динамическую
область для последующего резервирования.
В качестве аргумента в функцию malloc() передается целочисленное значение,
указывающее количество байтов, которое необходимо зарезервировать. Если резервирование
прошло успешно, функция malloc() возвращает переменную типа void*,
которую можно привести к любому необходимому типу указателя. Концепция использования
указателей типа void описана в стандарте ANSI С. Этот спецификатор предназначен
для создания обобщенных указателей неопределенного типа, которые впоследствии
можно преобразовывать к требуемому типу. Обобщенный указатель сам по себе не
может быть использован для обращения к каким-нибудь данным, так как он не связан
ни с одним из базовых типов данных. Но любой указатель может быть приведен к
типу void и обратно без потери информации.
В следующем фрагменте программы резервируется память для 300 чисел типа float:
float *pf;
int inura_floats = 300;
pf = (float *) malloc(inum_floats * sizeof(float));
В данном примере функция malloc() резервирует блок памяти, достаточный
для хранения 300 значений типа float. Выделенный блок надежно отделен
от других блоков и не перекрывается ими. Предугадать, где именно он будет размещен,
невозможно. Все блоки особым образом помечаются, чтобы система могла легко их
обнаруживать и определять их размер.
Если блок ячеек больше не нужен, его можно освободить следующей строкой:
free ((void*) pf);
В C/C++ резервирование памяти осуществляется двумя способами. Так, при объявлении переменной указатель стека программы продвигается ниже по стеку, "захватывая" новую ячейку. Когда переменная выходит за пределы своей области видимости, область памяти, отведенная для переменной, автоматически освобождается путем перемещения указателя назад, выше по стеку. Размер необходимой стековой памяти всегда должен быть известен еще до начала компиляции.
Но иногда в программе необходимо создавать переменные, размер которых неизвестен заранее. В таких случаях ответственность за резервирование памяти возлагается на программиста, и для этих целей используется динамическая область. В программах на языках C/C++ резервирование и освобождение блоков памяти в динамической области может происходить в любой момент времени. Также важно запомнить, что объекты, хранимые в динамической области, в отличие от обычных переменных, не подчиняются правилам видимости. Они никогда не могут оказаться вне области видимости, поэтому вы же ответственны за то, чтобы освободить зарезервированную память, как только отпадет необходимость в соответствующем объекте. Если вы не позаботитесь об освобождении неиспользуемой памяти, а будете создавать все новые и новые динамические объекты, то рано или поздно программа столкнется с недостатком свободной памяти.
В компиляторах языка С для управления динамической памятью используются библиотечные функции malloc() и free(),которые мы только что рассмотрели. Создатели языка C++ посчитали оперирование свободной памятью столь важной задачей для работы программы, что добавили дополнительные операторы new и delete, аналогичные вышеуказанным функциям. Аргументом для оператора new служит выражение, возвращающее число байтов, которое необходимо зарезервировать. Этот оператор возвращает указатель на начало выделенного блока памяти. Аргументом оператора delete выступает адрес первой ячейки блока, который необходимо освободить. В следующих двух примерах демонстрируются различия в синтаксисе программ на языках С и C++, работающих с динамической областью памяти. Вот пример программы на языке С:
/*
* malloc.c
* Эта программа на языке С демонстрирует применение
* функций malloc() иfree()
*/
#include <stdio.h>
#include <stdlib.h>
#define ISIZE 512
void main()
{
int *pimemory_buffer;
pimemory_buffer = malloc(iSIZE * sizeof
(int)) ;
if(pimemory_buffer == NULL)
printf("Недостаточно памяти\n"); else
printf("Память зарезервирована\n");
free(pimemory_buffer) ;
}
Первое, на что следует обратить внимание еще в начале программы, — это подключение к программе файла STDLIB.H, содержащего прототипы функций malloc() и free().Функция malloc() передает указателю pimemory_buffer адрес зарезервированного блока размером ISIZE*sizeof(int). Надежный алгоритм всегда должен включать проверку успешности резервирования памяти, поэтому указатель проверяется на равенство значению null, Функция malloc() всегда возвращает null, .если выделить память не удалось. Программа завершается вызовом функции free(), которая освобождает только что зарезервированную область памяти. Программа на языке C++ мало отличается от предыдущего примера:
//
// newdel.cpp
// Эта программа на языке C++ демонстрирует применение
// операторов new и delete.
//
#include <iostream.h>
#define NULL 0
#define ISIZE 512
void main()
(
int *pimemory_buffer;
pimemory_buffer = new int[ISIZE];
if(pimemory_buffer == NULL)
cout<< "Недостаточно памяти\n";
else
cout<< "Память зареэервирована\n";
delete(pimemory_buffer); }
Необходимости в подключении файла STDLIB.H здесь нет, так как операторы new и delete являются встроенными компонентами языка. В отличие от функции malloc(), где для определения точного размера блока применяется оператор sizeof, оператор new автоматически выполняет подобного рода вычисления, основываясь на заданном типе объекта. В обеих программах резервируется блок из 512 смежных ячеек типа int.
Указатели типа void
Если бы все указатели имели стандартный размер, не было бы необходимости на
этапе компиляции определять тип адресуемой переменной. Кроме того, с помощью
"стандартизированного" указателя можно было бы передавать в функции
адреса переменных любого типа, а затем, в теле функции, привести указатель к
требуемому типу в соответствии с полученной дополнительной информацией. Используя
такой подход, можно создавать универсальные функции, подходящие для обработки
данных разного типа.
Вот почему в язык C++ был добавлен указатель типа void. Использование
ключевого слова void при объявлении указателя имеет другой смысл, чем в списке
аргументов или в качестве возвращаемого значения ( в этих случаях void означает
"ничего"). Указатель на void - это универсальный указатель на данные
любого типа. В следующей программе на языке С++ демонстрируется применение таких
указателей:
//
// voidpt.cpp
// Эта программа на языке С++ демонстрирует
// применение указателей типа void.
//
#include <iostream.h>
#define ISTRING_MAX 50
void voutput(void *pobject, char cflag)
void main() {
int *pi;
char *psz;
float *pf;
char cresponse, cnewline;
cout << "Задайте тип данных,
которые\n";
cout << "вы хотите ввести.\n\n";
cout << "(s)tring, (i)nt, (f)loat: ";
cin >> cresponse;
switch(cresponse) {
case 's':
psz = new char [ISTRING_MAX];
cout << "\nВведите строку: ";
cin.get (psz, cresponse);
voutput (pi, cresponse);
break;
case 'i':
pi = new int;
cout << "\nВведите целое число :";
cin >> *pi;
voutput (pi, cresponse);
break;
case 'f':
pf = new float;
cout << "\nВведите число с плавающей запятой: ";
cin >> *pf;
voutput (pi, cresponse);
break;
default:
cout << "\n\nТакой тип данных не поддерживается!";
}
}
void voutput(void *pobject, char cflag)
{
switch(cflag) {
case 's':
cout << "\nВы ввели строку
" << (char *) pobject;
delete pobject;
break;
case 'i':
cout<< "\nВы ввели целое число "
<< *((int*) pobject);
delete pobject;
break;
case 'f':
cout<< "\nВы ввели число с плавающей запятой "
<< *((float *) pobject);
delete pobject;
break;
}
}
Прежде всего обратите внимание на прототип функции voutput(
) , в частности на то, что первый параметр функции — pobject— представляет собой
обобщенный указатель void*. В функции main()создаются указатели трех конкретных
типов: int*, char* и float*. Они будут использоваться в зависимости от того,
какого типа данные введет пользователь.
Выполнение программы начинается с выдачи пользователю приглашения указать тип
данных, которые он собирается вводить. Вас может удивить, почему для считывания
ответа используются две различные команды. Первый объект cin считывает символ,
введенный пользователем с клавиатуры, но не воспринимает символ новой строки
\n. Исправляет ситуацию функция cin. get (cnewline) .
Обработка ответа пользователя осуществляется с помощью инструкции switch, где
происходит выбор сообщения, выводимого на экран. В программе используется одна
из трех строк инициализации указателя:
psz = new char[ISTRING_MAX]; pi = new int; pf = new float;
Следующее выражение предназначено для считывания введенной строки текста, длина которой не может превышать значение, заданное константой istring_max, — 50 символов:
cin.get(psz, ISTRING_MAX) ;
Поскольку функция cin . get() ожидает в качестве первого аргумента указатель на строку, при вызове функции voutput( ) нет необходимости выполнять операцию раскрытия указателя:
voutput (psz, cresponse) ;
Две следующие ветви caseвыглядят почти одинаково, за исключением отображаемых сообщений и типа адресуемых переменных. Обратите внимание, что в трех различных обращениях к функции voutput() в качестве аргументов используются указатели разных типов:
voutput(psz,cresponse);
voutput(pi,cresponse);
voutput(pf,cresponse);
Функция voutput() воспринимает все эти аргументы независимо от их типа только благодаря тому, что в объявлении функции указан параметр типа void*. Напомним, что для извлечения содержимого указателей, заданных как void*, их сначала нужно привести к конкретному типу данных, например char*.
Начинающие программисты склонны к тому, чтобы быстро забывать о созданных ими динамических переменных. В нашей программе каждая ветвь case завершается явным удалением созданной перед этим динамической переменной. Где именно в программе происходит создание и удаление динамических переменных, зависит только от привычек программиста и особенностей конкретного приложения.
Подробнее об указателях и массивах
В следующих параграфах на примерах программ мы более подробно рассмотрим взаимосвязь между массивами и указателями.
Строки (массивы типа char)
Многие операции со строками в языках C/C++ выполняются с применением указателей и арифметических операций над ними, поскольку доступ к отдельным символам строки осуществляется, как правило, последовательно. Следующая программа на языке C++ является модификацией рассмотренной ранее в этой главе программы для вывода строки-палиндрома:
//
// chrarray.c
// Эта программа на C++ выводит на экран массив символов в обратной
// последовательности с применением арифметики указателей.
//
#include <iostream.h>
#include <string.h>
void main () {
char pszpalindrome[] = "Дом мод";
char *pc;
pc = pszpalindrome + (strlen(pszpalindrome)
- 1);
do {
cout << *pc;
pc--;
} while (pc >= pszpalindrome); }
После объявления и инициализации массива pszpalindrome создается указатель рс типа char*. Вспомните, что имя массива само по себе является адресом. Сначала указателю рс присваивается адрес последнего символа массива. Это осуществляется с помощью функции strlen(), которая возвращает длину массива символов.
Функция strlen() не учитывает символ окончания Строки \0.
Почему от полученной длины массива отнимается единица? Не забывайте, что первый
элемент массива имеет нулевое смещение, т.е. ею индекс равен 0, а не 1. Поэтому
вывод следует выполнять начиная с индекса, на единицу меньшего, чем количество
символов в массиве.
После того как указатель переместится на последний значащий символ массива,
запускается цикл do/while. В этом цикле на экран выводится символ, адресуемый
текущим значением указателя, а в конце каждой итерации адрес указателя уменьшается
на единицу, после чего полученный адрес сравнивается с адресом первого элемента
массива. Цикл продолжается до тех пор, пока все элементы массива не будут выведены
на экран.
Массивы указателей
В языках C/C++ имеется еще одна интересная возможность — создавать массивы
указателей. Такой массив представляет собой совокупность элементов, каждый из
которых является указателем на другие объекты. Эти объекты, в свою очередь,
также могут быть указателями.
Концепция построения массивов указателей на указатели широко используется при
работе с параметрами argc и argv функции main() , с которыми вы познакомились
в главе "Функции". В следующей программе определяется максимальное
или минимальное из значений, введенных в командной строке (ожидается ввод неотрицательных
чисел). Аргументами могут быть либо только числа, либо, в дополнение к ним,
специальная опция, задающая режим работы: поиск минимального (-s, -S) или максимального
(-l, -L) значений.
//
// argcargv.cpp
// Эта программа на языке C++ демонстрирует работу с массивами указателей
// на указатели на примере параметров argcи argvфункции main().
//
#include <iostream.h>
#include <process.h> // exit()
#include <stdlib.h> // atoi()
#define IFIND_LARGEST 1
#define IFIND_SMALLEST 0
#define IMAX32767
int main(int argc, char *argv[]) f
char *psz;
int ihow_many;
int iwhich_extreme = IFIND_SMALLEST; int irange_boundary = IMAX;
if(argc--< 2) {
cput<< "\nВведите одну из опций -S, -s,-L, -l"
<< " и, как минимум, одно целое число."; exit(l); }
if ((*++argv) [0] == '-') {
psz = argv[0] + 1; switch (*psz) {
case ' s ' : case 'S':
iwhich_extreme = IFIND_SMALLEST;
irange_boundary = IMAX;
break;
case 'l':
case 'L':
iwhich_extreme = IFIND_LARGEST;
irange_boundary =0;
break;
default:
cout << "Нераспознанныйаргумент " << *psz <<,
"\n";
exit(1) ;
}
if(*++psz != '\0'){
cout<< "Нераспознанный аргумент " << *psz << "\n"; exitfl.); ' )
if (--argc == 0) {
cout<< "Введите как
минимум одно число\n";
exit(l); }
argv++; }
ihow_many = argc;
while (argc--) {
int present_value;
present_value = atoi(*argv++);
if(iwhich_extreme .== IFIND_LARGEST && present_value > irange_boundary) irange_boundary = present_value;
if(iwhich_extreme == IFIND_SMALLEST S& present_value < irange_boundary) irange_boundary = present_value; }
cout << ( (iwhich_extreme) ?
"Максимальное " : "Минимальное ");
cout<< "значение в ряду из " << ihow_many
<< " введенных чисел = "
<< irange_boundary << "\n";
return(0); }
Прежде чем приступить к анализу текста программы, давайте разберемся, какие значения могут быть введены в командной строке в качестве аргументов. Ниже показан список возможных комбинаций аргументов:
argcargv
argcargv98
argcargv98 21
argcargv -s 98
argcargv -S 98 21
argcargv -l 14
argcargv -L 14 67
Напомним, что параметр а где функции main() представляет собой целое число, равное количеству аргументов командной строки, включая имя программы. Параметр argvпредставляет собой указатель на массив строковых указателей.
Примечание
Параметр argv не является константой. Напротив, это переменная, чье значение может быть изменено. Об этом важно помнить при изучении работы программы. Первый элемент массива — argv[0] — является указателем на строку символов, содержащую имя программы.
В первую очередь в программе проверяется, не является ли значение параметра argc меньшим 2. Если это так, значит, пользователь ввел в командной строке только имя программы без аргументов. По-видимому, пользователь не знает назначения программы или того, какие аргументы должны быть заданы в командной строке. Поэтому программа отобразит на экране информацию о своих аргументах и завершится выполнением функции exit().Обратите внимание, что после выполнения проверки счетчик аргументов уменьшается на единицу (argc--). К моменту начала цикла while счетчик должен стать равным количеству числовых аргументов, заданных в командной строке, здесь же мы уменьшили его на единицу, чтобы учесть аргумент, содержащий имя программы.
Затем происходит обращение к первому символу второго аргумента, и если полученный символ является дефисом, то программа определяет, какая именно опция, -s или -l, задана в командной строке. Заметьте, что к указателю массива argv сначала добавляется единица (++argv), в результате чего аргумент с именем программы (первый элемент массива) пропускается и активным становится следующий аргумент. Выражение с инкрементом взято в круглые скобки, так как идущий следом оператор индексации (квадратные скобки) имеет более высокий приоритет, и без скобок выражение возвращало бы второй символ имени программы.
Поскольку значение указателя argvбыло изменено и он теперь ссылается на второй аргумент командной строки, выражение argv[0] возвращает адрес первого символа этого аргумента. Путем прибавления единицы мы перемещаем указатель ко второму символу, записывая его адрес в переменную psz:
psz = argv[0] + 1;
Анализируя значение этого символа, программа определяет, какую задачу ставит перед ней пользователь: поиск минимального или максимального чисел из списка. При этом в переменной iwhich_extreme устанавливается флаг режима
(IFIND_SMALLEST — ПОИСК МИНИМAЛЬНОГО ЧИСЛA,IFIND_LARGEST — МAКСИМAЛЬНОГО),
а в переменную irange_boundary записывается начальное значение, с которым будут сравниваться числовые аргументы. Из соображений совместимости мы предполагаем, что неотрицательные числа типа int попадут в диапазон 0—32767, что соответствует 16-разрядным системам. В случае, если пользователь задаст неверную опцию, скажем, -d или -su, в ветви default будет выведено соответствующее сообщение. Проверка
if(--argc ==0)
позволяет убедиться, что после опции указан хотя бы один числовой аргумент. В противном случае программа завершит свою работу. Последняя строка данного блока
argv++;
необходима для того, чтобы переместить указатель argv на аргумент, идущий после
опции.
Функция atoi() в цикле while преобразовывает каждый из аргументов в целое число
(не забывайте, что на данный момент все они представлены в виде массивов символов)
и сохраняет результат в переменной present_value. Опять-таки после выполнения
этой функции указатель argv будет перемещен на следующий аргумент командной
строки. В оставшихся двух инструкциях if впеременную irange_boundагу записывается
текущее минимальное или максимальное значение из списка в зависимости от содержимого
флага iwhichextreme.
Дополнительные сведения об указателях на указатели
В следующей программе также демонстрируется применение указателей на указатели. Мы не рассмотрели ее раньше, когда обсуждали соответствующую тему, по той причине, что в этой программе используется динамическая память.
/*
* dblptr.c
* Эта программа на языке С демонстрирует работу с указателями на
указатели.
*/
#include <stdio.h>
#include <stdlib.h>
#define IMAXELEMENTS 3
void voutputdnt (**ppiresult_a, int
**ppiresult_b, int **ррiresult_с) ;
void vassignfint (*pivirtual_array[], int *pinewblock);
void main ()
{
int **ppiresult_a, **ppiresult_b, **ppiresult_c;
int *pivirtual_array[IMAXELEMENTS];
int *pinewblock, *pioldblock;
ppiresult_a = &pivirtual_array[0]; ppiresult_b = &pivirtual_array[l]; ,ppiresult_c = &pivirtual_array[2];
pinewblock = (int *) malloc(IMAXELEMENTS * sizeof(int)); pioldblock = pinewblock;
vassign(pivirtual_array, pinewblock);
**ppiresult_a = 7;
**ppiresult_b = 10;
**ppiresult_c =15;
voutput(ppiresult_a, ppiresult_b, ppiresult_c);
pinewblock = (int *) malloc(IMAXELEMENTS * sizeof(int));
*pinewblock = **ppiresult_a;
*(pinewblock +1)= **ppiresult_b;
*(pinewblock +2)= **ppiresult_c;
free (pioldblock) ;
vassign(pivirtual_array, pinewblock);
voutput(ppiresult_a, ppiresult_b, ppiresult_c);
void yassign(int *pivirtual_array [] , int *pinewblock)
pivirtual_array [0] = pinewblock;
pivirtual_af ray [1] = pinewblock + 1;
pivirtual_array [2] = pinewblock + 2;
void voutput (int **ppiresult_a, int **ppiresult_b, int **ppiresult_c){
printf ("%d\n",**ppiresult_a) ;
printf ("%d\n",**ppiresult_b) ;
printf ("%d\n",**ppiresult_c)
;
}
В этой программе указатели ppiresult_a, ppiresult_b иppiresult_c ссылаются на указатели с постоянными адресами (&pivirtual_array[0], spivirtual_array[l] , &pivirtual_array [2]), но содержимое последних может меняться динамически.
Рассмотрим блок объявлений в функции main().Переменные ppiresult_a,
ppiresult_b и ppiresult_c объявлены как указатели на указатели, содержащие адреса
значений типа int. Переменная pivirtual_array является массивом указателей типа
int с числом элементов, заданным константой imaxelements. Последние две переменные,
pinewblock и pioldblock, являются одиночными указателями такого же типа, что
и массив pivirtual_array. Взаимоотношения между всеми переменными после того,
как указателям ppiresult_a, pplresult_b и ppiresult_c были присвоены адреса
соответствующих элементов массива pivirtual_array, показаны на рис. 9.15.
Рис. 9.15. Взаимоотношения между переменными программы после инициализации
указателей ppiresult_a, pplresult_b и ppiresult_c
Идея программы заключается в том, чтобы в упрощенной форме проиллюстрировать процессы, на самом деле происходящие в многозадачной среде. Программа считает, что содержит действительный адрес переменной, хотя на самом деле она имеет в своем распоряжении лишь фиксированный адрес ячейки массива, в которой хранится текущий адрес переменной. Когда операционная система в соответствии со своими потребностями перемещает данные, хранимые в оперативной памяти, она автоматически обновляет ссылки на эти данные в массиве указателей. Используемые в программе указатели при этом сохраняют прежние адреса.
На рис. 9.16 показано, что произойдет после того, как в динамическую память будет помещен массив pinewblock, переменной pioldblock будет присвоен его адрес и выполнена функция vassign(). Обратите внимание на соответствие адресов, хранящихся в "физическом" массиве pinewblockи "виртуальном" массиве pivirtual_array.
В функцию vassign() в качестве аргументов передаются массив pivirtual_arгау
и адрес только что созданного динамического блока, представленного переменной
pinewblock. В теле функции каждому элементу массива присваивается соответствующий
ему адрес динамической ячейки памяти. Поскольку обращение к массиву из функции
осуществляется по адресу, все изменения вступают в силу и в теле функции main().
На рис. 9.17 показано состояние переменных после присвоения ячейкам динамической
памяти трех целочисленных значений.
Далее ситуация становится интереснее. С помощью функции malloc() в динамической
памяти создается еще один блок ячеек, адрес которого записывается в указатель
pinewblock. А указатель pioldblock по-прежнему связан с созданным ранее блоком.
Все это сделано для того, чтобы примерно смоделировать процесс перемещения блока
ячеек в новую позицию.
Рис.9.16. Динамическое создание блока ячеек и инициализация массива pivirtual_array
Рис. 9.17. Заполнение ячек данными
Как показано на рис. 9.18, перемещение блока сопровождается перемещением всех связанных с ним данных, что реализуется следующими строками программы:
*pinewblock = **ppiresult_a;
*(pinewblock + 1) = **ppiresult_b;
*(pinewblock + 2) = **ppiresult_c;
Поскольку указатель pinewblock содержит адрес первой ячейки блока, то для того
чтобы обратиться к следующим ячейкам, необходимо воспользоваться уже знакомыми
нам арифметическими операциями над указателями. При этом арифметическое
выражение следует взять в скобки, чтобы первой была выполнена операция сложения,
а затем применен оператор раскрытия указателя (*).
Рис. 9.18. Создание и заполнение нового динамического блока ячек
На рис. 9.19 показано, что произойдет после вызова функций free()и vassign() для переадресации указателей массива pivirtual_arrayна ячейки нового блока.
Рис. 9.19. Переадресация указателей массива pivirtual_array
Самое важное, что наглядно демонстрирует рис. 9.19, заключается в том, что указатели ppiresult_a, ppiresult_b и ppiresult_c не изменились в ходе выполнения программы. Поэтому когда программа выводит на экран их значения, мы видим все ту же последовательность чисел 7, 10 и 15, хотя размещение соответствующих ячеек в памяти изменилось.
Массивы строковых указателей
Простейший способ управления массивом строк состоит в создании массива указателей на эти строки. Это гораздо проще, чем объявлять двухмерный массив символов. В следующей программе для отображения на экране трех разных сообщений об ошибках применяется массив указателей на строки.
/*
* arraystr.c
* Эта программа на языке С демонстрирует работу
* с массивом указателей на строки.
*/
#include <ctype.h>
#include <stdio.h>
#define INUMBER_OF_ERRORS 3
char *pszarray[INUMBER_OF_ERRORS] =
{
"\nфайл недоступен.\n",
"\nВведенный символ не является алфавитно-цифровым.\n",
"\nЧисло не попадает в диапазон от 1 до 10.\n"
};
FILE *fopen_a_file (char *psz) char cget_a_char (void) ; int ,iget_an_integer (void) ; FILE *pfa_file;
void main () {
char cvalue;
int ivalue;
fopen_a_file("input.dat"); cvalue = cget_a_char() ; ivalue = iget_an_ihteger() ,
}
FILE *fopen_a_file(char *psz)
(
const ifopen_a_file_error = 0;
pfa_file = fopen(psz, "r");
if (!pfa_file)
printf("%s",pszarray [ifopen_a_file_error] );
return (pfa_f ile );
}
char cget_a_char(void) {
char cvalue;
const .icget_a_char_error = 1;
printf("\nВведитебукву: ");
scanf("%c",&cvalue);
if(!isalpha (cvalue))
printf("%s",pszarray[icget_a_char_error]);
return(cvalue); }
int iget_an_integer (void) {
int ivalue;
const iiget_an_integer =2;
printf("\nВведите целое число в диапазоне от 1 до 10: ");
scanf("%d", sivalue) ;
if ((ivalue< 1) I | (ivalue > 10) )
printf("%s", pszarray [iiget_an_integer] );
return (ivalue) ; }
Массив pszarray объявлен вне функций, поэтому является глобальным. Обратите внимание, что в каждой из трех функций — fopen_a_file() , cget_a_char () и iget_an_integer() — используется свой индекс для обращения к массиву. Подобный подход позволяет организовать весьма гибкую и надежную систему сообщений об ошибках, так как все такие сообщения локализованы в одном месте и ими легче управлять.
Ссылки в языке C++
Язык C++ предоставляет возможность прямого обращения к переменным по адресам, что даже проще, чем использовать указатели. Аналогично языку С, в C++ допускается создание как обычных переменных, так и указателей. В первом случае резервируется область памяти для непосредственного сохранения данных, тогда как во втором случае в памяти компьютера выделяется место для хранения адреса некоторого объекта, который может быть создан в любое время. В дополнение к этому язык C++ предлагает третий вид объявления переменных — ссылки. Как и указатели, ссылки содержат данные о размещении других переменных, но для получения этих данных не нужно использовать специальный оператор косвенного обращения. Синтаксис использования ссылок в программе довольно прост:
int iresult_a = 5;
int& riresult_a = iresult_a; // допустимое создание ссылки
int$ riresult_b; // недопустимо, поскольку нет инициализации
В данном примере создается ссылка (на это указывает символ & после имени типа данных) riresult_a, которой присваивается адрес существующей переменной iresult_a. С этого момента на одну и ту же ячейку памяти ссылаются две переменные: iresult_aи riresult_a. По сути, это два имени одной и той же переменной. Любое присвоение значения переменной riresult_a немедленно отразится на содержимом переменной iresult_a. Справедливо и обратное утверждение: любое изменение переменной iresult_a вызовет изменение значения переменной riresult_a. Таким образом, можно сказать, что использование ссылок в языке C++ позволяет реализовать систему псевдонимов переменных.
В отличие от указателей, на ссылки накладывается следующее
ограничение: их инициализация должна производиться непосредственно в момент
объявления, и однажды присвоенное значение не может быть изменено в ходе выполнения
программы. То есть если при объявлении ссылки вы присвоили ей адрес какой-либо
переменной, то эта ссылка на протяжении всего времени работы программы будет
связана только с этой ячейкой памяти. Вы можете свободно изменять значение переменной,
но адрес ячейки, указанный в ссылке, изменить невозможно. Таким образом, ссылку
можно представить как указатель с константным значением адреса.
Рассмотрим некоторые примеры. В следующей строке значение, присвоенное выше
переменной iresult_a(в нашем примере — 5), будет удвоено:
riresult_a *= 2;
Теперь присвоим новой переменной icopy_value(предположим, что она имеет тип int) значение переменной iresult_a, используя для этого ссылку:
icopy_value = riresult_a;
Следующее выражение также является допустимым:
int *piresult_a = &riresult_a;
Здесь адрес ссылки riresult_a (т.е., посути, адрес переменной iresult_a) присваивается указателю piresult_a типа int.
Функции, возвращающие адреса
Когда функция возвращает указатель или ссылку, результатом выполнения функции становится адрес в памяти компьютера. Пользователь может прочитать значение, сохраненное по этому адресу, и даже, если указатель не был объявлен со спецификатором const, записать туда свои данные. Это может привести к возникновению трудно обнаруживаемых ошибок в программах. Посмотрим, сможете ли вы разобраться в том, что происходит в следующей программе.
//
// refvar.cpp
// Эта программа на языке C++ демонстрирует,
// чего НЕ нужно делать с адресами переменных.
//
#include <iostream.h>
int *ifirst_function(void) ;
int *isecond_function(void);
void main(){
int *pi = ifirst_function ();
isecond_function();
cout<< "Это значение правильное? " << *pi;
int *ifirst_function(void){
int ilocal_to_first = 11;
return &ilocal_to_first;
}
int *isecond_function(void){
int ilocal_to_second = 44;
return &ilocal_to_second;}
Запустив программу, вы убедитесь, что она выводит значение 44, а не 11. Как такое может быть?
При вызове функции ifirst_function() в программном стеке резервируется место для локальной переменной ilocal_to_first и по соответствующему адресу записывается значение 11. После этого функция ifirst_function() возвращает адрес этой локальной переменной в основную программу (сам по себе малоприятный факт — компилятор выдает по этому поводу лишь предупреждение, но, увы, не ошибку). В следующей строке программы вызывается функция isecpnd_function(), которая, в свою очередь, резервирует место для переменной ilocal_ to_second и присваивает ей значение 44. Почему же оператор cout выводит на экран значение 44, если ему был передан адрес переменной ilocal__to_first?
А произошло вот что. Указатель pi получил адрес ячейки, в которой хранилось значение локальной переменной ilocal__to_first, присвоенное ей во время выполнения функции ifirst_function (). Указатель pi сохранил этот адрес даже после того, как переменная ilocal_to_first вышла за пределы своей области видимости по завершении функции. При этом ячейка памяти, которую занимала переменная ilocal_to_firstи адрес которой записан в указателе pi, оказалась свободной, и ее тут же использовала функция isecond_function () для сохранения своей локальной переменной ilocal_to_second. Но поскольку адрес ячейки в указателе piне изменился, то на экран будет выведено значение переменной ilocal_to_second, а не ilocal_to_first. Мораль такова: нельзя допускать, чтобы функции возвращали адреса локальных переменных.