, : for2, : Top


29 Указатели и динамическая память

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

Создание указателей

Указатель — это переменная специального типа. Она хранит не какое-то числовое значение, а адрес (номер первого байта в памяти компьютера), по которому хранится какая-то другая переменная. При создании указателя необходимо задать тип переменной, на которую он указывает. Синтаксис объявления указателя такой:

     имя_типа * идентификатор;

Пример:

     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 без указания элемента массива, как синоним для указателя на начало массива. С массивами можно выполнять все операции, которые можно делать с указателями, кроме тех, которые меняют значение самого указателя: =, +=, -=, ++. --, то есть единственное отличие массивов от указателей заключается в том, что значение указателя можно изменить в программе, а значение массива (то есть адрес его начала) — нет.

Упражнения

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

  1. Выведите на экран значение самого указателя (а не переменной, на которую он указывает).
  2. Определите с точностью до 1 мегабайта, какой объем динамической памяти может выделить программа. Запустите эту программу на различных компьютерах, желательно на различных операционных системах.
  3. Выведите те элементы заданного пользователем массива, которые больше среднего арифметического всех его элементов.
  4. Реализуйте алгоритм сортировки массива, размер которого не задан в явном виде (ни на момент компиляции, ни при запуске программы). Программа должна считывать данные со стандартного ввода до тех пор, пока данные не кончатся (метод cin.eof() не вернет true), после чего программа должна отсортировать все полученные данные. Подсказка: необходимо создать массив некоторого заранее определенного размера. когда количество считанных элементов массива превысит размер созданного массива, надо создать новый массив и скопировать данные в него и т.д.
  5. По данному массиву найдите число различных элементов в нем.
  6. Задача Иосифа Флавия. n человек стоят по кругу. Начиная с некоторой позиции, они ведут отсчет по кругу и предают жестокой казни каждого k-го, после каждой казни круг смыкается. Определите последовательность казни участников.
  7. По двум данным многочленам P и Q определите их частное и остаток от деления.