Первый слайд презентации
Хранение переменных в памяти Программа, которая выводит размер переменных разных типов:
Слайд 2
Оператор sizeof – специальная инструкция C++, которая возвращает размер своего аргумента в байтах. Результат работы программы (при использовании другого компилятора можно получить другие результаты):
Слайд 3
Значения переменных хранятся в оперативной памяти. Оперативная память разбита на байты, каждый из которых имеет свой адрес. Адрес записывается в шестнадцатеричном формате. Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x60FE98, 0x60FE99, 0x60FE9A, 0x60FE9B. И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x60FE98.
Слайд 4
Программа, демонстрирующая расположение переменных в памяти: Результатом может быть, например, значение 0x28ff18. В разных системах могут получиться разные результаты, потому что адреса в оперативной памяти распределяются таким образом, чтобы максимально уменьшить фрагментацию. Поскольку в любой системе список запущенных процессов, а также объем и разрядность памяти могут отличаться, система сама распределяет данные для обеспечения минимальной фрагментации.
Слайд 5
Итак, при выполнении любой программы все необходимые для ее работы данные должны быть загружены в оперативную память компьютера. Для обращения к переменным, находящимся в памяти, используются специальные адреса, которые записываются в шестнадцатеричном формате. Если переменных в памяти потребуется слишком большое количество, которое не сможет вместить в себя сама аппаратная часть, то произойдет перегрузка системы или ее зависание. Если объявлять переменные статично, они остаются в памяти до того момента, как программа завершит свою работу, а после чего уничтожаются. Такой подход может быть приемлем в простых программах, которые не требуют большого количества ресурсов. Если же разрабатываемый проект является огромным программным комплексом с высоким функционалом, объявлять таким образом переменные было бы неразумно. Например, если бы при создании игры жанра « Shooter » использовался этот метод работы с данными, то игрокам пришлось бы перезагружать свои высоконагруженные системы после нескольких секунд работы игры. Дело в том, что игрок в каждый момент времени видит различные объекты на экране монитора – все они занимают какое-то место в оперативной памяти компьютера. Если не уничтожать неиспользуемые объекты – обломки, пыль, тени, моменты выстрелов и т.п. – то очень скоро они заполнят весь объем ресурсов ПК.
Слайд 6
По этим причинам в языке C++ (и во многих других языках) имеется указатель. Указатель (англ. – pointer) – это переменная, которая содержит адрес ячейки оперативной памяти, адрес другой переменной (т.е. ее расположение в памяти). Можно обращаться, например, к массиву данных через указатель, который будет содержать адрес начала диапазона ячеек памяти, хранящих этот массив. После того, как этот массив станет не нужен для выполнения остальной части программы, нужно просто освободить память по адресу этого указателя, и она вновь станет доступной для других переменных.
Слайд 7
Указатели имеют две сферы применения: Использование выгоды косвенной адресации: экономия памяти. Делая указатель на файл, мы читаем его из памяти, а не загружаем в ОЗУ. Передавая указатель на переменную в функцию, мы не делаем копию этой переменной и редактируем ее напрямую. Указатели используют для хранения адресов точек входа для подпрограмм в процедурном программировании и для подключения динамических подключаемых библиотек; Методы динамического управления памятью. Выделяется место в так называемой куче (динамической памяти), а переменные, для которых таким образом выделили память, называют динамическими.
Слайд 8
Куча (англ. heap) – это название структуры данных, с помощью которой реализована динамически распределяемая память приложения, а также объем памяти, зарезервированный под эту структуру.
Слайд 9
Размер кучи – размер памяти, выделенной операционной системой для хранения кучи (под кучу). Принцип работы: при запуске процесса операционная система выделяет память для размещения кучи. В дальнейшем память для кучи (под кучу) может выделяться динамически. Память кучи можно разделить на занятую и свободную (еще не занятую или уже освобожденную). Перед началом работы программы выполняется инициализация кучи, в ходе которой вся изначально выделенная под кучу память отмечается как свободная. Стек (англ. stack – стопка ) – абстрактный тип данных, представляющий собой список элементов, организованных по принципу LIFO (англ. last in – first out, “ последним пришел – первым вышел ”). “ Куча – большой комод, в который можно класть вещи (оператор new). Но чтобы комод не лопнул, надо из него ненужное удалять (оператор delete). Чтобы с вещью, положенной в комод, можно было общаться, дается веревочка (указатель p = new XXX). Стек – это стопка книг. Читать и писать можно только в верхней. Сняли книгу, она пропала, всё, что в ней было написано – недоступно. Ну а очередь… – кто первый встал, того и валенки. ” (с) cyberforum
Слайд 10
Переполнение стека Переполнение стека (англ. stack overflow ) возникает, когда в стеке вызовов хранится больше информации, чем он может вместить. fn () пытается бесконечно вызывать сама себя. Это приводит к переполнению стека (если в fn () добавить еще переменных, то переполнение произойдет быстрее).
Слайд 11
int num = 4; int* pNum = # 0004 0008 000C 0010 0014 Адреса ячеек в памяти Переменная num объявляется и инициализируется. После чего объявляется переменная-указатель pNum. Затем указателю pNum присваивается адрес переменной num. Таким образом обе переменные можно использовать для доступа к одному и тому же месту в памяти. В C++ предусмотрено две операции над указателями: присваивание и разыменование. Первая из этих операций присваивает указателю некоторый адрес. Вторая служит для обращения к значению в памяти, на которое указывает указатель.
Слайд 12
Возможны три варианта расположения * при объявлении указателей, каждый из которых имеет своих сторонников: int* x; int * x; int *x; Компилятору безразлично, какой из способов используется программистом. Такой способ не самый наглядный float *x, y, *z; //описаны указатели на вещественные числа - x и z //а также вещественная переменная y поэтому рекомендуется объявлять каждую переменную в отдельной строке. Но если в одной строке объявляются однотипные указатели и переменные, звездочка рядом с типом может привести к путанице: float* x, y; // x – указатель, y – обычная переменная типа float. Visual Studio явно рекомендует 1-й способ.
Слайд 14
Помимо указателей, есть еще возможность создать ссылку на адрес переменной, которая будет хранить адрес переменной, но уметь работать с данными напрямую (без разыменования). Под ссылку тоже в оперативной памяти выделяется ячейка. Отличия указателя от ссылки: 1. В отличие от указателей, у ссылок нет оператора разыменования. Они и так выводят значение. Ссылки
Слайд 15
2. С помощью арифметики указателей можно изменить адрес, на который указывает указатель: Для ссылок нет понятия арифметики ссылок. Следующий код приведет к увеличению значения переменной, а не адреса (& xref выводит адрес переменной x) :
Слайд 16
3. Указатель может быть неинициализированным, а ссылка – нет. 4. Указатель может ссылаться на 0, на NULL или на nullptr, а ссылка такой тип данных хранить не может:
Слайд 17
Можно создать цепочку указателей: int x = 22; int * px = &x; // указатель px хранит адрес переменной x int ** ppx = & px ; // указатель p px хранит адрес указателя p x Здесь указатель ppx хранит адрес указателя px. Поэтому через выражение * ppx ( разыменование) можно получить значение, которое хранится в указателе px – адрес переменной x. А через выражение ** ppx можно получить значение по адресу из px, то есть значение переменной x.
Слайд 18
Можно организовать взаимодействие ссылок и указателей: Получается цепочка ссылок. Если изменить в конце цепочки значение, то изменение по цепочке перейдет к переменной x :
Слайд 19
Итак, значение можно изменить как с помощью указателя, так и с помощью ссылки: через них можно задать новое значение переменной x :
Слайд 20
Использование * Для объявления указателя Операция разыменование (получение значения того, чей адрес хранит указатель) Использование & Для объявления ссылки Получение адреса Получение адреса того, на что ссылается ссылка
Слайд 21
Примеры обращения к переменным через указатель и напрямую: Пример использования обычных переменных: 2. Пример оперирования динамическими переменными посредством указателей: new – оператор языка программирования C++, обеспечивающий выделение динамической памяти для размещения новых данных и, в случае успеха, возвращающий адрес свежевыделенной памяти. delete удалит данные по этому адресу, но указатель всё еще указывает на этот адрес. Поэтому после этого указатель нужно обнулить.
Слайд 22
Выделение памяти с помощью оператора new имеет вид: тип_данных *имя_указателя = new тип_данных; например int * a = new int; После удачного выполнения такой операции, в оперативной памяти компьютера происходит выделение диапазона ячеек, необходимого для хранения переменной типа int. Для разных типов данных выделяется разное количество памяти. Следует быть особенно осторожным при работе с памятью, потому что именно ошибки программы, вызванные утечкой памяти, являются одними из самых трудно находимых. На отладку программы в поисках одной ничтожной ошибки может уйти час, день, неделя ( в зависимости от упорства разработчика и объема кода ). Инициализация значения, находящегося по адресу указателя, выполняется схожим образом, только в конце ставятся круглые скобки с нужным значением: тип_данных *имя_указателя = new тип_данных (значение); например int * b = new int (5) ;
Слайд 23
Любая динамическая память, выделенная при помощи new, должна освобождаться с помощью оператора delete. Существует два варианта: один для единичных объектов, другой для массивов: int *ptrVar = new int; int * pArray = new int [50]; // динамический массив delete [] pArray ; delete ptrVar ; После того, как память была возвращена в кучу, необходимо обнулить указатель: ptrVar = 0; или (то же самое) ptrVar = NULL; // NULL – это скрипт, который просто заменяет значение NULL на 0. ptrVar = nullptr; // лучше использовать nullptr, т.к. nullptr != 0
Слайд 24
Пример освобождения памяти с помощью оператора delete : #include <iostream> using namespace std; int main() { // Выделение памяти int *a = new int; int *b = new int; float *c = new float; //... Любые действия программы... // Освобождение выделенной памяти delete c; delete b; delete a; return 0; } При использовании оператора delete для указателя – знак * не используется.
Слайд 25
Оператор delete возвращает память, выделенную оператором new, обратно в кучу. Вызов delete должен происходить для каждого вызова new, чтобы избежать утечки памяти. Утечка памяти – это процесс неконтролируемого уменьшения объема свободной оперативной или виртуальной памяти компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих ненужные уже участки памяти, или с ошибками системных служб контроля памяти. Пример: В 8 строке создается объект в динамической памяти. Код в 8 строке выполняется 1000 раз, причем каждый следующий раз адрес нового объекта перезаписывает значение, хранящееся в указателе px. В 11 строке выполняется удаление объекта, созданного на последней итерации цикла. Однако первые 999 объектов остаются в динамической памяти, и одновременно в программе не остается переменных, которые хранили бы адреса этих объектов. Т.е. в 11 строке невозможно ни получить доступ к первым 999 объектам, ни удалить их. Итого, утечка памяти здесь составляет 999*500*8 байт (почти 4 МБ). И программа скомпилируется без ошибок – об утечке памяти компилятор не сообщит.
Слайд 28
int X; int *pY; pY = &X; *pY = 10.0; Последняя строка требует, чтобы по адресу, выделенному под переменную размером 4 байта, было записано значение, имеющее размер 8 байтов. В этом случае компилятор приведет 10.0 к типу int перед тем, как выполнить присвоение. Соответствие типов указателей:
Слайд 29
Привести переменную одного типа к другому явным образом можно так ( явное понижающее приведение типа ): int i; double d = 10.0; i = (int) d; Так же можно привести и указатель одного типа к другому: int * pX ; double * pD ; pX = (int*) pD ;
Слайд 30
Сохранение переменных, имеющих б ó льшую длину: В строке double *d = (double*)&x; объявляется указатель * d типа данных double ; берётся адрес переменной x (не сама переменная, а ее значение. x остается int ), преобразуется в тип данных для хранения вещественных переменных и присваивается указателю *d. Тип указателя должен быть таким же, как у переменной, адрес которой он хранит.
Слайд 31
В результате работы этой программы возможно уничтожение переменных, расположенных рядом: переменная x может оказаться " забитой " мусором: Возможна ошибка на этапе компиляции или этапе выполнения. Ошибка сегментации возникает при попытке обращения к недоступным для записи участкам памяти.
Слайд 32
Размер x остается 4 байта. Размер значения, которое хранится по адресу содержащемуся в указателе *d, занимает 8 байт. Попытка преобразования типа и попытка записать 15.7 приводит к тому, что благодаря ошибке нарушения сегментации по одному адресу хранится мусор и в то же время 8-байтовое значение из * d.
Слайд 34
Передача указателей функциям Передача аргументов по значению Обычно нельзя изменить значение переменной, которая передавалась функции как аргумент, например:
Слайд 35
Передача значений указателей Указатель, как и любая другая переменная, может быть передан функции в качестве аргумента.
Слайд 36
Передача аргументов по ссылке В C++ возможна сокращенная запись приведенного ранее фрагмента, которая не требует от программиста непосредственной работы с указателями. Здесь переменная передается по ссылке: В этом примере функция fn () получает не значение переменной x, а ссылку на нее и, в свою очередь, записывает 27 в переменную x, на которую ссылается ссылка x.
Слайд 38
Область видимости Если можно передать функции указатель, то можно и вернуть его как результат работы функции. Функция, которая должна вернуть некоторый адрес, объявляется следующим образом: double *fn(void); При работе с возвращаемыми указателями следует учитывать область видимости переменных – это часть программы, в которой определена переменная.
Слайд 39
Этот фрагмент программы будет скомпилирован, но не будет корректно работать: Проблема в том, что переменная x объявлена внутри функции fn2(). Следовательно, в момент возврата адреса x из fn2() самой переменной x уже не существует, и адрес ссылается на память, которая может быть занята чем-то другим.
Слайд 40
Чтобы не возникло такой ошибки видимости: Здесь два разных указателя с одинаковым именем pX содержат в итоге один и тот же адрес: Адрес, возвращенный из функции fn2(), записывается в 12 строке в указатель pX (который локально объявлен в fn1() ) и потом используется в 14 строке для записи значения 1.7 в эту же память, ранее выделенную в куче в fn2(). В 18 строке обнуляется указатель pX из fn1(). Теперь, несмотря на то, что pX из fn2() имеет область видимости в пределах функции fn2(), память, на которую указывает этот pX, не будет освобождена после завершения работы функции fn2().
Слайд 42
Объявление и использование массивов указателей Поскольку массив может содержать данные любого типа, он может состоять и из указателей. В 28 строке – вывод размера массива указателей : указатель занимает в памяти 4 байта, поэтому размер массива указателей из 4 элементов 4*4=16 байт (а не 8*4=32). Например, элемент pA [0] является указателем на переменную n 1.
Слайд 43
Можно сразу при объявлении массива указателей этот массив инициализировать адресами:
Слайд 44
Задача 1. Написать функцию, которая принимает адреса двух переменных и меняет местами значения этих переменных. Вывод новых значений переменных должен быть в функции main ()
Последний слайд презентации: Хранение переменных в памяти Программа, которая выводит размер переменных
Подготовиться к ответу на вопросы: 1. Что такое указатель? Для чего нужны указатели? 2. С помощью какого оператора можно узнать размер переменной? Какой размер могут иметь переменные разного типа? 3. До какой буквы в шестнадцатеричном формате записывается адрес переменных? 4. До какого момента локальные переменные продолжают свое существование? Почему нельзя ограничиться использованием только локальных переменных? 5. Что означает значение переменной-указателя NULL ? Имеет ли указатель собственный адрес? 6. Что такое куча? Что такое размер кучи и на какие виды памяти она делится? 7. Как сделать, чтобы 2 переменные имели доступ к одному и тому же месту в памяти? Как получить адрес переменной? Как получить или изменить содержимое, находящееся по адресу, который содержится в указателе? Как называется эта операция? 8. Что делает оператор new ? 9. Зачем нужен оператор delete ? Каков синтаксис использования этого оператора? Обязательно ли его использовать для всех вызовов new ? Зачем? Что такое утечка памяти? 10. Что такое область видимости? Какие бывают области видимости, и что они означают? Что происходит с локальными переменными после того, как функция завершила свою работу – или они сохраняются и просто не используются, или уничтожаются? 11. Что такое массив указателей, что он хранит?