: functions2, : for2, : Top
Часто размер используемого при решении задачи массива не задан на момент компиляции программы, а определить его можно только при выполнении программы. В этом случае используется механизм, называемый динамическим распределением памяти. Для использования динамического распределения памяти необходимо создать переменную специального типа, называемую указателем, затем создать в памяти массив необходимого размера, доступ к которому будет осуществляться через этот указатель, наконец, освободить выделенную память после завершения работы с массивом.
Указатель — это переменная специального типа. Она хранит не какое-то числовое значение, а адрес (номер первого байта в памяти компьютера), по которому хранится какая-то другая переменная. При создании указателя необходимо задать тип переменной, на которую он указывает. Синтаксис объявления указателя такой:
имя_типа * идентификатор;
Пример:
int * pi; float * pf, f; double * ps, * pt;
В первой строке этого примера объявлены переменная pi
, являющейся указателем на
тип int
(то есть в ячейке памяти, на которую указывает pi
должна хранится
переменная типа int
). Во второй строке объявлены переменная pf
, являющейся указателем
на тип float
и переменная f
типа float
. Обратите особое внимание на эту строчку:
для того, чтобы объявить несколько указателей в одной строке, необходимо перед идентификатором каждого из них
поставить символ *
. А еще лучше объявлять в одной строке только одну переменную.
В третей строке объявляется два указателя на тип double
: ps
и pt
.
Сразу после объявления значение указателя не определено, то есть он может указывать в
произвольную ячейку памяти, поэтому пользоваться им нельзя. Иначе вы в лучшем случае
получите ошибку segmentation fault
, в худшем — программа будет
работать непредсказуемо.
Для того, чтобы создать в памяти новую переменную используется оператор new
.
Это унарный оператор, единственный операнд которого должен быть именем типа создаваемой переменной.
Оператор new
возвращает адрес вновь созданной переменной, который можно присвоить указателю. Например:
int * pi; // Объявление переменной pi pi=new int; // Выделение памяти для переменной pi
Для доступа к ячейке памяти, адрес которой хранится в указателе, необходимо использовать
унарный оператор разыменования, записываемый символом *
.
Пример:
*pi=5; // *pi - разыменование указателя pi cout<<(*pi); // Будет напечатано 5
В этом примере ячейке, на которую указывает pi
сначала присваивается число
5
, а потом значение этой ячейки памяти выводится на экран. Присвоить числовое
значение непосредственно указателю (pi=5
) нельзя!
Также указателю можно присвоить адрес уже существующей переменной, для
этого к переменной нужно применить унарный оператор взятия адреса &
:
int a, b, *p; // a и b имеют тип int, p - указатель на int a=3; p=&a; // Теперь p указывает на a cout<<*p; // Будет выведено 3 p=&b; // Теперь p указывает на b *p=7; // В результате будет изменено значение b cout<<b; // Будет выведено 7
new
Для создания массива в динамической памяти используется оператор
new
с указанием размера создаваемого массива. Пример:
int * pi; // Объявить указатель pa pi=new int[n]; // Создать массив из n элементов типа int
Теперь с pi
можно работать, как с обычным массивом из n
элементов
типа int
: становятся доступны элементы pi[0]
, pi[1]
, ...,
pi[n-1]
.
new
Существует единственное числовое значение, которое можно присвоить непосредственно
указателю: это 0
(то есть присваивание pi=0
разрешено, а присваивание
pi=1
— нет). Нулевой адрес — особый, по этому адресу не может
хранится ни одна переменная. То есть указатель, имеющий нулевое значение указывает
в "никуда", к такому указателю нельзя применить оператор разыменования.
Оператор new
использует функцию операционной системы для выделения памяти.
Если затребованный размер памяти слишком большой (а также при попытке создать
массив из нуля или отрицательного числа элементов), операционная система
не будет выделять память и оператор new
вернет нулевое значение.
Если это нулевое значение будет присвоено указателю, к которому впоследствии
будет применен оператор разыменования или оператор обращения к элементу массива,
то программа аварийно завершит работу с ошибкой segmentation fault
.
Чтобы быть уверенным, что оператор new
был выполнен удачно, необходимо
сразу после его вызова проверить значение, которое он вернул и в случае, если
оно равно 0
, выполнить какие-либо особенные действия, например,
вывести сообщение о невозможности выделения необходимого объема памяти.
Например:
int n=1000000000; int *pi=new int[n]; if(pi==0) { cout<<"Невозможно создать массив из "<<n<<" элементов int"; return 1; // Завершаем работу функции main }
После окончания работы с массивом, когда выделенная ранее память перестанет
быть нужной, ее необходимо освободить, чтобы дать возможность операционной
системе использовать эту память по своему усмотрению, например, выделить
другой программе. Для этого используется унарный оператор delete
,
единственный операнд которого — адрес, по которому начинается память,
ранее выделенная оператором new
, которую мы хотим освободить. Например:
delete pi;
При этом сам указатель pi
не уничтожается, ему можно присвоить новое значение,
однако разыменование этого указателя может привести к ошибке.
Теперь объединим все вышесказанное в одну программу, создающую в динамической памяти
массив типа double
, заполняющую его данными и уничтожающую в конце.
int n; // Размер массива double * p; // Указатель на начало массива cout<<"Введите размер массива: "; cin>>n; p=new double[n]; // Выделяем память if(p==0) // Проверка успешности выделения памяти { cout<<"Невозможно выделить память"<<endl; return 1; // Завершаем работу } for(int i=0;i<n;++i) // Цикл для считывания массива cin>>p[i]; // Считали i-й элемент массива //// Теперь выполняем что-нибудь с массивом delete p; // Освобождаем память
Указателям можно присваивать значение, являющееся указателем того же типа
(которое может быть результатом оператора new
, оператора &
или
другим указателем того же типа). К указателям можно применять оператор разыменования
*
. Кроме этого с указателями можно выполнять ряд других операций.
Далее мы предполагаем, что p
и q
— указатели одного типа, например,
объявленные как int *p, *q
.
Как и числа, указатели можно сравнивать между собой.
p==q
p!=q
p<q
true
, если ячейка, на которую указывает p
находится в памяти раньше,
чем ячейка, на которую указывает q
. Аналогично определяются сравнения
p<=q
, p>q
, p>=q
Как всегда, нельзя путать операторы проверки на равенство p==q
и присваивания
p=q
. В результаты выполнения оператора присваивания p
будет
указывать туда же, куда и q
, значения же ячеек памяти, на которые указывали
p
и q
не изменятся.
К указателям можно прибавлять и вычитать целые числа.
Пусть p
указывает на начало массива. Тогда
p+0
равно p
, p+1
—
это указатель на следующий элемент массива, то есть &p[1]
,
и для любого положительного i
p+i
— это указатель на
p[i]
. Вычитание из указателя 1 (а также прибавление к указателю
-1
) возвращает указатель на переменную, которую можно разместить
в памяти непосредственно перед переменной, на которую указывает p
.
Если p
указывает не на начало массива, а на произвольную
ячейку памяти, к значению p
можно прибавлять целые числа, при этом
результат будет такой же, как описано выше.
Также к указателям можно применять оператор доступа к элементам массива []
:
p[0]
при этом означает переменную, хранящуюся в ячейке памяти, на которую
указывает p
, p[1]
— следующую за ней, а p[-1]
—
предшествующую ей и т.д.
Поскольку определены операторы сложения и вычитания указателя с целыми числами, то
указателям можно применять операции инкремента ++
, декремента --
,
а также увеличивать (+=
) и уменьшать -=
значения указателей
на целые значения.
Наконец, два указателя одного типа можно вычитать друг из друга. В этом случае
оператор вычитания p-q
возвращает целое число, при прибавлении
которого к указателю q
получается указатель p
.
Никакие другие операторы к указателям применять нельзя. В частности, указатели нельзя складывать, умножать, умножать указатель на число и т.д.
Итак, с указателем можно работать, как с массивом, в частности, к нему можно применять
оператор доступа к элементу []
. Верно и обратное — с массивом можно работать,
как с указателем. Если мы объявили массив int arr[10]
, то мы можем использовать
идентификатор arr
без указания элемента массива, как синоним для указателя на
начало массива. С массивами можно выполнять все операции, которые можно делать с указателями,
кроме тех, которые меняют значение самого указателя: =
, +=
, -=
, ++
. --
,
то есть единственное отличие массивов от указателей заключается в том, что значение указателя
можно изменить в программе, а значение массива (то есть адрес его начала) — нет.
Во всех упражнениях, если это особо не оговорено, предполагается, что размер массива задается пользователем при запуске программы.
cin.eof()
не вернет true
), после чего программа
должна отсортировать все полученные данные. Подсказка:
необходимо создать массив некоторого заранее определенного размера.
когда количество считанных элементов массива превысит размер
созданного массива, надо создать новый массив и скопировать данные в него
и т.д.