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

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск
(Динамические структуры данных)
(Доступ к памяти, имеющей другое внутреннее представление)
 
(не показаны 94 промежуточные версии 15 участников)
Строка 1: Строка 1:
<small>Лекция 3</small>
+
[[Категория:Основы программирования]]
 +
== Указатели ==
  
===Указатели===
+
=== Адрес ===
Адрес. Переменная-указатель.
+
Оперативная память состоит из последовательный ячеек. Каждая ячейка имеет ''номер'', называемый '''адресом'''.
 +
<br />В 32-битных системах можно адресовать 2<sup>32</sup> байт (<math>\approx \;</math> 4Гб) памяти, в 64-битных — 2 <sup>64</sup> соответственно.
  
Для чего нужны указатели?
+
Переменная (или константа), хранящая адрес, называется '''указателем'''.
  
Типизированные и бестиповые указатели.
+
=== Для чего нужны указатели ===
 +
Указатели повышают '''''гибкость доступа к данным''''':
 +
# Вместо самих данных можно хранить указатель на них. Это позволяет хранить данные в одном экземпляре и множество указателей на эти данные. Через разные указатели эти данные можно обновлять (<u>пример</u> — корпоративная БД).
 +
# Указателю можно присвоить адрес другого объекта (вместо старого появился новый телефонный справочник).
 +
# С помощью указателей можно создавать сложные '''структуры данных'''.
  
Нулевой указатель.
+
=== Типы указателей ===
 +
Указатели делятся на:
 +
* '''''Типизированные''''' (указывают на объект некоторого типа) <br />Имеют тип: <tt>'''^<тип>'''</tt> <br /><u>Пример</u>. <tt>^integer</tt> — указатель на integer
 +
* '''''Бестиповые''''' (хранят адрес ячейки памяти неизвестного типа) <br />''Преимущество:'' могут хранить что угодно <br />Имеют тип: <tt>'''pointer'''</tt>
  
Операция разыменования.
+
<u>''Пример кода''</u>.
 +
<source lang="Pascal">var
 +
  i: integer := 5;
 +
  r: real := 6.14;
 +
 
 +
  pi: ^integer;
 +
  pr: ^real;
  
Указатели и явное приведение типа. Пример: типы pointer, pinteger и preal.
+
begin
Доступ к памяти, имеющей другое внутреннее представление.
+
  pi := @i;
 +
  pr := @r;
 +
  pi := @r; // ОШИБКА компиляции
 +
end.</source>
 +
<tt>'''@'''</tt> — унарная операция '''взятия адреса'''
 +
===Операция разадресации (разыменования)===
 +
<source lang="Pascal">var
 +
  i: integer := 5;
 +
  pi: ^integer;
  
Динамическая память. Явное выделение динамической памяти.
+
begin
Процедуры New и Dispose.
+
  pi := @i;
 +
 
 +
  pi^ := 8 - pi^;
 +
  writeln(i); // 3
 +
end.</source>
 +
<tt>'''^'''</tt> — операция '''разыменования'''
 +
<br /><tt>'''pi^'''</tt> — то, на что указывает pi, т.е. другое имя i или '''ссылка''' на i.
  
Ошибки при работе с динамической памятью
+
Тут надо вспомнить определение ссылки:
* Использование неинициализированного указателя
+
<br />'''Ссылка''' — другое имя объекта.
* Висячие указатели
+
===Нулевой указатель===
* Утечка памяти
+
Все глобальные ''неинициализированные'' указатели хранят специальное значение <tt>'''nil'''</tt>, что говорит о том, что они '''''никуда не указывают'''''.
 +
<br />Указатель, хранящий значение <tt>'''nil'''</tt> называется '''нулевым'''.
 +
<source lang="Pascal">var
 +
  pi: ^integer; //указатель pi хранит значение nil
 +
  i: integer;
  
===Классы-начало===
+
begin
Переменная типа класс как ссылка. Сравнение с записями.
+
  pi := @i;    //pi хранит адрес переменной i
 +
  pi := nil;    //pi снова никуда не указывает
 +
 
 +
  pi^ := 7;    //ОШИБКА времени выполнения:
 +
                //попытка разыменовать нулевой указатель</source>
 +
Попытка разыменовать нулевой указатель приводит к '''''ошибке времени выполнения'''''.
 +
===Бестиповые указатели===
 +
<source lang="Pascal">var
 +
  p: pointer;
 +
  i: integer;
  
Вызов конструктора и выделение динамической памяти.
+
begin
 +
  p := @i;
 +
end.</source>
 +
Бестиповому указателю можно присвоить адрес переменной любого типа, т.е. бестиповой указатель ''совместим по присваиванию с любым типовым'' указателем.
  
Шаблоны классов.
+
Попытка разыменовать бестиповой указатель приводит к '''''ошибке компиляции'''''. Т.е. он может только ''хранить'' адреса.
  
Решение проблемы освобождения памяти, занимаемой объектами классов: сборка мусора (.NET, Java).
+
Оказывается, любой типизированный указатель ''совместим по присваиванию с бестиповым'', т.е. следующий код верен:
 +
<source lang="Pascal">var
 +
  pi: ^integer;
 +
  i: integer;
 +
  p: pointer;
  
