Освобождение памяти с помощью free 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Освобождение памяти с помощью free



Теперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

Очевидно, что информация о размере выделенного участка должна где-то храниться. Есть несколько решения этой проблемы.

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

· Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free "подсматривает", сколько памяти необходимо удалить.

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

Работа с двумерными и многомерными массивами

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

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

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

#define COL_NUM 10

#define ROW_NUM 10

 

void main() {

float **p = NULL;

unsigned i;

 

p = (float**) malloc(ROW_NUM * sizeof(float*));

for (i = 0; i < ROW_NUM; i++) {

p[i] = (float*) malloc(COL_NUM * sizeof(float));

}

 

//Здесь какой-то важный код

for (i = 0; i < ROW_NUM; i++) {

free(p[i]);

}

free(p);

}

 

Сначала мы создаём массив указателей, а после этого каждому элементу этого массива присваиваем адрес вновь созданного массива. Это значит, что можно

· Создавать массивы "неправильной формы", то есть массив строк, каждая из которых имеет свой размер.

· Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим "треугольный" массив и заполним его значениями

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

#define SIZE 10

 

void main() {

int **A;

int i, j;

A = (int**) malloc(SIZE * sizeof(int*));

 

for (i = 0; i < SIZE; i++) {

A[i] = (int*) malloc((i + 1) * sizeof(int));

}

 

for (i = 0; i < SIZE; i++) {

for (j = i; j > 0; j--) {

A[i][j] = i * j;

}

}

 

for (i = 0; i < SIZE; i++) {

for (j = i; j > 0; j--) {

printf("%d ", A[i][j]);

}

printf("\n");

}

 

for (i = SIZE-1; i > 0; i--) {

free(A[i]);

}

free(A);

 

_getch();

}

 

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

Calloc

Функция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

 

void* calloc(size_t num, size_t size);

 

Realloc

 

Ещё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый указатель и новый размер памяти в байтах:

 

void* realloc(void* ptr, size_t size)

 

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает, где выделять память.

Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места. Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

 

#define TERM_WORD "end"

#define SIZE_INCREMENT 10

 

void main() {

//Массив указателей на слова

char **words;

//Строка, которая используется для считывания введённого пользователем слова

char buffer[128];

//Счётчик слов

unsigned wordCounter = 0;

//Длина введённого слова

unsigned length;

//Размер массива слов. Для уменьшения издержек на выделение памяти

//каждый раз будем увеличивать массив слов не на одно значение, а на

//SIZE_INCREMENT слов

unsigned size = SIZE_INCREMENT;

int i;

 

//Выделяем память под массив из size указателей

words = (char**) malloc(size*sizeof(char*));

do {

printf("%d: ", wordCounter);

scanf("%127s", buffer);

//Функция strcmp возвращает 0, если две строки равны

if (strcmp(TERM_WORD, buffer) == 0) {

break;

}

//Определяем длину слова

length = strlen(buffer);

//В том случае, если введено слов больше, чем длина массива, то

//увеличиваем массив слов

if (wordCounter >= size) {

size += SIZE_INCREMENT;

words = (char**) realloc(words, size*sizeof(char*));

}

//Выделяем память непосредственно под слово

//на 1 байт больше, так как необходимо хранить терминальный символ

words[wordCounter] = (char*) malloc(length + 1);

//Копируем слово из буффера по адресу, который

//хранится в массиве указателей на слова

strcpy(words[wordCounter], buffer);

wordCounter++;

} while(1);

 

for (i = 0; i < wordCounter; i++) {

printf("%s\n", words[i]);

}

_getch();

 

for (i = 0; i < wordCounter; i++) {

free(words[i]);

}

free(words);

}

 

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.


Тема №11

Параметры командной строки

Программа во время запуска может принимать параметры. Они являются аргументами функции main. Общий вид функции main следующий

 

void main(int argc, char **argv) {

...

}

 

Первым аргументом argc является число переданных функции параметров. Второй аргумент – массив строк – собственно сами параметры. Так как параметры у функции могут быть любыми, то они передаются как строки, и уже сама программа должна их разбирать и приводить к нужному типу.

Первым аргументом (argv[0]) всегда является имя программы. При этом имя выводится в зависимости от того, откуда была запущена программа.

 

#include <conio.h>

#include <stdio.h>

 

void main(int argc, char **argv) {

printf("%s", argv[0]);

}

 

Сочетание клавиш Win+R вызывает окно "Выполнить". Наберите в нём cmd и вы откроете командную строку. Также можно найти cmd.exe поиском в меню Пуск. В юникс-подобных операционных системах можно вызвать программу "терминал".

Стандартная для всех операционных систем команда cd осуществляет переход к нужной папке. Существует два зарезервированных имени -. (точка) и.. (две точки). Точка - это имя текущей папки.

 

cd.

 

никуда не переходит

 

.. обращение к родительской папке

 

cd..

 

переход в родительскую папку

 

Для перехода по нужному адресу пишется cd адрес. Например, нужно перейти на windows в папку C:\Windows\System32

 

