появляется еще одно имя – "*ptr_a". Часто в программах бывает удобно пользоваться
переменными, у которых есть только такие имена –динамическими переменными. Не-
зависимых имен у них нет. К динамическим переменным можно обращаться только
через указатели с помощью операции разыменования (например, "*ptr_a" и "*ptr_b").
Динамические переменные "создаются" с помощью оператора распределения
динамической памяти "new", а "уничтожаются" (т.е. занимаемая ими память освобож-
77
дается для дальнейшего использования) с помощью оператора "delete". Действие
этих операторов показано в программе 1.2 (она очень похожа на программу 1.1).
#include <iostream.h>
typedef int* IntPtrType;
int main()
{
IntPtrType ptr_a, ptr_b; /* СТРОКА 7 */
ptr_a = new int; /* СТРОКА 9 */
*ptr_a = 4; /* СТРОКА 10 */
ptr_b = ptr_a; /* СТРОКА 11 */
cout << *ptr_a << " " << *ptr_b << "\n";
ptr_b = new int; /* СТРОКА 15 */
*ptr_b = 7; /* СТРОКА 16 */
cout << *ptr_a << " " << *ptr_b << "\n";
delete ptr_a;
ptr_a = ptr_b; /* СТРОКА 21 */
cout << *ptr_a << " " << *ptr_b << "\n";
delete ptr_a; /* СТРОКА 25 */
return 0;
}
Программа 1.2.
Программа 1.2 печатает на экране следующие сообщения:
4 4
4 7
7 7
На рисунках 4-8 показаны состояния программы 1.2 после выполнения 7-й, 9-
11-й, 15-16-й, 21-й и 25-й строк.
Рис. 4.. Состояние про-
граммы 1.2 после выпол-
нения 7-й строки с описа-
ниями переменных.
Рис. 5.. Программа 1.2
после выполнения опера-
торов присваивания в 9-й,
10-й и 11-й строках.
Рис. 6.. Программа 1.2
после выполнения опера-
торов присваивания в 15-
й и 16-й строках.
78
Рис. 7.. Программа 1.2 после
выполнения оператора присваи-
вания в 21-й строке.
Рис. 8.. Программа 1.2 после вы-
полнения оператора "delete" в
25-й строке.
Состояния на рис. 4 и рис. 8 похожи тем, что значения указателей "ptr_a" и
"ptr_b" не определены, т.е. они указывают на несуществующие объекты. Обратите
внимание, что указатель "ptr_b" в конце программы оказывается в неопределенном
состоянии, хотя при вызове оператора "delete" этот указатель явно не передавался.
Если указатель "ptr" указывает на несуществующий объект, то использование
в выражениях значения "*ptr" может привести в непредсказуемым (и часто катастро-
фическим) результатам. К сожалению, в Си++ нет встроенного механизма проверки
несуществующих указателей. Программист может сделать свои программы более
безопасными, если всегда будет стараться присваивать несуществующим указателям
нулевой адрес (символическое имя нулевого адреса – "NULL"). Для хранения перемен-
ных в Си++ нулевой адрес не используется.
Константа "NULL" (целое число 0) описана в библиотечном заголовочном файле
"stddef.h". Значение "NULL" можно присвоить любому указателю. Например, в про-
грамме 1.3, являющемся усовершенствованным вариантом программы 1.2, для защи-
ты от использования неопределенных указателей "*ptr_a" и "*ptr_b" были добавлены
следующие строки:
#include <iostream.h>
#include <stddef.h>
...
...
delete ptr_a;
ptr_a = NULL;
delete ptr_b;
ptr_b = NULL;
...
...
if ( ptr_a != NULL )
{
*ptr_a = ...
...
...
Фрагмент программы 1.3.
В случае, если для создания динамической переменной не хватает свободной
оперативной памяти, то после вызова "new" Си++ автоматически присвоит соответст-
вующему указателю значение "NULL". В показанном ниже фрагменте программы 1.4
этот факт используется для организации типичной проверки успешного создания ди-
намической переменной.
#include <iostream.h>
#include <stdlib.h> /* "exit()" описана в файле stdlib.h */
#include <stddef.h>
...
...
79
ptr_a = new int;
if ( ptr_a == NULL )
{
cout << "Извините, недостаточно оперативной памяти";
exit(1);
}
...
...
Фрагмент программы 1.4.
Указатели можно передавать в качестве параметров функций. В программе 1.5
проверка на корректность указателя выполняется в отдельной функции, выполняю-
щей создание динамической целочисленной переменной:
void assign_new_int( IntPtrType& ptr )
{
ptr = new int;
if ( ptr == NULL )
{
cout << "Извините, недостаточно оперативной памяти";
exit(1);
}
}
Фрагмент программы 1.5.
2. Переменные типа "массив". Арифметические операции с указателями
В 6-й лекции были рассмотрены массивы –наборы однотипных переменных. В
Си++ понятия массива и указателя тесно связаны. Рассмотрим оператор описания:
int hours[6];
Этот массив состоит из 6-ти элементов:
hours[0] hours[1] hours[2] hours[3] hours[4] hours[5]
Массивы в Си++ реализованы так, как будто имя массива (например, "hours")
является указателем. Поэтому, если добавить в программу объявление целочисленно-
го указателя:
int* ptr;
то ему можно присвоить адрес массива (т.е. адрес первого элемента массива):
ptr = hours;
После выполнения этого оператора обе переменные – "ptr" и "hours" –будут
указывать на целочисленную переменную, доступную в программе как "hours[0]".
Фактически, имена "hours[0]", "*hours" и "*ptr" являются тремя различными
именами одной и той же переменной. У переменных "hours[1]", "hours[2]" и т.д.
также появляются новые имена:
*(hours + 1) *(hours + 2) ...
или
*(ptr + 1) *(ptr + 2) ...
В данном случае "+2" означает "добавить к адресу указателя смещение, соот-
ветствующее 2-м целым значениям". Из арифметических операций к указателям часто
применяется сложение и вычитание (в том числе операции инкремента и декремента
80
"++" и "--"), а умножение и деление не используются. Значения однотипных указате-
лей можно вычитать друг из друга.
Главное, что нужно запомнить относительно сложения и вычитания значений
из указателя –в выражениях Си++ указывается не число, которое нужно вычесть (или
добавить) из адреса, а количество переменных заданного типа, на которые нужно
"сместить" адрес.
Арифметические выражения с указателями иногда позволяют более кратко за-
писать обработку массивов. В качестве примера см. функцию для преобразования
английской строки в верхний регистр (фрагмент программы 2.1).
void ChangeToUpperCase( char phrase[] )
{
int index = 0;
while ( phrase[index] != '\0' )
{
if ( LowerCase(phrase[index]) )
ChangeToUpperCase( phrase[index] );
index++;
}
}
bool LowerCase( char character )
{
return ( character >= 'a' && character <= 'z');
}
void ChangeToUpperCase( char& character )
{
character += 'A' - 'a';
}
Фрагмент программы 2.1.
Обратите внимание на полиморфизм функции "ChangeToUpperCase(...)" –при
обработке вызова компилятор различает две перегруженных функции, т.к. у них раз-
ные параметры (у одной –параметр типа "char", а у другой –параметр типа "сим-
вольный массив"). Имя массива "phrase" является переменной-указателем, поэтому
функцию с параметром-массивом можно переписать короче, если использовать
арифметические выражения с указателями:
void ChangeToUpperCase( char* phrase )
{
while ( *phrase != '\0' )
{
if ( LowerCase(*phrase) )
ChangeToUpperCase(*phrase);
phrase++;
}
}
Фрагмент программы 2.2.
Эта модификация функции не влияет на остальные части программы –вызовы
вариантов функций с параметром-указателем или параметром-массивом записывают-
ся одинаково, например:
81
char a_string[] = "Hello World";