Основы программирования — второй семестр 08-09; Михалкович С.С.; II часть — различия между версиями

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск
(Двусвязные линейные списки)
(Доступ к памяти, имеющей другое внутреннее представление)
 
(не показаны 34 промежуточные версии 9 участников)
Строка 1: Строка 1:
<small>Лекция 3</small>
+
[[Категория:Основы программирования]]
 
 
 
== Указатели ==
 
== Указатели ==
  
Строка 15: Строка 14:
 
# С помощью указателей можно создавать сложные '''структуры данных'''.
 
# С помощью указателей можно создавать сложные '''структуры данных'''.
  
=== Подробнее об указателях ===
+
=== Типы указателей ===
 
Указатели делятся на:
 
Указатели делятся на:
 
* '''''Типизированные''''' (указывают на объект некоторого типа) <br />Имеют тип: <tt>'''^<тип>'''</tt> <br /><u>Пример</u>. <tt>^integer</tt> — указатель на integer
 
* '''''Типизированные''''' (указывают на объект некоторого типа) <br />Имеют тип: <tt>'''^<тип>'''</tt> <br /><u>Пример</u>. <tt>^integer</tt> — указатель на integer
Строка 34: Строка 33:
 
end.</source>
 
end.</source>
 
<tt>'''@'''</tt> — унарная операция '''взятия адреса'''
 
<tt>'''@'''</tt> — унарная операция '''взятия адреса'''
<xh4>Операция разадресации (разыменования)</xh4>
+
===Операция разадресации (разыменования)===
 
<source lang="Pascal">var
 
<source lang="Pascal">var
 
   i: integer := 5;
 
   i: integer := 5;
Строка 50: Строка 49:
 
Тут надо вспомнить определение ссылки:
 
Тут надо вспомнить определение ссылки:
 
<br />'''Ссылка''' — другое имя объекта.
 
<br />'''Ссылка''' — другое имя объекта.
<xh4>Нулевой указатель</xh4>
+
===Нулевой указатель===
 
Все глобальные ''неинициализированные'' указатели хранят специальное значение <tt>'''nil'''</tt>, что говорит о том, что они '''''никуда не указывают'''''.
 
Все глобальные ''неинициализированные'' указатели хранят специальное значение <tt>'''nil'''</tt>, что говорит о том, что они '''''никуда не указывают'''''.
 
<br />Указатель, хранящий значение <tt>'''nil'''</tt> называется '''нулевым'''.
 
<br />Указатель, хранящий значение <tt>'''nil'''</tt> называется '''нулевым'''.
Строка 64: Строка 63:
 
                 //попытка разыменовать нулевой указатель</source>
 
                 //попытка разыменовать нулевой указатель</source>
 
Попытка разыменовать нулевой указатель приводит к '''''ошибке времени выполнения'''''.
 
Попытка разыменовать нулевой указатель приводит к '''''ошибке времени выполнения'''''.
<xh4>Бестиповые указатели</xh4>
+
===Бестиповые указатели===
 
<source lang="Pascal">var
 
<source lang="Pascal">var
 
   p: pointer;
 
   p: pointer;
Строка 102: Строка 101:
 
end.</source>
 
end.</source>
 
Запись  
 
Запись  
  '''<тип>'''( <переменная> )
+
  ''тип''(''переменная'')
 
показывает, что используется '''явное приведение типов'''.
 
показывает, что используется '''явное приведение типов'''.
  
 
<span style="color: red">'''Внимание!''' Неконтролируемая ошибка!</span>
 
<span style="color: red">'''Внимание!''' Неконтролируемая ошибка!</span>
 
<source lang="Pascal">type  
 
<source lang="Pascal">type  
   pinteger = ^integer;
+
   preal = ^real;
 
var
 
var
 
   i, j: integer;
 
   i, j: integer;
Строка 117: Строка 116:
 
end.</source>
 
end.</source>
 
Область памяти, на которую указывает p трактуется как область, хранящее вещественное число (8 байт), и потому константа 3.14 записывается в эти 8 байт. Однако, переменная i занимает только 4 байта, поэтому затираются еще 4 соседних байта (в данном случае они принадлежат переменной j).
 