cd C:\Windows\System32

 

В линуксе если нужно перейти в папку /var/mysql

 

cd /var/mysql

 

Если путь содержит пробелы, то он пишется в двойных кавычках

 

cd "D:\Docuents and Settings\Prolog"

 

Терминал имеет следующие полезные особенности: если нажать стрелку вверх, по появится предыдущая выполненная команда. Если нажать tab, то терминал попытается дополнить строку до известной ему команды, или дополнить путь, перебирая все папки и файлы в текущей папке.

 

Наберите cd C:\

нажимайте tab и смотрите, что происходит.

 

Ещё одна важная команда dir на windows и ls на linux, выводит на консоль содержимое текущей папки (той папки, в которой вы находитесь в данный момент).

Наша программа вернула своё полное имя. Перейдя в папку, где располагается наша программа можно посмотрить её содержимое.

После того, как мы перешли в нашу папку, можно выполнить нашу программу. Для этого нужно набрать её имя.

Теперь изменим программу и сделаем так, чтобы она выводила все аргументы, которые ей переданы.

 

#include <conio.h>

#include <stdio.h>

 

void main(int argc, char **argv) {

int i;

for (i = 0; i < argc; i++) {

printf("%s\n", argv[i]);

}

}

 

Давайте теперь напишем программу, которая получает два аргумента числа и выводит их сумму

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

void main(int argc, char **argv) {

int a, b;

if (argc!= 3) {

printf("Error: found %d arguments. Needs exactly 2", argc-1);

exit(1);

}

a = atoi(argv[1]);

b = atoi(argv[2]);

printf("%d", a + b);

}

 

Таким образом работает большинство программ. Кликая на ярлык, вы вызываете программу, на которую он ссылается. Большинство программ также принимают различные аргументы. Например, можно вызвать браузер firefox из командной строки и передать аргументы

firefox.exe "www.mozilla.org" "kai.ru" и он сразу же откроет в двух вкладках сайты по указанным адресам.

Многие стандартные команды также имеют параметры. В windows принято, что они начинаются с прямого слеша, в юниксе с минуса или двух минусов. Например

 

dir /AD

 

выводит только папки, а в терминале linux

 

ls -l выводит все файлы и папки с указанием атрибутов

 

Для просмотра дополнительных команд windows наберите в командной строке help или смотрите руководство (его легко найти в интернете). Для линукса команд и их опций гораздо больше, а некоторые из них являются самостоятельными языками программирования, так что стоит выучить хотя бы минимальный набор и их опции.

 

Функции

1. Во-первых, помогут выделить в отдельные подпрограммы дублирующийся код.

2. Во-вторых, помогут логически разбить программу на части.

3. В-третьих, с функциями в си связано много особенностей, которые позволят использовать новые подходы к структурированию приложений.

Функция – это именованная часть программы, которая может быть многократно вызвана из другого участка программы (в котором эта функция видна). Функция может принимать фиксированное либо переменное число аргументов, а может не иметь аргументов. Функция может как возвращать значение, так и быть пустой (void) и ничего не возвращать.

Мы уже знакомы с многими функциями и знаем, как их вызывать – это функции библиотек stdio, stdlib, string, conio и пр. Более того, main – это тоже функция. Она отличается от остальных только тем, что является точкой входа при запуске приложения.

Функция в си определяется в глобальном контексте. Синтаксис функции:

 

<возвращаемый тип> <имя функции>(<тип1> <арг1>, <тип1> <арг2>,...) {

<тело функции>

}

 

Самый простой пример – функция, которая принимает число типа float и возвращает квадрат этого числа

 

#include <conio.h>

#include <stdio.h>

 

float sqr(float x) {

float tmp = x*x;

return tmp;

}

 

void main() {

printf("%.3f", sqr(9.3f));

getch();

}

 

Внутри функции sqr мы создали локальную переменную, которой присвоили значение аргумента. В качестве аргумента функции передали число 9,3. Служебное слово return возвращает значение переменной tmp. Можно переписать функцию следующим образом:

 

float sqr(float x) {

return x*x;

}

 

В данном случае сначала будет выполнено умножение, а после этого возврат значения. В том случае, если функция ничего не возвращает, типом возвращаемого значения будет void. Например, функция, которая печатает квадрат числа:

 

void printSqr(float x) {

printf("%d", x*x);

return;

}

 

в данном случа return означает выход из функции. Если функция ничего не возвращает, то return можно не писать. Тогда функция доработает до конца и произойдёт возврат управления вызывающей функции.

 

void printSqr(float x) {

printf("%d", x*x);

}

 

Если функция не принимает аргументов, то скобки оставляют пустыми. Можно также написать слово void:

 

void printHelloWorld() {

printf("Hello World");

}

 

эквивалентно

 

void printHelloWorld(void) {

printf("Hello World");

}

 

Параметры и аргументы

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

