Введение |
Массивы, строки и структуры |
Операторы |
Операции |
Переменные и типы |
Пояснения к курсовой работе |
Препроцессор |
Работа с файлами |
Стандарты безопасного кодирования |
Функции и модули |
Ввод-вывод |
Ранее рассматривались 4 вида памяти в компьютере, рассмотрим теперь как различные виды памяти используются для входных и выходных данных в функциях.
Аргументы функции всегда помещаются в стек и имена параметров просто связываются с соответствующими адресами стека. Также на стеке перед вызовом компилятором подготавливается место для результата функции (для базовых типов данных может использоваться регистровая память), и оператор return выполняет копирование своего аргумента в эту область памяти. Функция может возвращать значения базовых типов и структуры, но не массивы (так как имя массива является адресом, а размер задается отдельно).
Глобальные переменные, которые хранятся в статической памяти, могут использоваться для передачи данных в функцию и возврата результатов, хотя на практике это приводит к усложнению кода:
int max1(int x, int y) { // чистая функция
return x>y?x:y;
}
int pred_max=0;
void max2(int x) {
if(x>pred_max) pred_max=x;
}
int main() {
int a=4, b=5, c=3, d=1, e=2;
int sum=max1(max1(a,b),c)+max1(d,e); // сумма максимума из a,b,c и максимума d,e
// то же, но через глобальные переменные
pred_max=a; // передаем 1 аргумент
max2(b);
max2(c);
imt m1=pred_max; // сохраняем результат перед другим вызовом
pred_max=d;
max2(e);
sum=m1+pred_max;
}
Нельзя возвращать из функции адрес переменной в стековой памяти, так как время жизни этой переменной заканчивается при возврате из функции, и указатель становится некорректным. У переменных в регистровой памяти адресов нет.
Из функции можно вернуть адрес переменной (массива) в статической памяти, такой адрес будет константой, и можно рассматривать такой способ как возврат результата в глобальной переменной, рассмотренный выше, так как после каждого вызова нужно копировать результат, чтобы не потерять значение при повторном вызове.
Для создания уникальных значений, время жизни которых не ограничено функцией, используется динамическая память. В программе для доступа к таким значениям используются указатели.
Функции для работы с динамической памятью:
Функция | Назначение |
---|---|
malloc (s) |
Выделяет в куче участок памяти размером s байт и возвращает нетипизированный указатель (void* ) на его начало. Если памяти не может быть выделена, то функция возвращает нулевой указатель. |
calloc (n, s) |
Выделяет участок памяти размером из n элементов, каждый размером s байт, заполняет его нулями и возвращает нетипизированный указатель на его начало. |
free (p) |
Освобождает ранее выделенный участок памяти, адрес которого содержится в указателе p. Если в качестве аргумента нулевой указатель, то функция ничего не делает. UB при повторном освобождении участка или указателе, который не является адресом памяти из кучи. |
realloc (p,s) |
Изменяет размер ранее выделенного участка памяти (для нулевого указателя функция работает как malloc ) и возвращает нетипизированный указатель на новый участок. Данные из старого участка копируются в новый участок. Старый участок освобождается и указатель p становится недействительным. Данные могут быть базовых типов или структурами из полей базовых типов или массивов (нельзя указатели, string или vector). |
Выделение памяти в куче для одиночных значений базовых типов является бессмысленным, поэтому эти функции используются для выделения памяти для массивов или структур-элементов, которые между собой указателями.
#include <stdio.h>
int* reverse(const int a[], int n) {
int* ra=malloc(n*sizeof(int));
for(int i=0; i<n; ++i)
ra[i]=a[n-i-1];
return ra;
}
int main() {
int arr[5]={1,5,0,-4,6};
int *rev=reverse(arr,5);
for(int i=0; i<5; ++i)
printf("%d ",rev[i]);
free(rev);
}
Освобождать память в конце программы не обязательно, так как по завершении программы операционная система освобождает память, выделенную программе целиком. Но если память выделяется не однократно, а в цикле, то память нужно освобождать, когда она станет не нужной (обычно это делается, когда исчезает последний указатель на этот участок памяти).
Не стоит выделять память динамически для каждого массива (или структуры) в программе, нужно оценивать время их жизни. Если время жизни начинается и заканчивается в той же функции, то можно использовать стековую память, даже если размер массива задается выражением, а не константой:
// устаревший вариант 1, до C99
void func(int n) {
int *arr=calloc(n, sizeof(int));
// ... действия с arr
free(arr);
}
// устаревший вариант 2, до C99
void func(int n) {
int *arr=alloca(n*sizeof(int)); // нестандартная функция для выделение памяти на стеке
// поддерживалась многими компиляторами
// ... действия с arr
// память освобождается автоматически
}
// После C99
void func(int n) {
int arr[n];
// ... действия с arr, работает операция sizeof(arr)
// память освобождается автоматически
}
В C++ обязательным требованием является преобразование нетипизированного указателя к типизированному:
int *a;
int n;
...
a=(int *)malloc(n*sizeof(int));
Но это не поможет компилятору найти ошибку, если программист забудет умножить количество элементов на размер одного элемента или укажет неправильный тип:
a=(int *)malloc(n); // забыл умножить
a=(int *)malloc(n*sizeof(short)); // забыл изменить тип
Поэтому в C++ используются специальные операции для выделения памяти и освобождения памяти:
a=new int[n]; // состояние памяти не определено, аналог malloc
a=new int[n](); // с обнулением памяти, аналог calloc
delete[] a; // аналог free
Эти операции, в отличие от функций C, корректно работают со string
и другими типами С++, возвращают типизированный указатель (компилятор сможет проверить соответствие типов). Аналога realloc нет, так как в С++ вместо массивов в динамической памяти рекомендуется использовать vector
.