Область памяти, на которую указывает p трактуется как область, хранящее вещественное число (8 байт), и потому константа 3.14 записывается в эти 8 байт. Однако, переменная i занимает только 4 байта, поэтому затираются еще 4 соседних байта (в данном случае они принадлежат переменной j).
== Доступ к памяти, имеющей другое внутреннее представление ==
+
 
 +
=== Доступ к памяти, имеющей другое внутреннее представление ===
 
<source lang="Pascal">type
 
<source lang="Pascal">type
 
   Rec = record
 
   Rec = record
Строка 131: Строка 131:
 
begin
 
begin
 
   var temp : pointer := @r;
 
   var temp : pointer := @r;
   prec := PRecord(temp); //Явное приведение типа
+
   prec := temp;  
 
   writeln(prec^.b1, ' ', prec^.b2, ' ', {..., } prec^.b8);
 
   writeln(prec^.b1, ' ', prec^.b2, ' ', {..., } prec^.b8);
 
end.</source>
 
end.</source>
 
'''Замечание.''' Важно, что типы real и Rec имеют один размер.
 
'''Замечание.''' Важно, что типы real и Rec имеют один размер.
 +
 +
Другой способ сделать то же самое, но гораздо более безопасный - использовать класс System.BitConverter
 +
<source lang="Delphi">
 +
uses System;
 +
 +
begin
 +
  foreach b: byte in BitConverter.GetBytes(1.0) do
 +
    write(b,' ');
 +
end.</source>
 +
 +
=== Неявные указатели в языке Pascal ===
 +
# <tt>procedure p('''var''' i: integer)</tt> <br />Для параметра-переменной при вызове на стек кладется не сама переменная, а указатель на неё.
 +
# <tt>'''var''' pp: procedure(i: integer)</tt> <br />Для хранения процедурной переменной используется ячейка памяти, являющаяся указателем.
 +
# '''var''' a: '''array of''' real; <br />Переменная типа динамический массив является указателем на данные массива, хранящиеся в динамической памяти.
  
 
== Динамическая память ==
 
== Динамическая память ==
Строка 156: Строка 170:
 
   p^ := 3;
 
   p^ := 3;
 
   Dispose(p);  //возвращает динамическую память,  
 
   Dispose(p);  //возвращает динамическую память,  
                 //контролируемую указателем p, назад ОС
+
                 //контролируемую указателем p, назад ОС
 
end.</source>
 
end.</source>
 
По окончании работы программы, вся затребованная программой динамическая память возвращается ОС.
 
По окончании работы программы, вся затребованная программой динамическая память возвращается ОС.
Строка 174: Строка 188:
 
end.</source>
 
end.</source>
 