Управляемая динамическая память и ее возврат. Отсутствие утечки памяти.  
+
begin
 +
  p := @i;
 +
  pi := p;
 +
  pi^ += 2;
 +
end.</source>
  
===Динамические структуры данных. Списки===
+
''<u>Вопрос</u>''. Нельзя ли интерпретировать память, на которую указывает <tt>p</tt>, как принадлежащую к определенному типу?
Виды списков. Рисунки.
+
<br />''<u>Ответ</u>'' — да, можно. Вот как это сделать:
 +
<source lang="Pascal">type
 +
  pinteger = ^integer;
 +
var
 +
  i, j: integer;
 +
  p: pointer;
  
====Односвязные линейные списки====
+
begin
Класс узла списка (шаблонный)
+
  p := @i;
 +
  pinteger(p)^ := 7; //используем явное приведение типа
 +
  writeln(i); // 7
 +
end.</source>
 +
Запись
 +
''тип''(''переменная'')
 +
показывает, что используется '''явное приведение типов'''.
  
Стандартные операции с односвязными линейными списками
+
<span style="color: red">'''Внимание!''' Неконтролируемая ошибка!</span>
* Вставка элемента в начало
+
<source lang="Pascal">type
* Удаление элемента из начала
+
  preal = ^real;
* Вставка после текущего
+
var
* Удаление после текущего
+
  i, j: integer;
* Проход по списку
+
  p: pointer;
  
====Двусвязные линейные списки====
+
begin
 +
  p := @i;
 +
  preal(p)^ := 3.14; //ОШИБКА!
 +
end.</source>
 +
Область памяти, на которую указывает p трактуется как область, хранящее вещественное число (8 байт), и потому константа 3.14 записывается в эти 8 байт. Однако, переменная i занимает только 4 байта, поэтому затираются еще 4 соседних байта (в данном случае они принадлежат переменной j).
  
Стандартные операции с двусвязными линейными списками
+
=== Доступ к памяти, имеющей другое внутреннее представление ===
* Инициализация
+
<source lang="Pascal">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.</source>
 +
'''Замечание.''' Важно, что типы 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 />Переменная типа динамический массив является указателем на данные массива, хранящиеся в динамической памяти.
 +
 
 +
== Динамическая память ==
 +
=== Особенности динамической памяти ===
 +
Память, принадлежащая программе, делится на:
 +
*'''Статическую''' <br />(память, занимаемая глобальными переменными и константами)
 +
*'''Автоматическую''' <br />(память, занимаемая локальными данными, т.е. стек программы)
 +
*'''Динамическую''' <br />(память, выделяемая программе по специальному запросу)
 +
В дополнение к статической и автоматической памяти, которые фиксированы после запуска программы, программа может получать нефиксированное количество ''динамической'' памяти. Ограничения на объём выделяемой динамической памяти связаны лишь с настройками операционной системы и объемом оперативной памяти компьютера.
 +
<br />Основная проблема — явно выделенную динамическую память необходимо возвращать, иначе не хватит памяти другим программам.
 +
 
 +
Для явного ''выделения'' и ''освобождения'' динамической памяти используются процедуры:
 +
*'''<tt>New</tt>'''
 +
*'''<tt>Dispose</tt>'''
 +
<source lang="Pascal">var
 +
  p: pinteger;  //p никуда не указывает
 +
begin
 +
  New(p);      //в динамической памяти выделяется ячейка
 +
                //размером под один integer, и
 +
                //p начинает указывать на эту ячейку
 +
  p^ := 3;
 +
  Dispose(p);  //возвращает динамическую память,
 +
                //контролируемую указателем p, назад ОС
 +
end.</source>
 +
По окончании работы программы, вся затребованная программой динамическая память возвращается ОС.
 +
<br />'''''Но лучше освобождать динамическую память явно!''''' Иначе в процессе работы программы она может занимать большие объёмы (ещё не освобождённой) памяти, что вредит общей производительности системы.
 +
 
 +
=== Ошибки при работе с динамической памятью ===
 +
1. <source lang="Pascal">var p: pinteger;
 +
begin
 +
  p^ := 5;  //ОШИБКА
 +
end.</source>
 +
Ошибка '''''разыменования нулевого указателя''''' (попытка использовать невыделенную динамическую память).
 +
 
 +
2. <source lang="Pascal">var p: pinteger;
 +
begin
 +
  New(p);
 +
  New(p);  //ОШИБКА
 +
end.</source>
 +
'''''Утечка памяти''''' (память, которая выделилась в результате первого вызова <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;
 +
begin
 +
  for var  i:=1 to 1000000 do
 +
    New(p);  //ОШИБКА
 +
end.</source>
 +
'''''Out of Memory''''' (очень большие утечки памяти, в результате которых динамическая память может «исчерпаться»).
 +
 
 +
4. <source lang="Pascal">var p: pinteger;
 +
begin
 +
  New(p);
 +
  p^ := 5;
 +
  Dispose(p);
 +
  p^ := 7;  //ОШИБКА
 +
end.</source>
 +
После вызова <tt>Dispose(p)</tt>, <tt>p</tt> называют '''''висячим указателем''''' (т.к. он указывает на недоступную более область памяти).

Текущая версия на 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 называют висячим указателем (т.к. он указывает на недоступную более область памяти).