АЙТИШНЫЙ САЙТ ПЕТРА СЕМИЛЕТОВА

ПЕТР СЕМИЛЕТОВ

ПРОСТО С++: УРОК 11

Указатели, работа с памятью

Мы подобрались к очень мощной особенности программирования в С++ - указателям, без умения работы с которыми нельзя продвигаться далее.

Однако заговорю сначала о другом. Выделение памяти для переменных. Когда мы объявляем переменную, ей выделяется определенная область оперативной памяти. И есть несколько способов такого выделения.

До сих пор мы сталкивались только с автоматическим выделением, когда переменная существует лишь в пределах ее области видимости, то есть между фигурных скобок - иначе говоря, в теле функции или составного оператора.

int f()
{
  int x =13;
  return x;
}

В этом примере мы объявляем функцию f, в теле которой объявляем переменную x. Эта х видна только внутри функции. Функция возвращает значение x, равное 13, а не саму переменную. Только ее значение. По выходе из функции переменная х, условно говоря, погибает.

Что происходит, когда мы вызываем функцию f? Вызываем например вот так

cout << f() << endl;

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

На английском это stack, что переводится как стопка чего-либо. То, что для программиста на С++ выглядит как имя переменной и ее значение, на языке ассемблера выглядит как команда, помещающая значение, число, в этот стек, стопку, по такому-то смещению с нем.

Это как бросать на тарелку жареные блины. Каждый блин - переменная, но блины бывают разной толщины, в зависимости от типа переменной. Например int - блин толщиной в 4 байта, char - блин толщиной 1 байт. Такие блины называются кадрами стека.

Всё это при программировании на С++ скрыто от программиста, возня с блинами - то более низкий уровень. А С++, в отличие от ассемблера - язык высокого уровня.

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

Итак, при вызове функции ей автоматически дается этот стек, структурированная область памяти, и после завершения работы функции стек освобождается.

Но где, в чем дается стек? В другой, бОльшей области памяти, которая называется кучей - heap. Боже, то блины, то куча…

Но погодите… Прежде чем перейти к куче, надо сказать еще об одном виде переменных. Пока что вы знаете о тех, память под которые выделяется автоматически в стеке.

Есть еще статичные переменные, им отводится память в сегменте данных программы. Когда программа, чтобы быть выполненной, всасывается в память, то она разделена на несколько участков - сегментов - в частности кода (где алгоритмы), и сегмент данных (с переменными).

Статичные переменные сохраняют свои значения на протяжении всей работы программы. Статичные переменные инициализируются (т.е. им присваивают значение) только один раз, при первом вызове функции, где они объявлены. Для объявления такой переменной ее надо предварить словом static.

Пример 11-01.cpp:

#include <iostream>

using namespace std;

int f()
{
  static int x = 1;
  return x++;
}

int main (int argc, char *argv[])
{
  cout << f() << endl;
  cout << f() << endl;
  cout << f() << endl;
  
  return 0;
}

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

В функции main мы трижды вызываем функцию f. При запуске программы она выведет:

1
2
3

Итак, при каждом вызове функции f, она будет возвращать x и увеличивать значение этой переменной на 1. То есть значение х между вызовами функции f сохраняется. Если бы мы не написали static, то переменная x размещалась бы в стеке, и значение ее исчезало бы между вызовами функции. Что при этом выдала бы программа, оставляю вам проверить самим, коли есть любопытство.

Поговорим теперь о куче, о heap!

Зачем она?

В прошлой серии шла речь о массивах. Они были объявлены как обычным способом, следовательно, размещались в стеке.

Кстати, функции не могут возвращать такие обычные массивы. Но об этом чуть позже, к функциям мы еще вернемся.

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

Выделим в куче память под массив с именем x, размером в 12 элементов типа int:

int *x = new int[12];

Итак, мы объявляем массив x необычным образом. Перед его именем мы ставим звездочку. После имени переменной массива мы пишем знак равенства, и используем оператор new, после которого указываем тип элементов, содержащихся в массиве, и далее в квадратных скобках передаем размер массива.

Оператор new служит для выделения памяти в куче, в указанном нами объеме, надо лишь написать тип и в квадратных скобках под сколько ячеек массива мы хотим получить память.

Звездочка означает, что мы имеем дело с указателем. Указатель (pointer) - такая особая переменная, которая содержит в себе адрес первой ячейки некой области памяти. Это может быть как целый массив, так и отдельная переменная. Но об этом позже.

Пример 11-02.cpp:

#include <iostream>

using namespace std;

int main (int argc, char *argv[])
{
  int *x = new int [12];
  x[0] = 8;
  cout << x[0] << endl;
  delete [] x; 

  return 0;
}

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

И - новое слово - delete, удалить, после чего квадратные скобки и имя массива. Оператор delete удаляет массив из памяти, а квадратные скобки говорят оператору, что мы удаляем именно массив.

Если этого не сделать, то при выходе из программы, память, занятая массивом, так и останется занятой до перезагрузки компьютера. Это называется утечкой памяти.

Но мы только начали изучать волшебные свойства указателей. Новый пример может показаться сейчас излишне сложным, но стоит того, чтобы в нем разобраться. Не сейчас, так потом.

Пример 11-03.cpp:

#include <iostream>

using namespace std;

int main (int argc, char *argv[])
{
  int *a = new int [12];

  *a = 8;
  cout << *a << endl;

  a++;
  *a = 9;
  cout << *a << endl;

  a--;

  delete [] a;

  return 0;
}

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

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

Когда нам нужно работать с адресом, на который указывает указатель, мы просто прибавляем или отнимаем число - количество элементов. Когда мы хотим получить значение, СОДЕРЖАЩЕЕСЯ в элементе, на который СЕЙЧАС указывает указатель, мы ставим перед указателем звездочку.

В нашем примере, сначала, когда а указывает на нулевой элемент, мы присваиваем ему значение 8 и выводим на консоль:

  *a = 8;
  cout << *a << endl;

Теперь увеличим адрес в указателе на единицу.

  a++;

После этого указатель указывает уже не на нулевой элемент массива, а на следующий, первый. Присвоим указателю значение 9.

  *a = 9;

Оно присвоилось текущему элементу, на который указывает указатель, первому. Проверим:

  cout << *a << endl;

Теперь перемотаем указатель на элемент назад, то есть вернем в исходное положение, на нулевой элемент:

  a--;

И удалим массив.

  delete [] a;

Если не вернуть указатель в исходное значение, то оператор delete освободит память, занятую массовом, начиная не с нулевого его элемента, а с того, куда мы перемотали, то есть с первого, и отчитает для освобождения 12 элементов, то есть попытается освободить память за выделенными для массива пределами, ибо указатель-то сдвинулся на один вперед. Это вызовет сбой программы, вылет.

Подобная работа с указателями называется арифметикой указателей, или адресной арифметикой. Она весьма опасна в смысле стабильности работы программы и ее уязвимости, некоторые языки программирования вроде Java не имеют механизма адресной арифметики вообще. Вместе с тем это мощное и эффективное средство в руках тех, кто умеет с ним работать.

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

Указатели имеют еще много применений, но к ним мы перейдем позже.

Поддержать курс:

Навигация:

Оглавление

Предыдущий урок

Следующий урок