'''''Утечка памяти''''' (память, которая выделилась в результате первого вызова <tt>New(p)</tt>, принадлежит программе, но не контролируется никаким указателем.
 
'''''Утечка памяти''''' (память, которая выделилась в результате первого вызова <tt>New(p)</tt>, принадлежит программе, но не контролируется никаким указателем.
 +
 +
2a. <source lang="Pascal">
 +
procedure q;
 +
var p: pinteger;
 +
begin
 +
  New(p);
 +
end;
 +
begin
 +
  q;  //ОШИБКА
 +
end.</source>
 +
Утечка памяти в подпрограмме: обычно если динамическая память выделяется в подпрограмме, то она должна в этой же подпрограмме возвращаться. Исключение составляют т.н. "создающие" п/п:
 +
 +
<source lang="Pascal">
 +
function CreateInteger: pinteger;
 +
begin
 +
  New(Result);
 +
end;
 +
 +
begin
 +
  var p: pinteger := CreateInteger;
 +
  p^ := 555;
 +
  Dispose(p);
 +
end.</source>
 +
Ответственность за удаление памяти, выделенной в подпрограмме, лежит на программисте, вызвавшем эту подпрограмму.
  
 
3. <source lang="Pascal">var p: pinteger;
 
3. <source lang="Pascal">var p: pinteger;
Строка 190: Строка 228:
 
end.</source>
 
end.</source>
 
После вызова <tt>Dispose(p)</tt>, <tt>p</tt> называют '''''висячим указателем''''' (т.к. он указывает на недоступную более область памяти).
 
После вызова <tt>Dispose(p)</tt>, <tt>p</tt> называют '''''висячим указателем''''' (т.к. он указывает на недоступную более область памяти).
 
== Неявные указатели в языке Pascal ==
 
# <tt>procedure p('''var''' i: integer)</tt> <br />Для параметра-переменной при вызове на стек кладется не сама переменная, а указатель на неё.
 
# <tt>'''var''' pp: procedure(i: integer)</tt> <br />Для хранения процедурной переменной используется ячейка памяти, являющаяся указателем.
 
# '''var''' a: '''array of''' real; <br />Переменная типа динамический массив является указателем на данные массива, хранящиеся в динамической памяти.
 
== Введение в классы ==
 
=== От записей к классам ===
 
Рассмотрим запись студент:
 
<source lang="Pascal">type
 
  Student = record
 
    name: string;
 
    age: integer;
 
   
 
    procedure Init(n: string; a: integer);
 
    begin
 
      name := n;
 
      age := a;
 
    end;
 
   
 
    procedure Print;
 
    begin
 
      writelnFormat('Имя: {0} Возраст: {1}',
 
                    name, age);
 
    end;
 
  end;
 
 
var
 
  s: student;
 
begin
 
  s.Init('Иванов', 18);
 
end.</source>
 
Когда мы описываем переменную типа Student, она кладется на программный стек.
 
 
А вот так выглядит класс Student:
 
<source lang="Pascal">type
 
  Student = class
 
    name: string;
 
    age: integer;
 
   
 
    constructor Create(n: string; a: integer);
 
    begin
 
      name := n;
 
      age := a;
 
    end;
 
   
 
    procedure Print;
 
    begin
 
      writelnFormat('Имя: {0} Возраст: {1}',
 
                    name, age);
 
    end;
 
  end;
 
 
var
 
  s: student;
 
begin
 
  s := New Student('Иванов', 18);
 
end.</source>
 
Переменная типа класс является указателем. Для выделения динамической памяти под объект класса Student используется вызов специального метода, называемого '''конструктором''' (<tt>'''New''' Student(<имя>, <возраст>)</tt>).
 
 
<small>Лекция 4</small>
 
=== Шаблоны классов ===
 
<source lang="Pascal">type
 
  Point<T> = class
 
    x, y: T;
 
 
    constructor(_x, _y: T);
 
    begin
 
      x := _x;
 
      y := _y;
 
    end;
 
  end;
 
 
begin
 
  var p1 : Point<real>;
 
  var p2 : Point<integer>;
 
 
  p1 := new Point<real>(2.3, 5.7);
 
  p2 := new Point<integer>(2, 7);
 
 
 
  p1.x := 2;
 
  p2.x := 1.5; // ошибка компиляции, т.к.
 
              // тип поля x объекта класса Point<integer> (integer)
 
              // не совместим по присваиваиванию с вещественным типом real
 
 
 
  //можем пользоваться автоопределением типов:
 
  var p3 := new Point<real>(3.14, 7.9);
 
end.</source>
 
Тип <tt>Point<T></tt> называют '''обощенным''' типом.
 
 
Теперь рассмотрим присваивание объектов класса:
 
<source lang="Pascal">var p11 := new Point<real>(0, 1.5);
 
var p12 := new Point<real>(2.3, 5);
 
 
p11 := p12;  // происходит присваивание ссылок:
 
            // p11 теперь указывает на тот же объект, что и p12;
 
            // область памяти, на которую до этого указывал p11 более недоступна
 
 
var p2 := new Point<integer>(5, 7);
 
p11 := p2;  // ошибка
 
            // типы объектов не совпадают</source>
 
При присваивании же друг другу записей, происходит копирование самих записей.
 
 
=== Сборка мусора ===
 
('''Garbage collection''')
 
 
'''Мусор''' — любые ненужные объекты. 
 
<br />Под '''''ненужными''''' понимаются объекты, которые занимают память, но недоступны в программе.
 
 
'''Сборка мусора''' — процесс освобождения памяти, занятой ненужными объектами.
 
 
Обычно, сборка мусора запускается при нехватке места в памяти. Это позволяет не заботиться об ''утечках памяти'', т.к. все выделенное — освободится. (При этом, от всех остальных ошибок мы не застрахованы!!!)
 
<br />Главный недостаток механизма сборки мусора — во время сборки выполнение программы приостанавливается.
 
== Динамические структуры данных ==
 
=== Введение ===
 
Сейчас '''структуры данных''' занимают в программировании все более важную позицию.
 
<br />Мы уже знаем такие структуры, как:
 
*''массивы'' (подразумеваем статические)
 
*''записи''
 
Их основная проблема — фиксированный размер, определяемый на этапе компиляции.
 
<br />Решением проблемы являются '''динамические структуры данных'''. Они строятся из '''узлов''', которые, в свою очередь, состоят из '''''данных''''' и '''''полей связи'''''.
 
 
Рассмотрим такой пример:
 
<source lang="Pascal">type
 
  Node<DataType> = class
 
    data: DataType;
 
    next: Node<DataType>;
 
   
 
    constructor (d: DataType; n: Node<DataType>);
 
    begin
 
      data := d;
 
      next := n;
 
    end;
 
  end;
 
 
begin
 
  var p := new Node<char>('!', nil);  // под объект класса Node<char> выделилась динамическая память;
 
                                      // p начала указывать на эту динамическую память
 
end.</source>
 
 
=== Виды списков ===
 
# ''Линейный односвязный список''<br>[[Изображение:Линейный_односвязный_список.png]]
 
# ''Циклический односвязный список''<br>[[Изображение:Циклический_односвязный_список.png]]
 
# ''Двусвязный линейный список''<br> [[Изображение:Двусвязный_линейный_список.png|Двусвязный_линейный_список.png]]
 
# ''Циклический двусвязный список''<br>[[Изображение:Циклический_двусвязный_список.png]]
 
=== Односвязные линейные списки ===
 
<source lang="Pascal">type
 
  Node<DataType> = class
 
    data: DataType;
 
    next: Node<DataType>;
 
   
 
    constructor (d: DataType; n: Node<DataType>);
 
    begin
 
      data := d;
 
      next := n;
 
    end;
 
  end;
 
 
begin
 
  {Пусть у нас есть указатель на начало списка:}
 
  var head: Node<char>;
 
  // инициализация head</source>
 
 
<xh4>Стандартные операции с односвязными линейными списками</xh4>
 
* '''''Вставка элемента в начало'''''<br />
 
[[Изображение:Добавление_элемента_в_начало_линейного_односвязного_списка.gif]]<br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">head := new Node<char>('A', head);</source>
 
При многократной вставке в начало элементы располагаются в ''обратном порядке''.}}
 
 
* '''''Удаление элемента из начала'''''<br />
 
