Лекция 4. Указатели и динамическая память. Выделение и освобождение динамической памяти, размещение в ней переменных. Средства Windows для работы с памятью. Передача параметров функций по ссылке и по указателю. УКАЗАТЕЛИ И ДИНАМИЧЕСКАЯ ПАМЯТЬ Динамическая память - это оперативная память ЭВМ, предоставляемая программе при её работе. Динамическое размещение данных означает использование динамической памяти непосредственно при работе программы. В отличие от этого статическое размещение данных осуществляется компилятором, то есть должны быть известны заранее количество и тип размещаемых данных. При динамическом размещении они могут быть заранее не известны. Оперативная память ЭВМ представляет собой совокупность ячеек для хранения информации – байтов, каждый из которых имеет собственный номер. Эти номера называются адресами, они позволяют обращаться к любому байту памяти. Язык С++ предоставляет в распоряжение программиста средство управления динамической памятью – так называемые указатели (pointer). Указатель – это переменная, которая в качестве своего значения содержит адрес байта памяти. С помощью указателей можно размещать в динамической памяти любой тип данных. Лишь некоторые из них занимают во внутреннем представлении один байт, остальные – несколько смежных. Поэтому на самом деле указатель содержит адрес только первого байта данных. В С++ указатели всегда связываются с некоторым типом данных (при этом говорят, что указатель ссылается на соответствующий тип). Хотя адрес – это по существу 32 битное целое число, определяющее положение объекта в оперативной памяти ЭВМ, указатель как бы «помнит» на какого рода данные он ссылается. Объявление указателя выглядит так: тип_объекта *имя_указателя; Пример: int *aa; double *bb; Чтобы получить доступ к объекту (переменной), на который указатель ссылается, применяют операцию разыменования указателя *. Например, *aa будет представлять собой значение переменной, на которую ссылается указатель aa. Чтобы, наоборот, получить адрес переменной, нужно применить операцию взятия адреса &. Пример: double pi=3.14; double *aa; aa=&pi; // Здесь указателю аа присваивается адрес переменной pi. printf(“%f”, *aa); // Здесь на печать выводится значение, содержащееся по адресу аа. ВЫДЕЛЕНИЕ И ОСВОБОЖДЕНИЕ ДИНАМИЧЕСКОЙ ПАМЯТИ, РАЗМЕЩЕНИЕ В НЕЙ ПЕРЕМЕННЫХ Основное назначение указателей - создание и обработка динамических структур данных. В С++ можно выделить память под некоторый объект не только с помощью оператора объявления, но и динамически, во время исполнения программы. Выделение памяти для размещения объекта производится с помощью функции malloc() (для доступа к ней необходимо сослаться на заголовки <alloc.h> и <stdlib.h>). Пример: //-----------------------------------------------------------double *aa; //Объявление указателя на тип double aa=(double*)malloc(sizeof(double)); // Динамическое выделение памяти размера // sizeof(double) *aa=2.71; // присвоение значения 2.71 динамической переменной, размещённой по // адресу aa printf(“%f”,*aa); // вывод на печать значения динамической переменной free(aa); // Освобождение памяти, закреплённой за указателем аа. //------------------------------------------------------------Несколько пояснений к примеру. Аргументом функции malloc() является размер области памяти, которую нужно выделить; для этого можно применить операцию sizeof(), которая возвращает размер переменной или типа, указанного в качестве операнда. Функция malloc() возвращает значение типа void* – то есть указатель на пустые данные. Такой указатель нельзя разыменовывать, поскольку неизвестно, на данные какого типа он указывает, поэтому к нему применяется операция приведения типа (double*). Память, выделенную malloc(), следует освободить функцией free(), если динамическую переменную больше не предполагается использовать. Указатели и массивы Между указателями и массивами в С++ существует тесная связь. Имя массива без индекса эквивалентно указателю на его первый элемент. И наоборот, указатель можно использовать подобно имени массива, то есть индексировать его. Например: //--------------------------------int arr[5], i; int *aa; aa=arr; // указателю аа присваивается адрес первого элемента массива arr. // это эквивалентно записи aa=&arr[0]; i=aa[2]; // обращение к элементу массива arr с индексом [2]. //--------------------------------Приведём пример динамического объявления одномерного массива длины n: int *aa; int n; n=10; aa=(int*)malloc(n*sizeof(int)); // Теперь к массиву aa можно обращаться обычным способом аа[0], aa[1] и т.п. //После завершения с ним работы следует освободить занимаемую им память free(aa); //-----------------------------------Для организации многомерных динамических массивов применяется приём типа «указатель на указатель». Пример динамического объявления двумерного массива размерностью m на n: //-------------------------------------int **aa; int m, n, i; m=3; n=5; aa=(int**)malloc(m*sizeof(int*)); //Выделение памяти под массив из m указателей на int for(i=0;i<m;i++) { aa[i]=(int*)malloc(n*sizeof(int));// Выделение памяти под массивы из n целых чисел } // Теперь к массиву aa можно обращаться обычным способом аа[0][0], aa[1][0] и т.п. //После завершения с ним работы следует освободить занимаемую им память for(i=0;i<m;i++) { free(aa[i]); } free(aa); //-----------------------------------При работе с динамической памятью кроме функции malloc() в С++ часто используется также функция calloc(). Кроме того, существуют также специальные операции new и delete. Они введены для предоставления возможности перегрузки этих операций для придания им каких-либо дополнительных свойств. Вот пример создания и уничтожения динамической переменной с помощью операций new и delete. //--------------------------------int *aa, *bb; int n; n=5; aa=new int; // Выделение памяти под переменную типа int bb=new int[n]; // Выделение памяти под массив значений типа int длины n delete aa;// Освобождение памяти под переменной delete[] bb//; Освобождение памяти под массивом //--------------------------------Использование процедур выделения и освобождения памяти, как и вообще вся работа с динамической памятью требует особой осторожности и тщательного соблюдения следующего правила: освобождать нужно ровно столько памяти, сколько её было зарезервировано и именно с того адреса, с которого она была зарезервирована. СРЕДСТВА WINDOWS ДЛЯ РАБОТЫ С ПАМЯТЬЮ Система Windows имеет собственные средства для работы с памятью. Это так называемые API-функции (application programming interface), которые становятся доступными после ссылки на заголовочный файл <windows.h>. Так как почти все устройства ЭВМ (в том числе оперативная память) в многозадачных ОС являются разделяемыми ресурсами, то есть программное адресное пространство не совпадает с физическим, APIфункции в своей работе широко используют понятие системного дескриптора (стандартный тип HANDLE). Дескриптор – это 32 разрядное число, хранящее условный адрес структуры с описанием той или иной программной сущности (переменной, объекта, области памяти, файла, потока, окна и т.п.). Перечислим основные функции WinAPI, используемые для работы с динамической памятью (полное их описание можно получить, обратившись к справочным файлам WIN32.HLP или WIN32S.HLP). GlobalAlloc() – выделяет память требуемого размера; GlobalFree() – освобождает блок памяти; GlobalLock() – возвращает указатель на первый байт указанного блока памяти (как бы «преобразуя» дескриптор HANDLE в указатель); GlobalHandle() – возвращает дескриптор блока памяти, связанного с заданным указателем (как бы «преобразуя» указатель в дескриптор HANDLE); (См. также пример программы к лекции 5). ПЕРЕДАЧА ПАРАМЕТРОВ ФУНКЦИЙ ПО ССЫЛКЕ И ПО УКАЗАТЕЛЮ В языке С++ в качестве параметров функций допускается передавать не только значения переменных, но и указатели на переменные. Например: double sum(double *aa, int n) {int i; double j=0; for(i=0;i<n;i++) {j=j+aa[i];} return j; } Данная функция sum выдаст сумму элементов массива aa (в данном случае память под массив и его инициализация происходит вне тела функции sum; говорят, что массив aa передается в функцию по указателю). Следует помнить, что в случае передачи параметров по указателю, изменение их значений внутри тела функции привёдёт к изменению их значений и вне тела функции. В С++ имеется также модифицированная форма указателей, называемая ссылкой. Ссылка – это указатель, который автоматически разыменовывается при обращении к нему. Ссылки оказываются удобны для передачи в функцию параметров по указателю, причём позволяют сделать это без использования явных указателей и адресов. Пример передачи параметров функции по ссылке: //-------------------------------------------------------------int i, j; i=1; j=2; void example(int &i, int j) // переменная i передаётся в функцию как ссылка, // j – как значение {int k; k=i+j; i=k; j=k; } // Вызов функции example example(i, j); //------------------------------------------------------------Результатом работы данной программы будет i=3, j=2. То есть переменная i, переданная по ссылке (int &i в списке параметров функции example), изменила своё значение, а переменная j, переданная по значению, нет. По ссылке возможна не только передача параметров в функцию, но и передача из неё возвращаемого значения. В том случае, когда функция возвращает ссылку, она фактически возвращает адрес; это делает возможным написание функции слева от оператора присваивания в выражениях, соответствующее значение будет помещено по адресу, возвращённому функцией. int& func(int n) { … }; func(5) = 10; Помимо случаев, когда функция должна возвращать значения в параметрах, ссылки и указатели могут быть полезны при передаче переменных большого размера, поскольку функции в этом случае передаётся не сама переменная, а её адрес. ЗАДАНИЕ К ЛЕКЦИИ 4 Разместить в динамической памяти 2 массива – матрицу размерностью n на n и вектор длины n, с элементами типа long double (значения n и элементы массивов ввести с консоли). Написать функцию, вычисляющую произведение данной матрицы на данный вектор (их передать по указателю) и возвращающую указатель на массив-результат. Выдать на консоль значения полученного вектора.