ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 438
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Решение задач с указателями и динамической памятью 107
double * doublePointer = new double;
Доступ к содержимому памяти с помощью указателя называется операцией разыменования и осуществляется с помощью оператора
*
, который указывается слева от идентификатора указателя. То есть также как и при объявлении. А содержание операции определяется по контексту. Например:
X *doublePointer = 35.4;
Y double localDouble = *doublePointer;
Мы записали числовое значение типа double в участок памяти, который был выделен в предыдущем примере
X
, а потом присвоили содержимое этой памяти переменной localDouble
Y
Чтобы освободить память, выделенную при помощи оператора new
, когда в ней больше нет нужды, мы используем ключевое слово delete
:
delete doublePointer;
Этот процесс детально описан в разделе «Вопросы памяти» да- лее в этой главе.
Преимущества использования указателей
Использование указателей существенно расширяет возможности по сравнению с использованием статической памяти, а также позволя- ет использовать память более эффективно. Перечислим три основ- ных преимущества, возникающие при использовании указателей:
y
использование структур данных, размер которых определяет- ся во время выполнения программы;
y
использование динамических структур данных, которые мо- гут изменять свой размер во время выполнения программы;
yразделение памяти.
А теперь давайте рассмотрим каждое из них подробнее.
Структуры данных, размер которых определяется
во время выполнения программы
Применение указателей позволяет создавать массивы, размер которых определяется во время выполнения программы, вместо того, чтобы определять их размер при проектировании приложения. Это убере- гает нас от выбора между возможным переполнением массива и нера- циональным использованием памяти в случае создания максимально больших массивов. Мы уже рассматривали структуры данных, размер которых определяется во время выполнения программы в разделе
«В каких случаях использовать массивы». И вернемся к этому вопросу позже, в разделе «Строки переменной длины» далее в этой главе.
108 Глава 4
Динамические структуры
Также с помощью указателей можно создавать структуры данных, ко- торые при необходимости способны увеличиваться или уменьшать- ся во время выполнения программы. Связный список, который вы, скорее всего, уже встречали, — самый простой пример динамической структуры данных. Несмотря на исключительно последовательный доступ к данным, связный список всегда занимает ровно столько памя- ти, сколько требуется для хранения его данных и ни байтом больше.
Позже вы увидите, что более сложные структуры данных, основанные на указателях, используют упорядочивание и «формы», обеспечиваю- щие лучшее отражение связей данных, чем это реализовано в масси- вах. По этой причине, даже учитывая произвольный доступ к данным массива, который структуры, основанные на указателях, обеспечить не могут, операция поиска (когда мы ищем элемент, наилучшим обра- зом удовлетворяющий заданным критериям) может осуществляться намного быстрее в структурах, основанных на указателях. Позже в этой главе мы воспользуемся подобным преимуществом при создании структуры данных для хранения студенческих карточек, которая мо- жет увеличиваться по мере необходимости.
Разделение памяти
Указатели позволяют осуществлять совместный доступ к памяти, что повышает эффективность программного кода. Например, при вы- зове функции мы можем передать в нее указатель вместо того, что- бы копировать данные. Такой способ называется передачей по ссылке.
Скорее всего, вы уже сталкивались с этим раньше. Это те самые па- раметры, у которых символ & указан между типом и именем в списке параметров функции:
void refParamFunction (int
X& x) {
Yx = 10;
}
int number = 5;
refParamFunction(
Z number);
cout <<
[ number << "\n";
ПРИМЕЧАНИЕ.
Пробелы до и после символа & не обязательны, я поставил
их по эстетическим соображениям. В коде других разработчиков вы можете
встретить следующие формы записи: int& x, int &x и int&x.
В примере параметр x
X
является не копией аргумента number
Z
, а представляет собой ссылку на участок памяти, в котором хранится значение переменной number. Таким образом, при изменении пере- менной x
Y
изменяется значение в ячейки памяти переменной number и в итоге на экран будет выведено 10
[
. Передача по ссылке может также применяться и для возвращения результата из функции, как было показано в этом примере. В широком смысле передача по ссыл- ке позволяет вызванной и вызывающей функциям совместно исполь-
Решение задач с указателями и динамической памятью
1 ... 9 10 11 12 13 14 15 16 ... 34
109
зовать память, тем самым снижая накладные расходы. Если перемен- ная, передаваемая в качестве аргумента, занимает килобайт памяти, то при передаче по ссылке вместо килобайта копируется только 32- или 64-битный указатель. При помощи ключевого слова const мы мо- жем запретить функции изменять параметр, переданный по ссылке:
int anotherFunction(const int & x);
Ключевое слово const в объявлении ссылочного параметра x оз- начает, что функция anotherFunction получит ссылку на аргумент, переданный при вызове, но не сможет изменять значение этого ар- гумента, так как он является константой.
В общем, мы можем использовать указатели подобным образом для того, чтобы различные части программы или структуры данных, задействованные в программе, могли использовать одни и те же дан- ные без дополнительных затрат памяти.
В каких случаях использовать указатели
Как и массивы, указатели имеют свои недостатки и должны приме- няться только тогда, когда это действительно необходимо. Как по- нять, что применение указателя необходимо? Так как у нас есть толь- ко список возможностей, предоставляемых указателями, мы можем сказать, что их использование оправданно при возникновении по- требности в одной или нескольких из этих возможностей. Если ва- шей программе требуется структура данных, но вы не можете зара- нее определить объем этих данных. Если вам требуется структура, которая может увеличиваться или уменьшаться во время выполнения программы. Или вы собираетесь передавать объемные данные между частями программы, то можете применять указатели. Если же таких потребностей нет, то вам следует держаться подальше от указателей и динамического распределения памяти.
Так как указатели имеют печальную репутацию одной из са- мых сложных особенностей языка С++, вы можете предположить, что программисты стараются не применять их без лишней необхо- димости. Я не единожды удивлялся, обнаружив обратное. Иногда программисты обманывают сами себя, предполагая, что указатели необходимы. Предположим, вы вызываете чужую функцию из биб- лиотеки или интерфейса прикладного программирования со следу- ющим прототипом:
void compute(int input, int* output);
Мы можем предположить, что эта функция написана на язы- ке С, а не С++, и именно поэтому она использует указатель вместо ссылки (&) для создания «исходящего» параметра. При вызове дан- ной функции программист может небрежно сделать что-то вроде этого:
110 Глава 4
int num1 = 10;
int* num2 = new int;
compute(num1, num2);
Этот код неэффективно использует память , так как он создает лиш- ний указатель. Вместо памяти для двух переменных типа int, он захваты- вает место для двух переменных типа int и для указателя. Этот код так- же неэффективно использует время, так как выделение лишней памяти требует дополнительного времени (это мы рассмотрим в следующем разделе). И наконец, программист не должен забывать освобождать вы- деляемую память с помощью ключевого слова delete. Всего этого мож- но избежать, если использовать другой аспект оператора & , который по- зволяет получить адрес статической переменной, например:
int num1 = 10;
int num2;
compute(num1, &num2);
Строго говоря, мы все еще используем указатель во второй вер- сии, но делаем это косвенно, без объявления или динамического вы- деления памяти.
Вопросы памяти
Чтобы разобраться, каким образом работает динамическое распреде- ление памяти, нужно понимать механизм работы памяти в целом. Это одна из тех тем, ради которых начинающим программистам стоит из- учать язык С++. В конце концов, все программисты должны понимать механизм работы системы памяти в современных компьютерах, и язык
С++ ставит вас лицом к лицу с этой темой. Другие языки программиро- вания скрывают большинство неприглядных подробностей работы си- стемы памяти, поэтому начинающие программисты полагают, будто эти подробности их не касаются, что в корне неверно. Эти подробности ни- кого не касаются, только пока все работает. Как только возникает про- блема, дальнейшее пренебрежение основами работы памяти создает не- преодолимое препятствие между программистом и ее решением.
Стек и куча
Язык С++ выделяет оперативную память в двух местах: стеке и куче.
Как следует из названий, стек (от англ. stack – стопка) – аккуратный и организованный, а куча – бессвязная и беспорядочная. Название стоп-
ка очень меткое, так как помогает визуализировать устройство стека.
Представьте стопку коробок наподобие той, что показана на рис. 4.1
(а). Когда вам нужно отправить коробку на хранение, вы просто стави- те ее наверх стопки. Чтобы достать определенную коробку из стопки, вы сначала должны снять все коробки, стоящие сверху. Говоря языком программирования, однажды выделив блок памяти в стеке (коробку), вы уже не можете изменить его размер, так как следом расположены другие блоки памяти (коробки, стоящие сверху).
Решение задач с указателями и динамической памятью 111
Язык С++ позволяет создавать ваш собственный стек, работаю- щий по определенному алгоритму, но несмотря на это ваша програм- ма все равно всегда будет использовать так называемый стек вызовов .
Каждый раз при вызове функции (включая функцию main) будет вы- деляться блок памяти в голове стека. Такой блок называется запись ак-
тивации . Разговор обо всем его содержимом выходит за рамки нашей книги, но для решения задач нам будет достаточно знать, что запись активации является местом хранения переменных. Память для всех локальных переменных, включая параметры функции, выделяется внутри записи активации. Давайте рассмотрим пример:
int functionB(int inputValue) {
X return inputValue – 10;
}
int functionA(int num) {
int localVariable = functionB(num * 10);
return localVariable;
}
int main()
{
int x = 12;
int y = functionA(x);
return 0;
}
В этом коде функция main вызывает функцию functionA
, кото- рая, в свою очередь, вызывает функцию functionB
На рис. 4.1 (б) можно увидеть упрощенную версию организации стека вызовов перед возвращением управления из functionB
X
. За- писи активации всех трех функций будут расположены в стеке друг за другом, начиная с функции main в глубине стека. (Чтобы запутать вас еще больше, уточню, что стек может располагаться в памяти в об- ратном направлении, от больших адресов ячеек к меньшим. Однако ничего плохого не произойдет, если вы забудете об этом.) Логически запись активации main располагается в глубине стека, над ней лежит запись активации functionA, а еще выше запись активации functionB.
Ни одна из нижних записей активации не может вернуть управление, пока не вернет управление functionB.
Рис. 4.1. Стопка коробок и стек вызовов функции
112 Глава 4
Если стек представляет собой хорошо организованную структуру, то куча , напротив, не имеет практически никакой организации. Пред- ставим, что вы опять храните коробки, но это хрупкие коробки и вы не можете поставить их друг на друга. У вас есть большая пустая комната для хранения коробок, и вы можете ставить их на пол в любое место.
А так как коробки тяжелые, то вы стараетесь поставить их на ближай- шее свободное к двери место. Эта структура имеет свои преимущества и недостатки по сравнению со стеком. С одной стороны, такая система хранения более гибкая и позволяет вам добраться до любой коробки в любой момент. С другой стороны, в комнате очень быстро начинается неразбериха. А если все коробки разного размера, то никак не получа- ется использовать все полезное пространство пола. В итоге у вас пропа- дает слишком много места между коробками, так как туда уже ничего не помещается. Так как коробки сложно передвигать, то после выноса не- скольких остаются промежутки, которые трудно заполнить. Если гово- рить языком программирования, то наша куча представляет собой как раз такой пол. Участок памяти идущих подряд адресов ячеек. Если про- грамма предусматривает частые выделения и высвобождения памяти, то в итоге в памяти остается множество пустот между заполненными участками. Эта проблема носит название фрагментация памяти .
Для каждой программы создается своя собственная куча, память в которой распределяется динамически. Обычно в языке С++ это происходит с помощью оператора new, но также можно воспользо- ваться и старой функцией языка С для выделения памяти — malloc.
Каждый вызов оператора new (функции malloc ) выделяет кусок памя- ти в куче и возвращает указатель на него. Каждый вызов оператора delete
(или функции free , если память выделили с помощью malloc) возвращает выделенный кусок в свободный резерв кучи. Из-за фраг- ментации можно использовать далеко не всю память из резерва.
Если в начале программы в куче выделяется память под переменные
А, В, и С, то можно ожидать, что они будут соседствовать. Если мы удаляем переменную В, то оставшийся от нее участок памяти мож- но заполнить только совпадающей или меньшей по размеру заявкой, пока переменные А или С не будут удалены.
Рис. 4.2 наглядно демонстрирует положение дел. В части (а) пока- зан усеянный коробками пол комнаты. В какой-то момент простран- ство в комнате, возможно, было неплохо организовано, но теперь оно используется беспорядочно. Теперь некуда поставить малень- кую коробку (б), при том что суммарное свободное пространство значительно превосходит ее по размерам. В части (в) представлена небольшая куча. Она поделена пунктирными линиями на минималь- ные (неделимые) ячейки памяти, которые могут быть однобайтовы- ми, размером с машинное слово или более крупными – это зависит от менеджера кучи. Участки, закрашенные серым, представляют со- бой выделенную память. Для наглядности проставлена нумерация в одном выделенном блоке. Как и в случае с фрагментированным по- лом, свободная память фрагментированной кучи разбита на отдель-