[[Изображение:Удаление начального элемента.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">head := head.next;</source>
 
Если изначально список пуст, произойдет ошибка<span style="color: red"> «попытка разыменования нулевого указателя»</span>. Эту ситуацию надо предусмотреть:
 
<source lang="Pascal">if head <> nil then
 
  head := head.next;</source>}}
 
 
* '''''Вставка элемента после текущего''''' <br />
 
[[Изображение:Вставка элемента после текущего.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">cur.next := new Node<char>('C', cur.next);</source>
 
Если <tt>cur</tt> никуда не указывает, произойдет ошибка. Предусмотрим эту ситуацию:
 
<source lang="Pascal">if cur <> nil then
 
  cur.next := new Node<char>('d', cur.next);</source>
 
Заметим также, что если <tt>cur</tt> указывает на последний элемент списка, ошибки не произойдет (фактически, будет произведена ''вставка в конец списка'').}}
 
 
* '''''Удаление элемента после текущего''''' <br />
 
[[Изображение:Удаление после текущего.gif]]<br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">cur.next := cur.next.next;</source> <br />
 
 
Заметим, что текущий элемент — <tt>cur</tt>, должен не только не быть пустым, но и не быть последним в списке, т.к. происходят два разыменования: <tt>'''cur.'''next</tt> и <tt>'''cur.next.'''next</tt>. Для проверки этого факта можем воспользоваться утверждением:
 
