Управление памятью в C++


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

Краткое введение в управление памятью

Управление памятью контролирует то, как программа использует память компьютера. Во время выполнения каждая компьютерная программа использует оперативную память (то есть ОЗУ) для хранения временных переменных, структур данных и т. д. Управление использованием памяти включает в себя как выделение, так и освобождение памяти. Выделение памяти — это выделение части оперативной памяти по запросу программы. Освобождение памяти — это освобождение памяти, которая больше не нужна программе.

Язык программирования может использовать один из двух подходов к управлению памятью:

  • Автоматическое управление памятью (например, в Java, Python, C#)
  • Динамическое управление памятью (например, в C++, C)

C++ поддерживает динамическое управление памятью, то есть вы, как программист, отвечаете за выделение и освобождение памяти. С другой стороны, автоматическое управление памятью означает, что язык программирования автоматизирует этот процесс, выделяя и освобождая память за вас.

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

Несмотря на то, что это приводит к увеличению времени разработки, динамическое управление памятью позволяет адаптировать потребление памяти приложением и создавать высокопроизводительные приложения. Динамическое управление памятью — единственный возможный вариант, если вы работаете с устройствами с ограниченными ресурсами, например со встроенными устройствами. Оно также важно для поддержания высокой производительности в системах реального времени, поэтому C++ часто используется в разработке игр. Таким образом, язык C++ становится отличным выбором для ситуаций, когда важны производительность и небольшой объём используемой памяти.

Vногие люди до сих пор не решаются изучать динамическое управление памятью в C++. Помимо сложности освоения, существует реальный риск использования неправильных методов, которые могут привести к таким ошибкам, как утечка памяти (о чём мы вскоре поговорим). В некоторых случаях ошибки могут привести к ещё более серьёзным последствиям. Однако не стоит избегать изучения этого ценного навыка. В языке C++ реализовано несколько защитных механизмов и мер безопасности, которые помогают снизить риски, связанные с манипулированием аппаратным обеспечением. Если у вас будет достаточно практики, вы научитесь безопасно использовать управление памятью для прямого взаимодействия с аппаратным обеспечением компьютера и создания высокопроизводительных приложений.

Начало работы с управлением памятью в C++

Основы модели памяти C++

Каждое слово (или блок) памяти обычно состоит из двух, четырёх или восьми байт в зависимости от аппаратной архитектуры. В нашей программе на C++ мы можем обратиться к блоку по его числовому адресу. Адрес первого блока равен 0, а адрес последнего блока зависит от объёма памяти вашего компьютера. На рисунке ниже изображён блок памяти.

 

 

В C++ мы можем разделить память программы на три части:

  1. Статическая область, в которой хранятся статические переменные. Статические переменные — это переменные, которые используются на протяжении всего выполнения программы. Размер статической области не меняется во время выполнения программы на C++.

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

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

 

 

Мы сосредоточимся на выделении и освобождении памяти в куче.

В C++ блок памяти — это непрерывный массив байтов, где каждый байт имеет уникальный адрес.

Мы можем управлять памятью в C++ с помощью двух операторов:

  • new оператор для выделения блока памяти в куче
  • delete оператор для освобождения блока памяти из кучи

В следующем примере кода мы используем два наших оператора для выделения и освобождения памяти:

 

 

Теперь давайте разберёмся, что происходит в предыдущем коде:

  1. new Оператор резервирует место в памяти, где может храниться целое число C++ (то есть 4 байта). Затем он возвращает адрес вновь выделенной памяти.
  1. Мы создаём указатель ptr для хранения адреса памяти, возвращаемого оператором new .

 

 

  1. Мы сохраняем целочисленное значение по вновь выделенному адресу памяти с помощью *ptr=5.

 

 

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

 

 

Безопасное использование управления памятью в C++

Использование операторов new и delete требует некоторой осторожности. Они могут привести к ошибкам в работе с памятью. Однако мы можем использовать умные указатели, которые помогут нам более безопасно управлять памятью.

Распространённые ошибки при управлении памятью

При динамическом управлении памятью можно столкнуться с двумя распространёнными ошибками в коде: утечкой памяти и ошибкой сегментации.

Утечки памяти возникают, когда память не освобождается даже после того, как она больше не нужна. Это может привести к тому, что программе не хватит доступной памяти.

 

 

В этом примере кода функция memLeak() выделяет память, но эта память не освобождается. После возврата из функции выделенная память продолжает использоваться, даже если к ней нет доступа.

Ошибка сегментации — ещё одна известная ошибка, связанная с динамическим управлением памятью. Эта ошибка возникает, когда программа обращается к ячейке памяти, которая ей не выделена и не находится в адресном пространстве программы. Адресное пространство — это область памяти, в которой программе разрешено выделять память.

Следующая программа генерирует ошибку сегментации, как только заканчивается адресное пространство:

 

 

Обратите внимание, что ptr++ увеличивает адрес, хранящийся в указателе. Поскольку цикл while выполняется непрерывно, вскоре указатель будет указывать на область за пределами адресного пространства программы, что приведёт к ошибке сегментации. Чтобы избежать ошибок сегментации, мы должны убедиться, что программа не обращается к области памяти, которая ей не выделена.

 

Предотвращение ошибок с помощью умных указателей

 

В C++ предусмотрены различные типы интеллектуальных указателей. Мы называем эти указатели «интеллектуальными», потому что они автоматически освобождаются без явных указаний программиста или сборщика мусора. Хотя интеллектуальные указатели требуют больше ресурсов для работы и занимают больше памяти, чем классические указатели, они помогают уменьшить количество утечек памяти.

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

Уникальные указатели

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

В качестве примера рассмотрим следующий код, в котором показан уникальный указатель на объект MyClass, созданный в блоке if. Таким образом, областью видимости указателя является блок if. Указатель автоматически освобождается в конце блока if.

 

 

Как следует из их названия, уникальные указатели нельзя копировать. При копировании указателей создаётся несколько указателей на один и тот же объект. Когда любой из этих указателей выходит за пределы области видимости, объект удаляется. Оставшиеся указатели перестают указывать на действительный объект (мы называем их висячими указателями).

На следующем рисунке std::move переключает владение объектом с одного указателя на другой.

 

 

Вместо копирования мы можем использовать функцию std::move для безопасной передачи права владения текущим указателем другомуstd::move показано на предыдущем рисунке. Это можно понять из следующего кода, в котором мы передали право владения объектом MyClass от указателя ptr к ptr2. Будьте осторожны: чтобы избежать ошибки сегментации, предыдущий указатель нельзя использовать после передачи права владения.

В следующем коде показано, как безопасно передать право собственности с помощью функции std::move:

 

 

Общие указатели

Общий указатель std::shared_ptr использует подсчёт ссылок для освобождения памяти. В отличие от уникальных указателей, общий указатель позволяет нескольким указателям указывать на один и тот же объект. Общий указатель ведёт подсчёт каждого указателя, который всё ещё находится в области видимости. Когда указатель выходит из области видимости, счётчик уменьшается. Таким образом, объект автоматически удаляется, когда счётчик ссылок достигает нуля.

На следующем рисунке показан общий указатель с количеством ссылок, равным двум:

 

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

Ограничения интеллектуальных указателей

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

Следующий код удаляет смарт-указатель вручную:

 

 

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

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

 

 

Используйте все возможности C++ с помощью управления памятью. Хотя на обучение этому навыку требуется некоторое время, управление памятью в C++ — особенно ценный навык для программиста, особенно в свете того, что мы продолжаем двигаться в сторону распределённых систем.



Вернуться назад