Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Лабы / C++.Ашарина / !Сделанные / Lab3 / Динамические структуры

.doc
Скачиваний:
35
Добавлен:
17.04.2013
Размер:
233.47 Кб
Скачать

Лабораторная работа

Программирование задач с использованием динамических структур данных на языке С (С++).

Цель работы:

-изучить ссылочный тип данных в языке С++ и научиться программировать с использованием этого типа данных;

-получить практические навыки решения задач на ПЭВМ с использованием динамических переменных.

Теоретические сведения.

При программировании на языке C (C++) память требуется для четырех целей:

-для размещения программного кода;

-для размещения статических данных;

-для динамического использования;

-для резервирования компилятором на время выполнения программы.

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

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

Все статические переменные при программировании на IBM PC размещаются в одной непрерывной области памяти, называемой сегментом данных. Размер сегмента данных определяется архитектурой процессора 8086 и не может превышать 65536 байт, что делает весьма затруднительной обработку большого количества данных. Выходом может служить использование динамических данных, располагаемых в куче.

Физически куча располагается в старших адресах памяти сразу за стеком.

Динамические переменные могут создаваться и уничтожаться в любой момент выполнения программы посредством использования функций alloc, malloc, и им подобным.

Функция выделения памяти

void * malloc(size_t size); //STDLIB.H или ALLOC.H

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

Функция выделения и очистки памяти

Void calloc(size_t nitems, size_t size); //STDLIB.H

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

Параметры size_t nitems – количество выделяемых элементов,

size_t size – размер одного элемента в байтах.

Функция освобождения выделенного блока

void free (void *block); //ALLOC.H

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

В программах, написанных на ANSI C, используются функции malloc( ) (и аналогичные) и free( ) для выделения и освобождения блоков памяти в куче. В программах, написанных на С++, часто используются операторы (а не функции) new и delete. В большинстве реализаций С++ new и delete скрыто реализуются вызовами функций malloc( ) и free( ). Однако как операторы new delete могут перегружаться с целью задания новых возможностей управления памятью для объектов класса.

Никогда не следует освобождать память, захваченную оператором new c помощью функции free( ), и никогда не следует пользоваться функцией malloc, если предполагается освобождать память оператором delete.

Для доступа к динамическим переменным используются ссылки и указатели.

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

Существует четыре способа задания переменной – указателю осмысленного начального значения:

-описать указатель вне любой функции или снабдить его предписанием static. При этом начальным значением является нулевой адрес памяти;

-присвоить указателю адрес переменной;

-присвоить указателю значение другого указателя, к этому моменту уже правильно инициализированного;

-использовать функции распределения памяти:

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

что означает:x описан как указатель на целое, начальное значение которого равно адресу, возвращаемому функцией malloc.

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

Связный список – это список, созданный с помощью указателей структур своего собственного типа, поэтому связные списки иногда называют самоссылочными структурами:

head

“Вишня”

12

“Малина”

10

“Клубника”

5

маркер конца, NULL

Следует обратить внимание на то, что указатель указывает на структуру (узел) целиком, а не на отдельные компоненты, находящиеся внутри узла.

В С++ узлы реализуются в виде структур или классов.

Опишем структуру для узла списка, показанного на рисунке:

const int STRING_SIZE=10;

struct ListNode

{

char item[STRING_SIZE];

int count;

ListNode *link;

};

typedef ListNode*ListNodePtr;

Здесь head является не узлом, а переменной-указателем, указывающим на первый узел. Указатель head объявляется следующим образом:

ListNodePtr head;

Запись (*head). count=12; эквивалентна записи

head>count=12;

Первая из приведенных здесь записей называется операцией прямого выбора, а вторая-операцией косвенного выбора. Операция косвенного выбора (->) объединяет в себе назначение операции разыменования* и операции выбора поля структуры(.). Следует отметить, что наличие круглых скобок вокруг head* обязательно, так как приоритет операции выбора члена выше приоритета операции разыменования.