<source lang="Pascal">Assert( (cur <> nil) and (cur.next <> nil) );
 
cur.next := cur.next.next;</source>
 
}}
 
 
* '''''Проход по списку''''' <br />
 
[[Изображение:Проход_по_списку.gif]]<br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">var cur := head;
 
while cur <> nil do
 
begin
 
  writeln(cur.data);
 
  cur := cur.next;
 
end;</source> }}
 
 
<xh4>Примеры использования</xh4>
 
''<u>Пример 1</u>''.
 
<br />Дан файл целых чисел. <br />Записать все его элементы в линейный односвязный список.
 
<source lang="Pascal">var f: file of integer;
 
Assign(f, 'numbers.dat');
 
Reset(f);
 
 
var a: integer;
 
Read(f, a);
 
var head := new Node<integer>(a, nil);
 
 
var cur := head;
 
while not Eof(f) do
 
begin
 
  read(f, a);
 
  cur.next := new Node<integer>(a, nil);
 
  cur := cur.next;
 
end;
 
 
Close(f);</source>
 
''<u>Пример 2</u>''.
 
<br />Поиск элемента с заданным значением.
 
<source lang="Pascal">// x — искомый символ
 
var cur := head;
 
while (cur <> nil) and (cur.data <> x) do
 
  cur := cur.next;
 
 
if cur = nil then
 
  // не найдено
 
else
 
  // cur — ссылка на искомый x</source>
 
 
===Двусвязные линейные списки===
 
<xh4>Введение</xh4>
 
В отличие от односвязных линейных списков, двусвязные, помимо полей <tt>data</tt> и <tt>next</tt>, имеют поле <tt>prev</tt> (указатель на предыдущий элемент списка):
 
<source lang="Pascal">type
 
  Node<T> = class
 
    data: T;
 
    prev, next: Node<T>;
 
   
 
    constructor (d: T;  p, n: Node<T>);
 
    begin
 
      data := d;
 
      prev := p;
 
      next := n;
 
    end;
 
  end;</source>
 
В случае двусвязного списка нам достаточно иметь ссылку на любой узел, тогда все остальные можно найти. Однако, для удобства, будем считать, что у нас есть две ссылки:
 
* <tt>head</tt> — на начало списка
 
* <tt>tail</tt> — на конец списка
 
<xh4>Стандартные операции с двусвязными линейными списками</xh4>
 
'''Замечание.''' При выполнении любой операции нужно следить за возможными изменениями <tt>head</tt> и <tt>tail</tt>.
 
* '''''Инициализация''''' <br />
 
<source lang="Pascal">head := nil;
 
tail := nil;</source>
 
 
* '''''Добавление элемента в начало''''' <br />
 
[[Изображение:Вставка_в_начало.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = '''Примечание.''' Если изначально список был пуст, после добавления элемента надо не забыть сделать <tt>tail</tt> указывающим на него.
 
<source lang="Pascal">head := new Node<T>(0, nil, head);
 
if head.next <> nil then
 
  head.next.prev := head
 
else  // если список был пуст
 
  tail := head;</source>}}
 
 
* '''''Добавление элемента в  конец''''' <br />
 
[[Изображение:Вставка_в_конец.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">tail := new Node<T>(2, tail, nil);
 
if tail.prev <> nil then
 
  tail.prev.next := tail
 
else  // если список был пуст
 
  head := tail;</source> }}
 
 
* '''''Удаление элемента из начала''''' <br />
 
[[Изображение:Удаление_из_начала_д.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">head := head.next;
 
if head = nil then
 
  tail := nil
 
else 
 
  head.prev := nil;</source> }}
 
 
* '''''Удаление элемента из конца''''' <br />
 
