|
||||||||
Глава VII. Заключительная часть. Если вы дочитали книгу, до данного параграфа, изучили все приведённые примеры, выполнили все предложенные задания, то к настоящему времени вы должны были узнать практически все возможности языка PascalABC.NET и получить хороший опыт программирования. Поэтому дальнейшее ваше развитие по большей части будет заключаться в изучении уже готовых классов, которые предоставляют разработчики программных продуктов для программирования. В PascalABC.NET существует ряд классов, которыми вы можете пользоваться. Если подробно расписывать, как с ними работать, то может получиться ещё одна книга. Поэтому в рамках данного учебника будет проведён просто их обзор с небольшими примерами. Их изучение вы должны провести самостоятельно, т.к. в справочной системе они достаточно хорошо и понятно описаны. Думаю, что если вы не просто прочитали учебник, а выполнили самостоятельно все предложенные задания, то изучение этих классов не составит вам труда. Данная глава является заключительной. Поэтому прежде чем сделать обзор нам необходимо изучить ещё две оставшиеся темы – это «Динамическая память» и «Исключения». Возможно, на первых порах вы не будете пользоваться данными темами, однако если вы станете настоящими программистоми, то будете применять их постоянно. В любом случае, вы должны иметь представление об этих темах и уметь применять их на практике. §30. Динамическая память. Динамическая память, указатели. Все переменные, которые мы до сих пор создавали, хранятся в том же участке памяти, который отводится под память программы. Если нам понадобиться переменная, требующая большой объём памяти, то её необходимо хранить уже отдельно от программы, т.к. под программу, как правило, отводится не так много места. То, что было сказано в предыдущем абзаце, не совсем точно. Однако на данном этапе развития можно воспринимать ситуацию с местоположением переменных именно таким образом. Память, не занятая программой и в которой можно хранить переменные, называется динамической. Для того, что бы создать переменную, хранящуюся в динамической памяти отдельно от самой программы, используется указатель. Указатель – это переменная, хранящая в себе адрес кусочка оперативной памяти и его размер. Размер кусочка этой памяти соответствует типу переменной хранящейся в данном кусочке. Другими словами указатель указывает на какую-либо переменную, хранящуюся в оперативной памяти. Отсюда и название «указатель». Объявляется указатель так же как и обычная переменная в разделе объявления переменных или в теле программы следующим образом: var pi: ^integer; Как видно из кода отличие составляет только символ «^» перед типом. Так же стоит сказать, что перед именем указателя, как правило, но не обязательно, ставится префикс p (от слова pointer-указатель). Так как указатель – переменная, хранящая в себе адрес кусочка памяти, то при обращении к ней мы получим как раз этот адрес. Однако просто после объявления указатель ещё не на что не указывает, и вместо адреса мы получим константу nil: begin writeln(pi); end. _____________________ nil Константа nil означает, что указатель на данный момент пуст и ни на что не указывает. Так же если мы присвоим указателю эту константу, то мы его освободим, и он перестанет на что-либо указывать. Однако из-за ряда причин таким способом пользоваться не рекомендуется. Для освобождения указателя существует специальная процедура, о которой будет сказано позже. Для того, что бы заполнить указатель, и он начал указывать на определённый кусок оперативной памяти, эту память нужно выделить специальной процедурой new. В качестве параметра ей нужно передать как раз нужный нам указатель. Т.е. данная процедура выделяет кусочек памяти и присваивает адрес этого кусочка указателю, который передан в качестве параметра: begin new(pi); writeln(pi); end. _________________ $181FF8 В последней строчке примера находится не понятная строка. Здесь стоит сделать небольшое отступление. Каждая ячейка любой компьютерной памяти имеет свой порядковый номер. Именно этот номер и является её адресом. Если в компьютере 4 Гб оперативной памяти, то соответственно последняя ячейка будет иметь адрес, который можно обозначить десятизначным десятеричным числом. Т.е. если выражать адреса десятеричными числами, то придётся оперировать со слишком большим количеством цифр. Понятно, что это неудобно. Поэтому, как правило, все адреса выражаются шестнадцатеричными числами. Они несколько короче десятеричных. О том, что такое десятеричное или шестнадцатеричное число, надеюсь, вы знаете. Так вот в последней строке примера как раз находится шестнадцатеричное число, которое означает адрес выделенного кусочка памяти. О том, что это именно шестнадцатеричное число, а не какое-либо другое, говорит символ «$», стоящий перед числом. На том, что обозначают буквы в таких числах, здесь останавливаться не будем. Двигаемся дальше. Для того, что бы освободить указатель, и соответственно очистить оперативную память, используется процедура dispose: begin new(pi); writeln(pi); dispose(pi); writeln(pi); end. _________________ $17A110 nil Мы научились выделять и освобождать кусочки оперативной памяти, теперь осталось научиться записывать туда данные и считывать их оттуда. Для этого используется так называемая операция разыменования. В коде программы для разыменования указателя необходимо после его имени поставить символ «^»: begin new(pi); pi^:=30; writeln(pi^); end. ________________ 30 Как видно работа с разыменованным указателем аналогична работе с обычной переменной. Указатели в PascalABC.NET делятся на типизированные и бестиповые. До сих пор мы изучали и работали с типизированными указателями, т.е. с ними вы уже знакомы. В отличии от них бестиповые указатели содержат в себе только адрес. Для их объявления используется слово pointer: var p:pointer; Операция разименования к такому указателю не применима. Значение одного указателя можно присвоить другому. Так же указатели можно сравнивать друг с другом на равенство (=) или (<>), для того, что бы определить указывают ли они на один объект. Далее пример: var i:integer; p1,p2: ^integer; begin i:=3; p1:=@i; p2:=p1; if p1=p2 then writeln('Указатели указывают на одну и ту же переменную'); end. _________________________________________________________________________ Указатели указывают на одну и ту же переменную Теперь стоит сказать про операцию @, которая возвращает адрес объекта. Если поставить символ «@» пред именем переменной, то мы получим не значение этой переменной, а адрес кусочка памяти, в котором находится эта переменная. Этот адрес можно присвоить указателю и работать с переменной через указатель. Делается это следующим образом: var i:integer; p: ^integer; begin i:=3; p:=@i; writeln(p^); end. ________________ 3 Работать с переменной через указатель удобно, например, в случае, если подпрограмма в результате своей работы должна изменить значения напрямую одной или нескольких локальных переменных, увидеть которые напрямую подпрограмма не может. В таком случае удобно в качестве параметров передать подпрограмме адреса этих переменных. Пример: procedure P(p1,p2:^integer); begin p1^:=Random(1,100); p2^:=Random(1,100); end; begin var i1,i2:integer; P(@i1,@i2); writeln('i1=',i1,' i2=',i2); end. Такой подход использовался раньше в старых версиях языка Pascal. Теперь в PascalABC.NET он не актуален, т.к. вместо указателей можно использовать слово var при объявлении подпрограммы: procedure P(var ii1,ii2:integer); begin ii1:=Random(1,100); ii2:=Random(1,100); end; begin var i1,i2:integer; P(i1,i2); writeln('i1=',i1,' i2=',i2); end. С таким подходом вы уже хорошо знакомы. И в своих программах будете поступать именно так. Тем не менее мне хотелось что б вы знали и устаревший принцип, потому что в дальнейшем вам возможно придётся работать с какой-нибудь старой версией языка программирования. В принципе про указатели сказать больше нечего. Ещё необходимо сказать несколько слов о динамической памяти. Само название говорит само за себя. Слово динамическая – значит меняющаяся во времени. Объём памяти, который выделен под программу, не меняется во время работы программы. Объём выделенной памяти под переменные, на которые указывают указатели может меняться во время работы программы, соответственно такую память называют динамической. Теперь, когда мы рассмотрели данную тему, хочется поговорить об объектах. Для создания объекта необходимо вначале объявить переменную определённого класса, а затем вызвать конструктор этого класса. Так обстоит дело потому, что объекты хранятся не в памяти программы, а в динамической памяти. В памяти программы хранится переменная типа данного класса, а сам объект созданный конструктором, хранится именно в динамической памяти. По своей сути переменная типа класса является указателем на объект. Думаю, после этих слов вам стало ясно, почему при создании объекта необходимо помимо объявления переменной ещё создать объект с помощью конструктора. У вас может возникнуть уместный вопрос, если объект можно создать в теле программы, то возможно ли его уничтожить так же в теле программы? На самом деле в любом классе есть метод, который называется деструктором. Он как раз и должен заниматься уничтожением объекта. Однако в PascalABC.NET из-за некоторых особенностей языка этот метод не работает. Поэтому даже если вы опишите его в классе, то он будет работать как обычный метод и объект уничтожаться не будет. Однако, если вам будет очень необходимо освободить память, то можете присвоить переменной указателю на объект константу nil. В таком случае память, занятая под объект освободиться, но объект при этом уничтожен не будет. Указатель, в свою очередь, не перестанет указывать на эту память, соответственно и на объект. И пока на место этой памяти не запишется что-либо другое, можно будет пользоваться объектом. Поэтому может создаться иллюзия, что после присвоения указателю на объект константы nil, ничего не произошло. Однако если вы будете продолжать пользоваться объектом через этот же указатель, и если на его место будет записано, что-либо новое, то возникнет ошибка. Динамический список. Динамическим список называется потому, что его размер может меняться в течение работы программы, и потому, что заранее не известен даже его максимальный размер. В списке могут быть данные какого-либо одного и того же типа. Например, создадим список целых чисел типа integer. Предлагаю, прежде чем читать дальше, подумать самостоятельно над вопросом: как можно организовать динамический список целых чисел? Сделаю подсказку. Динамический список можно организовать с помощью использования указателей. Если у вас появятся какие-либо идеи, то попробуйте их реализовать, прежде чем читать дальше. Думаю, в таком случае материал уложится лучше, и вам будет интереснее. Итак, по какому принципу можно организовать динамический список? Принцип очень прост: каждый элемент списка должен иметь ссылку на следующий элемент списка. Т.е. помимо самого числа, элемент списка должен содержать адрес нахождения следующего элемента. Схематически этот принцип можно представить следующим образом: Теперь осталось только реализовать этот принцип в коде программы. Так как всё современное программирование не безосновательно построено на принципах ООП, то и список предлагаю оформить в виде класса TSpisok. Это даст возможность без особого труда создавать сколь угодно много объектов-списков, а так же можно будет использовать методы этого класса для работы с ними. Далее представлено описание получившегося класса TSpisok и код программы, проверяющей работу этого класса: program Spisok; type TElement = record Data:integer; p:pointer; end; type TSpisok = class public constructor ; begin new(pEl); P1:=pEl; end; procedure write(i:integer); begin pEl^.Data:=i; new(pSled); pEl^.p:=pSled; pSled^.p:=nil; pEl:=pSled; end; procedure Nachalo; begin pEl:=P1; end; function EOS:boolean;//Конец списка begin if pEl^.p=nil then EOS:=true else EOS:=false; end; function read:integer; begin read:=pEl^.Data; pEl:=pEl^.p; end; protected pEl,pSled:^TElement; P1:pointer; end; var Sp:TSpisok:=new TSpisok; begin for var i:=1 to random(1,10) do begin var temp:=random(1000); Sp.write(temp); writeln(i,' temp=',temp); end; Sp.Nachalo; while not Sp.EOS do writeln(Sp.read); end. ___________________________________________ 1 temp=944 2 temp=78 3 temp=824 4 temp=122 5 temp=692 6 temp=591 7 temp=342 944 78 824 122 692 591 342 Обратите внимание, что описания указателей помещены в защищённой секции, для того, что бы тот, кто будет пользоваться данным списком, не мог случайно их изменить, нарушив тем самым сам список. Думаю, что остальные комментарии по данному коду излишни, т.к. названия методов и полей говорят сами за себя. Так же стоит сказать, что на основе этого класса можно создать список с любым типом данных. Для этого необходимо сменить тип поля Data в описании записи TElement. Динамические массивы. Те массивы, которыми мы пользовались, называются статическими, т.к. заранее известно количество элементов массивов и память под них выделяется во время загрузки программы и её объём не меняется в течение работы всей программы. Есть возможность создавать и определять количество элементов массива во время работы программы. Такие массивы называются динамическими. Динамический массив описывается, так же как и статический, но без указания количества элементов. Тип элементов и размерность должны указываться при описании: var dm_1:array of integer;//одномерный массив dm_2:array [,] of integer;//двухмерный массив А вот для того, что бы использовать динамический массив его необходимо создать в теле программы следующим образом: dm_1:=new integer[5]; dm_2:=new integer[3,4]; После операции new ставиться тип элементов и в квадратных скобках их количество. После создания динамического массива им можно пользоваться как обычным (статическим). Здесь будьте внимательны, нумерация в динамических массивах начинается с нуля, поэтому если вы указали количество элементов 5, то индекс последнего элемента будет 4, а индекс первого элемента – 0. Далее пример: var dm_1:array of integer;//одномерный массив dm_2:array [,] of integer;//двухмерный массив begin var raz_1:=random(2,4); var raz_2:=random(2,4); var raz_3:=random(2,4); dm_1:=new integer[raz_1]; dm_2:=new integer[raz_2,raz_3]; //Заполняем массивы for var i:=0 to raz_1-1 do dm_1[i]:=random(1000); for var i:=0 to raz_2-1 do for var j:=0 to raz_3-1 do dm_2[i,j]:=random(1000); //Выводим массивы на экран for var i:=0 to raz_1-1 do write(dm_1[i]:4,' '); writeln; writeln; for var i:=1 to raz_2-1 do begin for var j:=1 to raz_3-1 do write(dm_2[i,j]:4,' '); writeln; end; end. ___________________________________________________________ 389 449 168 444 325 298 Существует ещё второй способ создания динамических массивов с помощью процедуры SetLength. Вместо следующих строчек предыдущего примера: dm_1:=new integer[raz_1]; dm_2:=new integer[raz_2,raz_3]; мы можем написать:
SetLength(dm_1,raz_1); SetLength(dm_2,raz_2,raz_3); результат работы программы не измениться. Отличительная особенность этой процедуры от операции new заключается в том, что при её использовании, если массив уже был создан и его элементы содержали какие-либо данные, то эти данные будут сохранены. Т.е. с помощью данной процедуры можно при необходимости увеличить или уменьшить количество элементов динамического массива. Добавим в предыдущий пример следующий код: SetLength(dm_1,raz_1+2); SetLength(dm_2,raz_2+2,raz_3+2); //Выводим массивы на экран for var i:=0 to raz_1+1 do write(dm_1[i]:4,' '); writeln; writeln; for var i:=1 to raz_2+1 do begin for var j:=1 to raz_3-1 do write(dm_2[i,j]:4,' '); writeln; end; _________________________________________________________ 389 449 168 444 0 0 325 298 0 0 0 0 В предыдущем подразделе мы создали динамический список на основе использования указателей. Теперь, изучив данную тему, мы можем создать, такой же меняющийся во времени список, используя динамический массив и процедуру SetLength. Пример подобной программы приводить не буду. Думаю, если у вас возникнет необходимость в таком списке, вы без труда напишите его сами. Ещё есть возможность создавать массив массивов. Т.е. массив, элементами которого являются массивы. О том, как работать с динамическим массивом массивов при необходимости можете прочитать в разделе справки: «Справочник по языку -> Типы данных -> Динамические массивы». Так же в этом же разделе раскрыты некоторые вопросы, связанные с особенностями работы с динамическими массивами. На мой взгляд, данная тема в этом разделе раскрыта достаточно понятно, поэтому предлагаю изучить её самостоятельно. В данном параграфе мы познакомились с принципами работы с динамической памятью. Соответственно, узнали, что такое указатели и динамические массивы. Плюсом ко всему написали класс TSpisok, который реализует динамический список. Задачи. 1. Усовершенствовать класс TSpisok, добавив свойство, содержащее количество элементов, свойство содержащее значение текущего элемента списка и следующие методы:
Для усовершенствования класса создать класс потомок от класса TSpisok. Вставить комментарии описывающие работу, написанных вами свойств и методов. Написать программу, демонстрирующую работу данного класса. 2. Решить следующую задачу: уравнение работы электродвигателя имеет следующий вид: , где – момент, развиваемый двигателем, – частота вращения вала двигателя. Изменение частоты вращения находится из выражения: , где – момент сопротивления, создаваемы нагрузкой, – время за которое произошло изменение частоты. Для расчётов принять равным 0.0001. Момент сопротивления принять от 100 до 1000 по желанию. Рассчитать запуск двигателя с нулевой до максимальной скорости. Сохранить результаты промежуточных вычислений: время – , момент двигателя , частота вращения – , массиве. Пояснение: изначально система находится в покое, соответственно . Делаем первый шаг и рассчитываем параметры, которые будут через : ; ; . Дальше делаем второй шаг: ; ; . Так продолжаем до тех пор, пока система не уравновесится. Критерием уравновешенности является равенство и . Так как заранее неизвестно количество шагов, то и количество элементов в массиве заранее предугадать невозможно. Поэтому для сохранения результатов необходимо использовать массив динамический. Решение. 1. program ModernSpisok; type TElement = record Data:integer; p:pointer; end; type TSpisok = class ............... end; type TModernSpisok = class (TSpisok) public //Содержит текущий элемент property TekElem:integer write write read read; //Возвращает количество элементов в списке function KolElem:integer; begin var i:=0; Nachalo; while not EOS do begin pEl:=pEl^.p; Inc(i); end; KolElem:=i; end; //Возвращает значение элемента по номеру function ElemPoNomeru(n:integer):integer; begin Poziciya(n); ElemPoNomeru:=pEl^.Data; end; //Меняет значение элемента с номером n procedure PerepisElPoNomeru(n,dat:integer); begin Poziciya(n); pEl^.Data:=Dat; end; //Вставляет новый элемент на место старого с номером n //при этом весь список сдвигается procedure Vstavit(n,dat:integer); begin Poziciya(n); new(PSled); pSled^.p:=pEl^.p; pSled^.Data:=pEl^.Data; pEl^.Data:=dat; pEl^.p:=pSled; end; //Удаляет из списка элемент с номером n procedure Udalit(n:integer); begin if (n>1) and (n<>KolElem) then begin//Если элемент где-то в середение Poziciya(n); pSled:=pEl^.p; dispose(pEl); Poziciya(n-1); pEl^.p:=pSled; end else if n=1 then begin//если удаляем первый элемент Poziciya(1); P1:=pEl^.p; dispose(pEl); end else begin//если удаляем последний элемент Poziciya(n); pEl^.p:=nil; Poziciya(n+1); Dispose(pEl) end; end; //Очищает весь список procedure Ochistit; begin Nachalo; var pTemp:pointer; pTemp:=pEl^.p; pEl^.p:=nil; pEl:=pTemp; while pEl^.p<>nil do begin pTemp:=pEl^.p; dispose(pEl); pEl:=pTemp; end; end; protected procedure Poziciya(n:integer);//Переход на позицию n begin Nachalo; var i:integer:=1; for i:=1 to n-1 do pEl:=pEl^.p; end; end; var Sp:TModernSpisok:=new TModernSpisok; begin writeln('В списке ',Sp.KolElem,' элементов.'); writeln('Создаём случайный список:'); var max:=random(1,10);//Количество элементов списка //Заполняем список for var i:=1 to max do begin var temp:=random(1000); Sp.TekElem:=temp; writeln(i:2,' temp = ',temp); end; //Выводим список на экран writeln('Выводим список на экран:'); Sp.Nachalo; while not Sp.EOS do write(Sp.TekElem,' '); writeln; writeln('Теперь в списке ',Sp.KolElem,' элементов.'); //Выводим элемент с номером temp var temp:=Random(1,max); Writeln('Элемент с номером ',temp,' равен ',Sp.ElemPoNomeru(temp)); //Меняем значение этого элемента Sp.PerepisElPoNomeru(temp,Random(1,1000)); //Снова выводим этот элемент Writeln('После изменения элемент с номером ',temp,' равен ' ,Sp.ElemPoNomeru(temp)); //Вставляем элемент Sp.Vstavit(temp,Random(1,1000)); //Выводим новый список writeln('После вставки нового элемента на место ',temp, ' список выглядит следующим образом:'); Sp.Nachalo; while not Sp.EOS do write(Sp.TekElem,' '); writeln; //Удаляем элемент под номером random(1,max) и выводим новый список temp:=Random(1,max+1); Sp.Udalit(temp); writeln('После удаления элемента под номером ',temp,' список выглядит'+ ' следующим образом:'); //Выводим список Sp.Nachalo; while not Sp.EOS do write(Sp.TekElem,' '); writeln; //Очищаем список и для проверки вызываем метод Sp.KolElem Sp.Ochistit; writeln('После очистки списка колчичество элементов в нём равно - ', IntToStr(Sp.KolElem)); end. _________________________________________________________________________ В списке 0 элементов. Создаём случайный список: 1 temp = 31 2 temp = 719 3 temp = 790 4 temp = 78 5 temp = 597 6 temp = 254 Выводим список на экран: 31 719 790 78 597 254 Теперь в списке 6 элементов. Элемент с номером 3 равен 790 После изменения элемент с номером 3 равен 412 После вставки нового элемента на место 3 список выглядит следующим образом: 31 719 684 412 78 597 254 После удаления элемента под номером 2 список выглядит следующим образом: 31 684 412 78 597 254 После очистки списка колчичество элементов в нём равно – 0 2. program ZapuskDvigatela; const dt=0.0001; type TData = record t,w,Md:real; end; var Mc:real; mData:array of TData; begin Mc:=Random(100,1000); writeln('Mc=',Mc); SetLength(mData,1); var i:integer:=0; mData[i].t:=0; mData[i].w:=0; mData[i].Md:=15372/ ((1-mData[i].w/314)/0.07+0.07/(1-mData[i].w/314)+0.196); while (mData[i].Md-Mc)>0.001 do begin Inc(i); SetLength(mData,i+1); mData[i].t:=mData[i-1].t+dt; mData[i].w:=mData[i-1].w+(1/0.6)*(mData[i-1].Md-Mc)*dt; mData[i].Md:=15372/ ((1-mData[i].w/314)/0.07+0.07/(1-mData[i].w/314)+0.196); end; writeln('t':9,'w':9,'Md':9); for i:=0 to length(mData)-1 do writeln(mData[i].t:9:3,mData[i].w:9:3,mData[i].Md:9:3); end. _________________________________________________________________________ Mc=808 t w Md 0.000 0.000 1056.370 0.000 0.041 1056.506 0.000 0.083 1056.643 0.000 0.124 1056.779 0.000 0.166 1056.915 ....................... 0.017 7.469 1081.500 ....................... 0.023 10.299 1091.332 ....................... 0.033 14.912 1107.749 ....................... 0.054 26.394 1150.829 ....................... 0.095 53.076 1265.057 ....................... 0.125 79.528 1402.908 ....................... 0.156 115.072 1642.853 ....................... 0.211 263.519 5249.811 ....................... 0.222 312.827 809.629 ....................... 0.229 312.829 808.001 Примечание: на практике с помощью подобной программы: во-первых можно узнать время запуска двигателя при определённой нагрузке; во-вторых, используя промежуточные данные, можно проследить изменение момента на валу двигателя и построить график. Используя заполненный динамический массив, можно записать данные в файл для сохранения.
|