В качестве конца списка используется константа NULL. NULL-это предопределенная константа, являющаяся особым значением указателя.

В общем случае NULL используется в качестве значения переменной-указателя, которая в другом случае вообще не имела бы значения. NULL может быть присвоено переменной-указателю любого типа. Идентификатор NULL определен в библиотеке с заголовочным файлом stdlib.h

Рассмотрим более подробно связные списки.

Первый узел связного списка называется головным (или заголовочным) узлом, поэтому переменная-указатель, указывающая на первый узел, обычно называется head. Необходимо обратить внимание на то, что переменная-указатель head является не заголовочным узлом списка, а лишь указателем на него. Последний узел списка имеет указатель на следующий элемент, равный NULL.

Для простоты будем использовать связный список, состоящий из узлов, имеющих всего два поля:

struct Node { int data;

Node *link;

};

typedef Node*NodePtr;

Создадим список, состоящий для начала, всего из одного элемента.

NodePtr head; //объявляем переменную указатель головного узла.

Для формирования первого узла воспользуемся операцией new для создания динамической переменной:

head=new Node;

Теперь можно присвоить значения переменным-членам созданного узла:

head>data=3;

head>link=NULL;

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

Создадим функцию, добавляющую новые узлы в начало списка. Список, организованный по такому принципу носит название стековой структуры или стека (stack). Доступ к элементам стека осуществляется по принципу “последним вошел, первым вышел” (LIFO-Last Input, First Output). Прототип создаваемой функции выглядит следующим образом:

void head_insert(NodePtr &head, int the_number);

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

Чтобы вставить новый узел в связный список, его (узел) нужно создать с помощью операции new. Затем данные копируются в новый узел, который вставляется в начало списка. При этом новый узел окажется первым (а не последним!) узлом списка. Так как динамические переменные не имеют имен, для указания на узлы списка следует использовать локальные переменные. Например, если локальная переменная имеет имя temp_ptr, то к новому узлу можно обратиться как *temp_ptr.

Поле link нового узла должно указывать на первый узел исходного связного списка, а переменная указатель head нового списка должна указывать на новый узел. Обеспечить это могут следующие операторы:

temp_ptr>link=head;

head=temp_ptr;

temp_ptr– >link=head;

temp_ptr

12

?

temp_ptr

12

head

15

15

head

3

NULL

3

NULL

рис.1а рис.1б

head=temp_ptr;

12

12

temp_ptr

15

head

15

head

3

NULL

3

NULL

рис.1в рис.1г

Рис.1 Вставка первого элемента в связный список.

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

При этом

head=NULL;

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

Если известно количество элементов в формируемом списке, то функция формирования имеет вид:

struct Node *form(int n)

{ struct Node head=NULL, *temp_ptr;

for (int i=0;i<n;i++)

if((temp_ptr=new Node)==NULL) return NULL;

else temp_ptr–>data=random(100)–50;

temp_ptr–>link=head;

head=temp_ptr;

return temp_ptr;

}

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

Прототип такой функции имеет вид:

NodePtr search(NodePtr head, int target);

Здесь указатель head указывает на первый узел, значение которого равно target. Если таких узлов не найдено, функция возвращает NULL.

Для передвижения по связному списку и поиска значения target мы будем использовать локальную переменную-указатель here, которая в первый момент указывает на первый (головной) узел, а затем перемещается по списку, следуя указателю из текущего узла.

target=6

head

2

head

2

не здесь

here

?

1

here

1

6

6

3

NULL

3

NULL

рис.2а рис.2б

head

2

head

2

1

here

1

не здесь

6

here

6

найдено

3

NULL

3

NULL

рис.2в рис.2г

Рис.2 Поиск элемента в связном списке.

// Функция поиска значения среди узлов связного списка.

// Использует stddef.h

NodePtr search(NodePtr head, int target)