[[Изображение:Удаление_из_конца_д.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">tail := tail.prev;
 
if tail = nil then
 
  head := nil
 
else
 
  tail.next := nil;</source> }}
 
 
* '''''Вставка элемента перед текущим''''' <br />
 
[[Изображение:Вставка_перед_текущим.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">if cur = head then
 
  // вставка в начало
 
else
 
begin
 
  cur.prev := new Node<T>(3, cur.prev, cur);
 
  cur.prev.prev.next := cur.prev;
 
end;</source>}}
 
 
* '''''Вставка элемента после текущего''''' <br />
 
[[Изображение:Вставка_после_текущего.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">if cur = tail then
 
  // вставка в конец
 
else
 
begin
 
  cur.next := new Node<T>(3, cur, cur.next);
 
  cur.next.next.prev := cur.next;
 
end;</source> }}
 
 
* '''''Удаление текущего''''' <br />
 
[[Изображение:Удаление_текущего.gif]] <br />
 
{{Hider
 
|title = Код
 
|content = <source lang="Pascal">if cur = head then
 
  // удаление из начала
 
else if cur = tail then
 
  // удаление из конца
 
else
 
begin
 
  cur.prev.next := cur.next;
 
  cur.next.prev := cur.prev;
 
  cur := cur.next;
 
end;</source>}}
 
 
* '''''Проход по списку''''' <br />
 
Проход по списку в ''прямом порядке'' аналогичен этой операции для односвязных списков.
 
<br />Проход в ''обратном порядке'' можно организовать заменой:
 
*<tt>head</tt> на <tt>tail</tt>
 
*<tt>next</tt> на <tt>prev</tt>
 
 
<xh4>Двусвязный линейный список как класс</xh4>
 
Ясно, что удобно оформить все операции в виде подпрограмм. Но тогда каждый раз в качестве параметров надо передавать ссылки на начало и конец списка.
 
<br />Создадим класс ''двусвязный линейный список'', полями которого будут <tt>head</tt> и <tt>tail</tt>:
 
<source lang="Pascal">type
 
  Node<T> = class
 
    data: T;
 
    prev, next: Node<T>;
 
   
 
    constructor (d: T;  p, n: Node<T>);
 
    begin
 
      data := d;
 
      prev := p;
 
      next := n;
 
    end;
 
  end;
 
 
  DoubleLinkedList<T> = class
 
    head, tail: Node<T>;
 
   
 
    constructor;
 
    begin
 
      head := nil;
 
      tail := nil;
 
    end;
 
 
    procedure AddFirst(d: T);
 
    begin
 
      head := new Node<T>(d, nil, head);
 
      if head.next <> nil then
 
        head.next.prev := head;
 
      else  // если список был пуст
 
        tail := head;
 
    end;
 
    procedure AddLast(d: T);
 
    begin
 
      tail := new Node<T>(d, tail, nil);
 
      if tail.prev <> nil then
 
        tail.prev.next := tail
 
      else  // если список был пуст
 
        head := tail;
 
    end;
 
 
    procedure DeleteFirst;
 
    begin
 
      head := head.next;
 
      if head = nil then
 
        tail := nil
 
      else 
 
        head.prev := nil;
 
    end;
 
    procedure DeleteLast;
 
    begin
 
      tail := tail.prev;
 
      if tail = nil then
 
        head := nil
 
      else
 
        tail.next := nil;
 
    end;
 
 
    procedure InsertBefore(cur: Node<T>; d: T);
 
    begin
 
      if cur = head then
 
        AddFirst(d);
 
      else
 
      begin
 
        cur.prev := new Node<T>(d, cur.prev, cur);
 
        cur.prev.prev.next := cur.prev;
 
      end;
 
    end;
 
    procedure InsertAfter(cur: Node<T>; d: T);
 
    begin
 
      if cur = tail then
 
       
 
      else
 
      begin
 
        cur.next := new Node<T>(d, cur, cur.next);
 
        cur.next.next.prev := cur.next;
 
      end;
 
    end;
 
 
    function RemoveAt(cur: Node<T>): Node<T>;
 
    begin
 
      if cur = head theb
 
        DeleteFirst
 
      else if cur = tail then
 
        DeleteLast
 
      else
 
      begin
 
        cur.prev.next := cur.next;
 
        cur.next.prev := cur.prev;
 
        cur := cur.next;
 
      end;
 
    end;
 
 
    procedure Print;
 
    begin
 
      var cur := head;
 
      while cur <> nil do
 
      begin
 
        writeln(cur.data);
 
        cur := cur.next;
 
      end;
 
    end;
 
    procedure PrintReverse;
 
    begin
 
      var cur := tail;
 
      while cur <> nil do
 
      begin
 
        writeln(cur.data);
 
        cur := cur.prev;
 
      end;
 
    end;</source>
 