Далее параметром мы будем называть переменную из списка параметров, заключенного в круглые скобки и заданного в определении функции, а аргументом — значение, используемое при обращении к функции.

Например, пусть есть функция, которая возвращает квадрат числа и функция, которая суммирует два числа.

 

#include <conio.h>

#include <stdio.h>

 

//параметры имеют имена a и b

//по ним мы обращаемся к переданным аргументам внутри функции

int sum(int a, int b) {

return a+b;

}

 

float square(float x) {

return x*x;

}

 

void main() {

//аргументы могут иметь любое имя, в том числе и не иметь имени

int one = 1;

float two = 2.0;

 

//Передаём переменные, вторая переменная приводится к нужному типу

printf("%d\n", sum(one, two));

//Передаём числовые константы

printf("%d\n", sum(10, 20));

//Передаём числовые константы неверного типа, они автоматически приводится к нужному

printf("%d\n", sum(10, 20.f));

//Переменная целого типа приводится к типу с плавающей точкой

printf("%.3f\n", square(one));

//В качестве аргумента может выступать и вызов функции, которая возвращает нужное значение

printf("%.3f\n", square(sum(2 + 4, 3)));

 

getch();

}

 

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

Если функция возвращает значение, то оно не обязательно должно быть сохранено. Например, мы пользуемся функцией getch, которая считывает символ и возвращает его.

 

#include <conio.h>

#include <stdio.h>

 

void main() {

char c;

 

do {

//Сохраняем возвращённое значение в переменную

c = getch();

printf("%c", c);

} while(c!= 'q');

//Возвращённое значение не сохраняется

getch();

}

 

Передача аргументов

По значению

При передаче аргументов по значению происходит их копирование. Это значит, что любые изменения, которые функция производит над переменными, имеют место быть только внутри функции. Например

 

#include <conio.h>

#include <stdio.h>

 

void change(int a) {

a = 100;

printf("%d\n", a);

}

 

void main() {

int d = 200;

printf("%d\n", d);

change(d);

printf("%d", d);

getch();

}

 

Программы выведет

 

 

Понятно почему. Внутри функции мы работаем с переменной «а», которая является копией переменной «d». Мы изменяем локальную копию, но сама переменная d при этом не меняется. После выхода из функции локальная переменная будет уничтожена. Переменная d при этом никак не изменится.


По указателю (ссылке)

Каким образом тогда можно изменить переменную? Для этого нужно передать адрес этой переменной. Перепишем функцию, чтобы она принимала указатель типа int

 

#include <conio.h>

#include <stdio.h>

 

void change(int *a) {

*a = 100;

printf("%d\n", *a);

}

 

void main() {

int d = 200;

printf("%d\n", d);

change(&d);

printf("%d", d);

getch();

}

 

Вот теперь программа выводит

 

 

Здесь также была создана локальная переменная, но так как передан был адрес, то мы изменили значение переменной d, используя её адрес в оперативной памяти.

В программировании первый способ передачи параметров называют передачей по значению, второй – передачей по указателю (ссылке). Запомните простое правило: если вы хотите изменить переменную, необходимо передавать функции указатель на эту переменную. Следовательно, чтобы изменить указатель, необходимо передавать указатель на указатель и т.д. Например, напишем функцию, которая будет принимать размер массива типа int и создавать его. С первого взгляда, функция должна выглядеть как-то так:

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

void init(int *a, unsigned size) {

a = (int*) malloc(size * sizeof(int));

}

 

void main() {

int *a = NULL;

init(a, 100);

if (a == NULL) {

printf("ERROR");

} else {

printf("OKAY...");

free(a);

}

getch();

}

 

Но эта функция выведет ERROR. Мы передали адрес переменной. Внутри функции init была создана локальная переменная a, которая хранит адрес массива. После выхода из функции эта локальная переменная была уничтожена. Кроме того, что мы не смогли добиться нужного результата, у нас обнаружилась утечка памяти: была выделена память на куче, но уже не существует переменной, которая бы хранила адрес этого участка.

Для изменения объекта необходимо передавать указатель на него, в данном случае – указатель на указатель.

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

 

void init(int **a, unsigned size) {

*a = (int*) malloc(size * sizeof(int));

}

 

void main() {

int *a = NULL;

init(&a, 100);

if (a == NULL) {

printf("ERROR");

} else {

printf("OKAY...");

free(a);

}

getch();

}

 

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

 

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

 

char* initByString(const char *str) {

char *p = (char*) malloc(strlen(str) + 1);

strcpy(p, str);

return p;

}

 

void main() {

char *test = initByString("Hello World!");

printf("%s", test);

free(test);

getch();

}

 

В этом примере утечки памяти не происходит. Мы выделили память с помощью функции malloc, скопировали туда строку, а после этого вернули указатель. Локальные переменные были удалены, но переменная test хранит адрес участка памяти на куче, поэтому можно его удалить с помощью функции free.



Поделиться:


Последнее изменение этой страницы: 2017-01-20; просмотров: 407; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.190.152.38 (0.168 с.)