{ NodePtr here=head;

if (here==NULL) return NULL;

else { while (here–>data!=target && here–>link!=NULL)

here =here–>link;

if ( here–>data==target) return here;

else // Обработка пустого списка.

return NULL;

}

}

Теперь создадим функцию для вставки узла в указанное место связного списка. Чаще всего используется и проще всего реализуется вставка элемента после указанного узла связного списка.

Пусть указатель after_me указывает на тот узел, после которого будет осуществляться вставка нового узла. Прототип функции вставки элемента имеет вид:

void insert (NodePtr after_me, int the_number);

head

2

after_me

3

5

temp_ptr

9

18

NULL

Рис 3. Вставка звена в середину связного списка.

Проверка показывает, что вставка элемента в середину списка и в его конец вполне идентичны.

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

//Функция для добавления узла в середину связного списка

//Использует iostream.h, stdlib.h, stddef.h

void insert (NodePrt after_me, int the_number)

{ NodePrt temp_prt;

temp_prt=new Node;

if (temp_prt==NULL)

{ cout <<”Ошибка: Недостаточно памяти.\n”

exit(1);

}

temp_ptr–>date=the_number;

temp_ptr–>link=after_me–>link;

after_me–>link=temp_ptr;

}

Удаление узла представляет собой не больше сложностей, чем вставка нового узла. Для этого следует установить указатель, названный здесь discard, таким образом, чтобы он указывал на удаляемый узел, а указатель, обозначенный как before, - на узел, расположенный перед удаляемым узлом.

head

2

head

2

1

before

1

6

6

3

discard

discard

удален

5

NULL

5

NULL

Рис. 4 Удаление узла.

После установки указателей before и discard удалить узел можно с помощью следующего оператора:

before–>link=discard–>link;

Очень важно иметь возможность удалять узлы связного списка. Но не менее важно возвращать занимаемую удаленным узлом память в кучу. Это можно сделать с помощью обращения к операторам delete:

delete discard;

Кроме стековых структур, с помощью связных списков можно организовать структуру типа “очередь” (squeue), то есть структуру организованную по принципу:”первым вошел-первым вышел” (FIFO-First Input, First Output).

В качестве базового узла для очереди выбираем такой же узел, как для стека:

struct Node { int data;

Node * link;

};

typedef Node *NodePtr;

//Процедура формирования очереди

void end_insert (NodePtr &head, in the_number)

{ NodePtr tail; // хвост

NodePtr tamp_ptr;

temp_ptr=new Node;

if (temp_ptr==NULL)

{ cout<<”Ошибка: недостаточно памяти\п”;

exit(1);

}

if (head==NULL) head=temp_ptr;

else tail–>link=temp_ptr;

temp_ptr–>data=the_number;

temp_ptr–>link=NULL;

tail=temp_ptr;

}

Таким образом, формирование очереди не слишком сильно отличается от формирования стека. Основное отличие состоит в том, что приходится вводить один дополнительный указатель, указывающий на “хвост” очереди.

head

5

head

5

7

NULL

tail

7

tail

рис.5а

head

5

8

NULL

рис.5в

tail

7

8

NULL

temp_ptr

рис.5б

Рис.5 Формирование очереди.

Формирование списка типа “очередь” существенно упрощается и становится ещё более похожим на формирование стека, если известно количество элементов очереди:

void end_ptr(NodePtr &head, int n)

{ NodePtr temp_ptr, tail;

head=NULL;

for (int i=0; i<n; i++)

{ temp_ptr=new Node;

if (temp_ptr==NULL) return temp_ptr;

else

{ temp_ptr–>data=random(10)-5;

temp_ptr–>link=NULL;

if (head==NULL) head=temp_ptr;

else tail–>link=temp_ptr;

tail=temp_ptr;

}

}

temp_ptr–>next=NULL;

return head;

}

Рассмотрим еще несколько часто встречающихся функций для работы со связным списком. При этом совершенно не важно, как именно был сформирован список: по принципу стека или очереди.

Печать списка.

Соседние файлы в папке Lab3