''<u>Пример</u>''.
 
<br />Дан двусвязный линейный список с целыми значениями. <br />Удалить все его отрицательные элементы.
 
<source lang="Pascal">var list: DoublyLinkedList<integer>;
 
// создание списка
 
 
var cur := list.head;
 
while cur <> nil do
 
  if cur.data < 0 then
 
    cur := list.RemoveAt(cur);</source>
 

Текущая версия на 10:29, 18 февраля 2013

Указатели

Адрес

Оперативная память состоит из последовательный ячеек. Каждая ячейка имеет номер, называемый адресом.
В 32-битных системах можно адресовать 232 байт (<math>\approx \;</math> 4Гб) памяти, в 64-битных — 2 64 соответственно.

Переменная (или константа), хранящая адрес, называется указателем.

Для чего нужны указатели

Указатели повышают гибкость доступа к данным:

  1. Вместо самих данных можно хранить указатель на них. Это позволяет хранить данные в одном экземпляре и множество указателей на эти данные. Через разные указатели эти данные можно обновлять (пример — корпоративная БД).
  2. Указателю можно присвоить адрес другого объекта (вместо старого появился новый телефонный справочник).
  3. С помощью указателей можно создавать сложные структуры данных.

Типы указателей

Указатели делятся на:

  • Типизированные (указывают на объект некоторого типа)
    Имеют тип: ^<тип>
    Пример. ^integer — указатель на integer
  • Бестиповые (хранят адрес ячейки памяти неизвестного типа)
    Преимущество: могут хранить что угодно
    Имеют тип: pointer

Пример кода.

var
  i: integer := 5;
  r: real := 6.14;
  
  pi: ^integer;
  pr: ^real;

begin
  pi := @i;
  pr := @r;
  pi := @r; // ОШИБКА компиляции
end.

@ — унарная операция взятия адреса

Операция разадресации (разыменования)

var
  i: integer := 5;
  pi: ^integer;

begin
  pi := @i;
  
  pi^ := 8 - pi^;
  writeln(i); // 3
end.

^ — операция разыменования
pi^ — то, на что указывает pi, т.е. другое имя i или ссылка на i.

Тут надо вспомнить определение ссылки:
Ссылка — другое имя объекта.

Нулевой указатель

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

var
  pi: ^integer; //указатель pi хранит значение nil
  i: integer;

begin
  pi := @i;     //pi хранит адрес переменной i
  pi := nil;    //pi снова никуда не указывает
  
  pi^ := 7;     //ОШИБКА времени выполнения:
                //попытка разыменовать нулевой указатель

Попытка разыменовать нулевой указатель приводит к ошибке времени выполнения.

Бестиповые указатели

var
  p: pointer;
  i: integer;

begin
  p := @i;
end.

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

Попытка разыменовать бестиповой указатель приводит к ошибке компиляции. Т.е. он может только хранить адреса.

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

var 
  pi: ^integer;
  i: integer;
  p: pointer;

begin
  p := @i;
  pi := p;
  pi^ += 2;
end.

Вопрос. Нельзя ли интерпретировать память, на которую указывает p, как принадлежащую к определенному типу?
Ответ — да, можно. Вот как это сделать:

type 
  pinteger = ^integer;
var
  i, j: integer;
  p: pointer;

begin
  p := @i;
  pinteger(p)^ := 7; //используем явное приведение типа
  writeln(i); // 7
end.

Запись

тип(переменная)

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

Внимание! Неконтролируемая ошибка!

type 
  preal = ^real;
var
  i, j: integer;
  p: pointer;

begin
  p := @i;
  preal(p)^ := 3.14; //ОШИБКА!
end.

Область памяти, на которую указывает p трактуется как область, хранящее вещественное число (8 байт), и потому константа 3.14 записывается в эти 8 байт. Однако, переменная i занимает только 4 байта, поэтому затираются еще 4 соседних байта (в данном случае они принадлежат переменной j).

Доступ к памяти, имеющей другое внутреннее представление

type
  Rec = record
    b1, b2, b3, b4, b5, b6, b7, b8: byte;
  end;

  PRecord = ^Rec;

var
  r: real := 3.1415;
  prec: ^Rec;

begin
  var temp : pointer := @r;
  prec := temp; 
  writeln(prec^.b1, ' ', prec^.b2, ' ', {..., } prec^.b8);
end.

Замечание. Важно, что типы real и Rec имеют один размер.

Другой способ сделать то же самое, но гораздо более безопасный - использовать класс System.BitConverter

uses System;

begin
  foreach b: byte in BitConverter.GetBytes(1.0) do
    write(b,' ');
end.

Неявные указатели в языке Pascal

  1. procedure p(var i: integer)
    Для параметра-переменной при вызове на стек кладется не сама переменная, а указатель на неё.
  2. var pp: procedure(i: integer)
    Для хранения процедурной переменной используется ячейка памяти, являющаяся указателем.
  3. var a: array of real;
    Переменная типа динамический массив является указателем на данные массива, хранящиеся в динамической памяти.

Динамическая память

Особенности динамической памяти

Память, принадлежащая программе, делится на:

  • Статическую
    (память, занимаемая глобальными переменными и константами)
  • Автоматическую
    (память, занимаемая локальными данными, т.е. стек программы)
  • Динамическую
    (память, выделяемая программе по специальному запросу)

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

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

  • New
  • Dispose
var
  p: pinteger;  //p никуда не указывает
begin
  New(p);       //в динамической памяти выделяется ячейка
                //размером под один integer, и 
                //p начинает указывать на эту ячейку
  p^ := 3;
  Dispose(p);   //возвращает динамическую память, 
                //контролируемую указателем p, назад ОС
end.

По окончании работы программы, вся затребованная программой динамическая память возвращается ОС.
Но лучше освобождать динамическую память явно! Иначе в процессе работы программы она может занимать большие объёмы (ещё не освобождённой) памяти, что вредит общей производительности системы.

Ошибки при работе с динамической памятью

1.
var p: pinteger;
begin
  p^ := 5;  //ОШИБКА
end.

Ошибка разыменования нулевого указателя (попытка использовать невыделенную динамическую память).

2.
var p: pinteger;
begin
  New(p);
  New(p);  //ОШИБКА
end.

Утечка памяти (память, которая выделилась в результате первого вызова New(p), принадлежит программе, но не контролируется никаким указателем.

2a.
procedure q;
var p: pinteger;
begin
  New(p);
end;
begin
  q;  //ОШИБКА
end.

Утечка памяти в подпрограмме: обычно если динамическая память выделяется в подпрограмме, то она должна в этой же подпрограмме возвращаться. Исключение составляют т.н. "создающие" п/п:

function CreateInteger: pinteger;
begin
  New(Result);
end;

begin
  var p: pinteger := CreateInteger;
  p^ := 555;
  Dispose(p);
end.

Ответственность за удаление памяти, выделенной в подпрограмме, лежит на программисте, вызвавшем эту подпрограмму.

3.
var p: pinteger;
begin
  for var  i:=1 to 1000000 do
    New(p);  //ОШИБКА
end.

Out of Memory (очень большие утечки памяти, в результате которых динамическая память может «исчерпаться»).

4.
var p: pinteger;
begin
  New(p);
  p^ := 5;
  Dispose(p);
  p^ := 7;  //ОШИБКА
end.

После вызова Dispose(p), p называют висячим указателем (т.к. он указывает на недоступную более область памяти).