|
паскаль
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:39 | Сообщение # 21
|
Администраторы
Сообщений: 301
| В прошлых выпусках мы познакомились с дисковой памятью. Теперь мы начнём знакомство с оперативной памятью. Её нет так много, но она намного быстрее дисковой. Сегодняшний выпуск чисто теоретический, объяснит кое что об устройстве этой самой памяти. Точное положение человека определяет его почтовый адрес. Точное положение нашей программы также определяется адресами. Например для переменных, которые мы используем, отводится место в памяти и при работе программы используются не имена, а адреса переменных (т.е. точные указание их местаперебывания в памяти). Пока наши программы являются программами реального режима работы процессора (он называется Real Mode). Этот режим использовался на первых процессорах семейства 8086/88 (ну и на некоторых моделях далее, сейчас он оставлен для совместимости). Те Pentium'ы и Celeron'ы, которые стоят у вас являются дальними родственниками 8086. На этих процессорах использовалась сегментная модель памяти. Что это значит? Это значит, что вся память разбивается на куски (сегменты), например вот так: ПАМЯТЬ сегмент 0 сегмент 1 сегмент 2 и т.д. Так вот каждый сегмент имеет размер в 64 Kb. Из одной большой памяти мы сделали много маленьких Однако нам нужно знать точное положение переменной в памяти (т.е. указав номер сегмента, мы укажем только с точностью до 64 Кб). Для этого укажем её смещение относительно начала сегмента, т.е. сегмент выглядит так: сегмент 1 смещение 0 смещение 1 смещение 2 и т.д. Так вот, что бы точно указать место нашей переменной в памяти мы должны указать сегмент и смещение в нём. Обычно это записывается в таком виде: SEGMENT : OFFSET (сегмент : смещение). Реальный (физический) адресс расчитывается таким образом: Address = Segment * 16 + Offset Понятно, что для разных пар Segment и Offset мы можем иметь один и тот же физический адрес. Например эти пары указывают на одино и то же место в памяти: 10FA:01C2 10F0:0262 10E0:0362 Замечу, что цифры, используемые в этой записи - шеснадцатеричные, если вы чувствуете себя не очень среди них, то вернитесь к выпуску #2 (http://web-pascal.narod.ru/ras/stunde/s2.htm), где объяснено о том, что это такое. Таким образом формально мы можем иметь доступ к адресам с 00000 - FFFFFh ( буква h в конце числа означает, что это число шеснадцатерично).. Замечу, что при Segment = FFFFh и Offset = FFFFh мы получим Address = 10FFEFh. Однако ввиду ограничения на шину адреса в 20 бит (т.е. размер Address в 20 бит, 20 двоичных цифр) эта комбиинация физически указывает на 0FFEFh - т.е. получается как бы кольцо с небольшим нахлёстом. Кстати наши программы тоже разбиты на сегменты. Различают сегмент кода, сегмент данных и сегмент стека. В сегменте данных хранятся все наши глобальные переменные. В сегменте кода находится код, который исполняет процессор (т.е. все наши процедуры, функции и главная часть программы от begin до end.). И в сегменте стека хранятся локальные переменные и кое-какая служебная информация. Со стеком мы уже сталкивались в предыдущих выпусках. Естесственно, что сегменты кода, данных и стека имеют размер в 64 Kb (сегмент стека может быть и меньше, его размер задаётся опцией Stack size, о которой говорилось в предыдущих выпусках). Например напишем такую программу: program test; var a : array [0 .. 65535] of byte; begin end. вроде бы ничего особенного. Просто большой массив. Однако попробуем запустить .... бац Error 22: Structure too large.(размер структуры слишком большой). Нетрудно понять, что наш массив не влезает в 1 сегмент, который отведён для данных. Точно так же обстоят дела и с кодом и со стеком. Ошибку переполнения стека мы уже разбирали. Получается, что в сумме память, которую использует наша программа равна 64 + 64 + 64 = 192 Кб. ??? Зачем же тогда на компьютеры ставят так много памяти? Так как наши программы работают в Real Mode, нам доступен только 1 Мбайт памяти (на самом деле ещё меньше) - максимальное число из 20 двоичных цифр. Остальные доступны в только защищённом режиме работы процессора. Те 192 Кб, которые мы насчитали называют статическими (т.к. они неизменны). Переменные, созданные в разделе переменных называются статическими переменными. В противоположность им есть динамические переменные - те память под которые выделяется и освобождается программистом. Ниже приведено распределение памяти: * 00000h - 9FFFFh - Base Memory, 640 Кбайт - стандартная память, доступная программам реального режима. Распределена следующим образом: o 00000h - 003FFh - Interupt Vectors - векторы прерываний, что это такое я раскажу позже. o 00400h - 004FFh - BIOS Data Area - область переменных Bios. o 00500h - 00xxxxh - DOS Area - область DOS o 00xxxxh - 9FFFFh - User RAM - память, доступная для использования. * A0000h - FFFFFh - Upper Memory Area (UMA), 384 Кбайта - верхняя память, зарезервированая для системных нужд. Например, в ней размещается память адаптеров. * 10000h и выше - Extended Memory - дополнительная память, непосредственно доступная только в защищенном режиме процесора. Что бы не осталось недоговорок: защищённый режим (Protected Mode). Впервые появился на 80286 процессоре. Попытки выполнения недопустимых команд, выхода за рамки отведённого пространства памяти и областей ввода/вывода контролируются системой защиты. В защищённом режиме работает, например Windows (и соответственно программы для него), а в реальном режиме DOS (большинство ДОС программ так же работают в этом режиме). добавление Хочу кое что добавить к предыдущим выпускам. Каждый раз создавая новый тип, мы сначала описывали его в разделе type, а потом создавали переменную этого типа в разделе var. Однако, если вам нужно только несколько глобальных (или локальных) переменных этого типа, то можно сделать так: var c : file of integer; т.е. указать тип сразу после имени переменной, не описывая его предварительно. Вот это я и хотел добавить.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:39 | Сообщение # 22
|
Администраторы
Сообщений: 301
| В прошлый раз я рассказывал об устройстве памяти. Сегодня мы научимся использовать эту самую память. В паскале существует специальный тип - указатели. Переменная указатель - это ссылка на данные или код. Указатель это вам не это Значение переменной указателя - это адрес памяти. А что распологается по этому адресу решать вам. Но сначала несколько общих слов и соображений. Во первых некоторые сокращения: RAM (Random Access Memory) - память общего доступа, т.е. память с которой мы можем делать всё, что захотим. ROM (Read Only Memory) - память только для чтения, в неё мы не можем ничего записать. Ещё одно соображение: мы пишем программы для операционной системы DOS. В Windows большинство DOS программ работает нормально. Однако в WinNT, 2k, XP на 99% уверен, что возникнут проблеммы с работой программ, приведённых ниже. Всё дело в том, что Windows не "любит" когда кто-то работает с памятью, а ему об этом не слова. Поэтому он ограничивает работу таких программ, давая им уверенноть, что они по работали с памятью, а на самом деле ничего такого не произошло. Так что если что-то не сработает, не надо сразу завалить меня гневными письмами. Единственный выход из такого положения: создать загрузочную дискетку с DOS'ом и загрузившись с неё запускать программы. Для получения адреса переменной вам нужно указать перед её именем знак "собака" - @. Т.е. если i : integer; то @i - это адрес переменной i. Для объявления указателя нам надо создать переменную соответствующего типа. Для начала мы рассмотрим самый простой тип указателей - безтиповые указатели Они могут указывать (т.е. хранить адрес) чего угодно. Хотите адрес переменной - пожалуйста, адрес записи - с легкостью, процедуры - ни каких проблем. Имя у этого типа - Pointer (англ. указатель). Напишем такую программку: var i : integer; p : pointer; begin i := 3; p := @i end. а теперь вопрос, что содержится в p (после выполнения программы) ? Ответ адрес переменной i. Если вы посмотрите на указатель p в отладчике, то вы увидите, что он равен Ptr($13C1,$50) (надо довести выполнение программы до полного завершения). Что это такое? Для работы с адресами в паскале есть функция: function Ptr(Seg, Ofs: Word): Pointer; она преобразовывает адрес сегмента (Seg) и смещения (Ofs) к указателю. Знак доллара - "$" - перед числом означает, что оно шеснадцатеричное. это специфическое обозначение паскаля. Я при объяснении буду использовать букву h, а в программах мы увидим знак $. Просто это одно и тоже 10h = $10 = 16. Если вы слабо владеете этой системой - обязательно прочитайте выпуск #2 Системы счисления ч1 Шестнадцатеричная Так вот Ptr($13C1,$50) означает, что наш указатель указывает на область памяти с сегментом 13С1h и смещением в этом сегменте 50h. Поменяем местами объявления p и i. var p : pointer; i : integer; begin i := 3; p := @i end. Что же тогда произойдёт? Казалось бы ничего такого - просто поменяли местами объявления двух переменных, что от этого может изменится ? Оказывается может! Давайте посмотрим на это дело в отладчике... действительно p теперь равен Ptr($13C1,$54). Почему такое произошло ? Мы перераспределяем память в сегменте данных. Т.е. в первом случае у нас сначала в памяти отводится место под переменную i, а потом идёт указатель. Во втором случае память сначала выделяется под указатель, а только потом под i. Поэтому смещение в обоих случаях разное. Указатель без типа хорошо, ну а с типом лучше! Для использования таких указателей лучше создать новый тип. Перед именем того типа, на который наш указаетль указывает нужно поставить символ каре - "^". type pinteger = ^integer; Теперь мы можем создать указатель на переменную типа integer. И присвоить указателю её адрес. var i : integer; p : pinteger; begin i := 3; p := @i; Однако наверняка вам хочется знать, что же расположено по адресу на который указывает p ?? Для этого существует приём, который называется "разыменование указателей". Суть его в том, что разыменовав указатель вы можете работать с ним так, как если бы это была переменная того типа, на который он указывает. Чтобы разыменовать указатель, поместите ^ после его имени. begin i := 3; p := @i; write (p^); выдаст на экран значение 3. Используя символ ^ мы указываем, что надо использовать указатель не как адрес, а как то что содержится по этому адресу. На что указывает p ? На адрес переменной i. Т.е. изменив i мы изменим значение по адресу p. А изменив p, мы получим в p указатель на неизвестно что. При работе программы используются, не имена переменных, а их адреса. Так как в р у нас адрес i, то изменив значение по этому адресу (по адресу в р) мы изменим значение i. А изменив значение i, мы изменим только значение 2-х байтов по адресу р (и соответственно изменим p^). Например чему будет равно i после такого: begin i := 3; p := @i; p^ := 123; Ответ i = 123. Как? Ведь мы i нигде не изменяем ?? Что делает эта строчка p^ := 123; - она помещает по адресу p значение 123. А адрес р равен адресу i. Для нашего случая условно говоря p^ = i !!! Однако поменяв адрес p на что-то ещё p^ уже будет не равно i !!! Важно вбить себе в голову, что указатель (р) - это только адрес, по которому может распологаться всё, что угодно. А p^ - это значение по этому адресу. На один и тот же адрес может указывать бесконечное число указателей. Например такой код: var i : integer; p1, p2 : pinteger; begin i := 3; p1 := @i; p2 := p1; p2^ := 123; writeLn (i) end. Опять тот же вопрос: чему равно i ? Ответ i = 123. Давайте разберёмся построчно и с отладчиком. Кстати в отладчике вместо адреса сегмента данных указывается Dseg (от англ DateSegment - сегмент данных). Это результат такой функции function DSeg: Word; Она возвращает адрес сегмента данных. Итак: * i := 3; - смотрим в отладчике: i = 3; p1, p2 = nil. Что это за nil чуть позже и ниже :). * p1 := @i; - присваиваем р1 адрес i. ( i = 3; p1 = Ptr(DSeg,$52); p2 = nil) * p2 := p1; - присваиваем р2 значение р1, т.е. присваиваем р2 адрес i (i = 3; p1, p2 = Ptr(DSeg,$52)) * p2^ := 123; - помещаем по адресу p2 значение 123. А что у нас за адрес в р2 ? Адрес i. Значит мы изменяем значение по этому адресу, т.е. значение i (i = 123; p1, p2 = Ptr(DSeg,$52)) Вернёмся к nil. Когда указатель равен nil это означает, что он указывает ни на что. Т.е. не проинициализированный указатель. Программа Сегодня мы не будем писать что-то особенное, а просто полазим по некоторым адресам в памяти. Давайте вспомним распределение памяти: * 00000h - 9FFFFh - Base Memory, 640 Кбайт - стандартная память, доступная программам реального режима. * A0000h - FFFFFh - Upper Memory Area (UMA), 384 Кбайта - верхняя память, зарезервированая для системных нужд. В ней размещается память адаптеров. * 10000h и выше - Extended Memory - дополнительная память, непосредственно доступная только в защищенном режиме процесора. Extended memory нас не интересует, т.к. для нас она не доступна. Обратим наш взгляд на UMA - столько всего интересного... Давайте для начала полазаем по памяти нашего видеоадаптера. В зависимости от его типа она занимает следующие адреса: * MDA RAM - B000h - B0FFFh * CGA RAM - B800h - BBFFFh * EGA ROM - C000h - C3FFFh * VGA ROM - C000h - C7FFFh * EGA, VGA RAM - A000h - BFFFFh, в зависимости от режима используют следующие области: o Графика - A000h - AFFFFh o Цветной текст - B800h - BFFFFh o Моно текст - B000h - B7FFFh Думаю, что у всех сейчас стоит VGA (SVGA тоже сюда относится). Так что разговор пойдёт о нём. Наши программы работают в текстовом режиме (существует ещё и графический, но о нём позже). К тому же в цветном, поэтому видеопамять распологается по этим адресам B800h - BFFFFh . В этом режиме под символ отводится 2 байта (или одно слово: сам символ + аттрибуты (цвет)). Например давайте прочитаем самый первый символ на экране. Мы можем читать только 1 байт, т.к. сначала идёт байт, который отвечает за символ, а потом байт-аттрибут. type pbyte = ^byte; var video : pbyte; begin video := Ptr ($B800, 0); writeLn (chr(video^)) end. Если вы не очищали экран и запускали программу из среды BP, то должна вывестись буква B (первый символ на экране). А теперь вообще чудный фокус: давайте изменим значение этого первого символа на весёлую рожу. Изменим строки: begin video := Ptr ($B800, 0); video^ := 1 end. Запустим... J ... порадуемся. Сейчас мы использовали метод, называемый прямой доступ к памяти (в нашем случае к видео памяти). Это самый быстрый способ вывода символа на экран. Так как под символ отводится слово, то мы можем в байте-аттрибуте задать цвет символа, цвет фона и ещё кое что. Биты байта-аттрибута отвечают за следующее: Фон Текст 7 6 5 4 3 2 1 0 BL R G B I R G B Буквы RGB представляют собой комбинацию для цвета. R - красная составляющая, B - синяя, G - зелёная. Бит 7 (BL) - отвечает за мигание символа, а бит 3 (I) - интенсивность свечения. Т.е. если мы хотим вывести мигающий символ красным цветом на чёрном фоне, то мы должны составить такую комбинацию 10000100. Давайте изменим код предыдущей программы таким образом: begin video := Ptr ($B800, 1); video^ := $84 end. Мы изменили смещение на 1 байт, поэтому теперь video^ это не символ, а байт-аттрибут первого символа на экране. Изменив значение по этому адресу мы изменим значение байта-аттрибута первого символа. Если там оставалась весёлая рожица, то она станет красной и начнёт моргать, если там оставалась буква В, то тогда это приключится с ней. Однако помимо экрана (кстати запомните этот адрес B800h - он может встречаться довольно часто), так вот есть ещё куча интересных мест, куда нам следует сунуть свой нос. Например слово по адресу 0:413 - в нем содержится информация BIOS'a о наличной памяти в килобайтах. Оно заполняется во время теста POST. Что бы узнать значение по этому адресу напишем такую "програмку". type pword = ^word; var p : pword; begin p := Ptr (0, $413); writeLn (p^) end. ничего сложного. Однако, если нам понадобится не байт, не два, а сразу много ? Тогда надо создать указатель на массив. Например 8 байт по адресу FFFF:5 - это дата изготовления BIOS'a. type tarray = array [0..7] of byte; p8byte = ^tarray; var p : p8byte; i : integer; begin p := Ptr ($FFFF, 5); for i := 0 to 7 do write (chr(p^[i])) end. При загрузке компьютера в момент, когда определяется количество памяти (тест POST), нажмите на клавишу Pause - и посмотрите в левый нижний угол экрана - там обычно эта дата и печатается. Однако кроме того, что мы указали на массив из 8 байт, мы могли указать на 1 байт, и в цикле менять смещение. Память вообщем отдалённо напоминает простой массив. Можно посвятить целую рассылку, перечисляя адреса памяти, по которым можно полазить и узнать что-нибудь полезное, однако на сегодня хватит. К тому же подобную информацию легко найти.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:40 | Сообщение # 23
|
Администраторы
Сообщений: 301
| Прочитать что-нибудь в памяти это конечно хорошо, но гораздо лучше размещать в ней свои переменные. До этого мы пользовались статическими переменными - память для них выделялась как бы "по умолчанию". Однако можно создавать и динамические переменные - память для них мы должны выделять и освобождать самостоятельно. Вообще, честно говоря, память можно не освобождать, т.к. ОС после завершения работы программы затирает её сама. Но иногда случаются глюки и ..... вообщем мы будем всегда освобождать неиспользуемую память. Для выделения/освобождения память существует 2 пары процедур: New/Dispose и GetMem/FreeMem. Начнём с procedure New(var P: Pointer) Вызвав эту процедуру, передав ей в качестве параметра имя указателя (можно так же имя типизированного указателя, т.к. pointer вмещает его в себя). Так вот, вызвав эту процедуру, вы соответственно отведёте кусок памяти нужного размера (т.е. размера той переменной, на которую указывает указатель p). После вызова процедуры получаем следующее: p - адрес, p^ - переменная, того типа на который указывает p. Например: var pInt : ^integer; i : integer; begin i := 1; New (pInt); pInt^ := i; Вызвав New (pInt); мы выделяем два байта (именно столько занимает переменная типа integer). Теперь у нас как бы появляется новая целочисленная переменная pInt^. Освободить, выделенную в ходе работы программы память можно процедурой: procedure Dispose(var P: Pointer); Завершим предыдущий пример таким образом: i := pInt^ * 2; Dispose (pInt) end. теперь посмотрим на это дело в отладчике. Добавим только ещё пару строк. Теперь наша программа будет выглядеть так: type pinteger = ^integer var pInt, pInt1 : pinteger ; i : integer; begin i := 1; New (pInt); pInt^ := i; i := pInt^ * 2; Dispose (pInt); New (pInt1); pInt^ := i; Dispose (pInt1) end. Давайте добавим просмотр значений указателей pInt и pInt1 и значений по этим адресам (вместо имени переменной надо ввести pInt^ и pInt1^ соответственно). Входим в пошаговое исполнение, видим как pInt присваивается адрес (New (pInt)), потом по этому адресу меняется значение (pInt^ := i;), потом происходит вызов Dispose (pInt).... вроде бы ничего не поменялось... ладно смотрим дальше...После New (pInt1) pInt1 ссылается на тот же адрес, что и pInt !!! Поэтому pInt^ := i; изменит значение pInt1^ !!! Почему так происходит ? Памятью заведует операционная система, именно она по нашему запросу решает дать или не дать. Когда мы вызываем New (pInt) ОС выделяет 2 байта и возвращает нам адрес этих байт. Вызывая же Dispose (pInt); мы "говорим" ОС, что эти 2 байта нам больше не нужны и она считает их свободными. Когда мы в следующий раз просим 2 байта (New (pInt1)), то она опять выделяет нам те же самые байты, поэтому происходит такая фишка, что pInt и pint1 указывают на один и тот же адрес! Поэтому, что бы не возникло логической ошибки (такую ошибку будет трудно поймать), можно после освобождения памяти присваивать неиспользуемым указателям значение nil и так же перед вызовом Dispose заполнить эту память 0. Так же есть возможность использовать New, как функцию. В таком случае она возвращает указатель типа, имя которого является единственным параметром такой функции. Т.е. в предыдущем примере мы могли написать следующее: pInt := New (pinteger); Это особенно полезно в случаях, когда может потребоваться присваивать указателю элементы различных типов. Теперь обратим наш взгляд ко второй паре процедур. procedure GetMem(var P: Pointer; Size: Word); Как видите эта процедура отличается от New вторым параметром. Size - это точное число выделяемых байт. Т.е. под указатель на integer Size = 2, на byte Size = 1 и так далее. Естесственно, что для определения размера структур можно использовать функцию sizeof. Для тех, кто забыл я повторюсь: function SizeOf(X): Integer; возвращает размер аргумента Х в байтах. Соответственно вторая процедура procedure FreeMem(var P: Pointer; Size: Word); Параметр Size теперь определяет число байт, которые надо освободить. Параметр Size у GetMem / FreeMem должен совпадать, иначе в памяти останутся байты, которые не используются, но и не считаются свободными. Т.е. происходит простое захламленее памяти. Или же если вы освободите больше байт, чем "заказали", то тогда вы можете освободить память другой переменной! Кажется зачем такие трудности, когда используя New/Dispose можно не заботится об размере выделяемой памяти ? Однако давайте рассмотрим такой пример: считать из файла 1000 строк и сохранить их в массиве: program read; type pstring = ^string; var buffer : string; data : array [1 .. 1000] of pstring; f : text; i : integer; begin assign (f, 'C:\autoexec.bat'); reset (f); for i := 1 to 1000 do begin readLn (f, buffer); getMem (data[i], length (buffer)); data[i]^ := buffer end; ..... Итак мы создаём массив указателей data. Каждый элемент такого массива - указатель на строку. Потом мы в цикле читаем из файла строки, при этом заносим строку во временную переменную buffer. Теперь обратите внимание, как мы выделяем память getMem (data[i], length (buffer)); В качестве размера выделяемой памяти мы используем длинну считанной строки. Используя New, мы бы всегда выделяли бы 256 байт (размер string), однако если размер считанной строки меньше чем 256 байт, зачем нам тратить лишнюю память ? А теперь маленький вопрос как вы видите в конце программы я поставил троеточие. Так вот вопрос: как в данном случае правильно освободить память ? Ответы как всегда шлите на мыло, внизу. Ещё один момент, может быть он и очевиден, но всё же: для выделения/освобождения память надо использовать только парные функции. Т.е. вы не можете вызвать с New - FreeMem, а с GetMem нельзя использовать Dispose.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:40 | Сообщение # 24
|
Администраторы
Сообщений: 301
| Информационный ресурс: фильмы, игры, софт. Итак сегодня мы займёмся рассмотрением классического примера: связанный список. Для начала самый простой вариант - односвязный. Что же такое связаный список ? Давайте представим, что у нас есть запись.... для простоты введём только одно поле tlist = record data : integer; end; И пускай нам нужно создать массив из N таких записей, причём N задаётся пользователем и может изменяться во время работы программы, как в сторону увеличения, так и в сторону уменьшения. Конечно можно создать массив таким образом: list : array [1 .. N] of tlist; где N будет максимально возможное число для того, что бы массив поместился в сегменте данных. Однако это не есть гуд. Гораздо проще такая задача решается с использованием указателей. Чем мы сейчас и займёмся. Идея связанного списка проста как бит: в каждом элементе содержится указатель на следующий. Т.е. запись выглядит так: type plist = ^tlist; tlist = record data : integer; next : plist end; Давайте рассмотрим организацию такого списка. Проще всего это дело нарисовать, к сожалению к рассылке нельзя приаттачить картинку, поэтому я нарисую её тут с использованием текстовых символов (не судите за убогость): +------+ |---->+------+ |---->+------+ | data | | | data | | | data | +------+ | +------+ | +------+ | next | | | next | | | next | | ----------- | ----------- | -------------> nil +------+ +------+ +------+ итак как мы видим в указателе next находится адрес следующего элемента в памяти. Последний же указатель указывает в никуда (т.е. в nil). Поэтому для организации списка нам потребуются 2 указателя на tlist. Один из них будет постоянно хранить адрес начала (т.е. первого элемента) списка, второй будет "суетится" по списку, т.е. бегать по нему из начала в конец. Можно было бы ввести ещё и указатель на последний элемент (т.е. 3-й указатель на tlist) , но в нём нет надобности, т.к. узнать кончился ли список мы можем, сравнив next с nil. Т.е. у нас будет 2 глобальных переменных: var list, start : plist; Start - будет указывать на начало списка, а list будет нашей рабочей лошадкой. Как видите наш список занимает в памяти всего 8 байт (под один указатель отводится 4). Для начала напишем процедуру, которая добавляет элемент в список. Замечу, что добавляем мы всегда в конец списка! Параметр d - это то, что нам нужно добавить. list у нас указывает на последний элемент списка. procedure Add (d : integer); var l : plist; begin New (l); { выделяем память под структуру } l^.data := d; { инициализируем данные } l^.next := nil; { т.к. добавляем в конец, то указатель на следующий равен nil } { если start не равен nil, значит список не пуст} if start <> nil then begin list := start; { "перематываем" список, что бы list стал последним элементом } while list^.next <> nil do list := list^.next; list^.next := l { устанавливаем указатель на следующий элемент, а это у нас l } end else start := l; { если список был пуст, тогда делаем l первым элементом } list := l { теперь меняем текущий элемент, присваивая ему значение l } end; Обратите внимание, что мы не освобождаем память, которую занимает l. Почему мы так делаем ? Опять "нарисую" картинку: +------+ |---->...->+------+ +------+ | data | | ... | data | | data | +------+ | ... +------+ +------+ | next | | ... | next | | next | | ----------- ... | -------->nil | -------------> nil +------+ ... +------+ +------+ ^ ^ ^ | - это start | - это list | - это l Выполняя присваивание list^.next := l мы переходим к такому виду: +------+ |---->...->+------+ |---->+------+ | data | | ... | data | | | data | +------+ | ... +------+ | +------+ | next | | ... | next | | | next | | ----------- ... | ------------ | -------------> nil +------+ ... +------+ +------+ ^ ^ ^ | - это start | - это list | - это l и после list := l мы получаем такую картину: +------+ |---->...->+------+ |---->+------+ | data | | ... | data | | | data | +------+ | ... +------+ | +------+ | next | | ... | next | | | next | | ----------- ... | ------------ | -------------> nil +------+ ... +------+ +------+ ^ ^ | - это start | - это list и l Если же мы добавляем первай элемнт, то в начале start = nil. В конце и list и start и l будут указывать на одно и тоже. Если же освободить память, которую занимает l, то последний (или первый) элемент списка отрежется и мы ничего не добавим. Теперь напишем процедуру, которая выводит результаты нашей деятельности, т.е. список на экран: procedure Display; begin if start = nil then begin writeLn ('Список пуст.'); exit end; list := start; while list <> nil do begin write (#32, list^.data); list := list^.next end end; Итак тут мы устанавливам указтель list на начало списка (list := start), а потом "прокручиваем" его, пока он не равен nil (т.е. пока он существует). Обратите внимание, что мы переходим к следующему элементу таким образом: list := list^.next После выполнения программы было бы неплохо (я сказал даже очень замечательно) освободить память, которую занимает список. Это кстати может понадобится и в процессе выполнения самой программы. Процедура осовобождения очень похожа на процедуру вывода - мы так же последовательно перебираем список, только вместо вывода на экран мы освобождаем память. procedure Clean; var l : plist; begin if start = nil then exit; { список пуст, удалять нечего } list := start; { переходим на начала списка } while list <> nil do begin l := list; { теперь l - это текущий элемент } list := list^.next; { list переходит на следующий } Dispose (l) { освобождаем память } end; start := nil { обнуляем указатель начала } end; Естественно, что нам может понадобится найти чего-то в списке. Для этого предназаначена следующая процедура. Мы ищем элемент с data = d. Указатель на него останется в list после завершения работы процедуры, если же элемент не найден, то list = nil. Сама она не представляет из себя что-то сверх выдающееся. Тот же перебор от начала до конца (или же до той поры пока не найдём). procedure Search (d : integer); var flag : boolean; begin list := start; flag := false; while (list <> nil) and (not flag) do begin if d = list^.data then flag := true else list := list^.next end end; Ну и напоследок напишем процедуру для удаления произвольного элемента из списка. Тут сразу же возникает 3 случая: удаляется первый, удалется последний и удаляется средний (т.е. не первый и не последний) элемент. Для начала рассмотрим третий случай. Список в данный момент представляет из себя что-то такое: ...->+------+ |---->+------+ |---->+------+ | data | | | data | | | data | +------+ | +------+ | +------+ | next | | | next | | | next | | ----------- | ------------ | ------------... +------+ +------+ +------+ prev cur nxt Я назвал условными именами элементы: cur - элемент, который удаляем; prev - элемент, который идёт перед удаляемым; nxt - элемент, идущий за удаляемым. Что бы удалить cur мы должны сделать следующие действия: prev.next = nxt (т.е. предыдущий должен указать на последующий за удаляемым): ...->+------+ +------+ |--|---->+------+ | data | | data | | | | data | +------+ +------+ | | +------+ | next | | next | | | | next | | -----------| | ---------+--- | ------------... +------+ | +------+ | +------+ prev | cur | nxt | | |--------------| Таким образом мы исключили из списка элемент cur, теперь просто освободим память и список преобразуется в такой: ...->+------+ |------>+------+ | data | | | data | +------+ | +------+ | next | | | next | | -----------| | | ------------... +------+ | | +------+ prev | | nxt | | |------| Случай удаления последнего элемента, является частным от удаления среднего, если предположить, что nxt = nil. А случай для удаления первого, если prev = nil. Процедура, которая выполняет всё описанное выше написана ниже При этом обратите внимание, что в момент входа в процедуру list указывает на элемент, который предшествует удаляемому (т.е. условно list = prev) procedure Delete; var l : plist; begin if start = nil then exit; { список пуст - удалять нечего } l := list^.next; { теперь l указыват на удаляемый элемент l = cur} if list <> nil then begin{ если удаляется не первый элемент } list^.next := l^.next; { prev.next = nxt (или же prev.next = cur.next) } Dispose (l){ Освобождаем память } end else begin { удаляем первый элемент } list := start^.next; Dispose (start); { освобождаем память } start := list { устанавляваем новое начало списка } end end; Теперь у нас есть довольно мощный арсенал для работы со списком. Мы можем заполнить список с помощью Add, очистить его Clean, найти нужный элемент Search и удалить его .... или нет удалить его сразу мы не можем. Ведь после выполнения Search list указывает на найденный элемент, а для выполнения Delete нам в list нужен указатель на предыдущий. Надо бы написать ещё процедуру, которая бы находила элемент предшествующий данному. Это и будет домашним заданием. Написать процедуру findPrior, которая ищет элемент списка, предшествующий list.... после её выполнения именно list и должен указывать на этот элемент (что бы потом проще было вызывать Delete). Так же было бы не плохо (а вернее очень хорошо) написать процедуру для добавления элемента в начало списка. Ниже я привёл пример работы со связанным списком. Это просто все процедуры описанные выше + маленькая менюшка. Естественно не хватает процедуры findPrior. Поэтому не пытайтесь что-то удалять пока не напишите её. colspan
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:41 | Сообщение # 25
|
Администраторы
Сообщений: 301
| [/code]program listexample; uses CRT; type plist = ^tlist; tlist = record data : integer; next : plist end; var list, start : plist; i : integer; procedure Add (d : integer); var l : plist; begin New (l); l^.data := d; l^.next := nil; if start <> nil then begin list := start; while list^.next <> nil do list := list^.next; list^.next := l end else start := l; list := l end; procedure Display; begin if start = nil then begin writeLn ('‹Ёбв Їгбв'); exit end; list := start; while list <> nil do begin write (#32, list^.data); list := list^.next end end; procedure Clean; var l : plist; begin if start = nil then exit; list := start; while list <> nil do begin l := list; list := list^.next; Dispose (l) end; start := nil end; procedure Delete; var l : plist; begin if start = nil then exit; l := list^.next; if list <> nil then begin list^.next := l^.next; Dispose (l) end else begin list := start^.next; Dispose (start); start := list end end; procedure Search (d : integer); var flag : boolean; begin list := start; flag := false; while (list <> nil) and (not flag) do begin if d = list^.data then flag := true else list := list^.next end end; procedure findPrior; begin **** ERROR **** end; begin while true do begin ClrScr; writeLn ('Џа®Ја ¬¬ ¤«п а Ў®вл б® бўп§ л¬ бЇЁбЄ®¬'); writeLn ('1 - ¤®Ў ўЁвм н«Ґ¬Ґв'); writeLn ('2 - ©вЁ Ё г¤ «Ёвм н«Ґ¬Ґв'); writeLn ('3 - ®зЁбвЁвм бЇЁб®Є'); writeLn ('4 - ўлўҐбвЁ бЇЁб®Є'); writeLn ('5 - ўл室'); writeLn ('>>>>>'); Display; gotoxy (7, 7); readLn (i); gotoxy (1, 9); case i of 1 : begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); Add (i) end; 2 : begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); Search (i); if list = nil then begin writeLn ('ЌҐ ©¤Ґ®'); readLn; continue end; findPrior; Delete; end; 3 : Clean; 4 : begin ClrScr; Display; readLn; end; 5 : begin Clean; break end end end end.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:41 | Сообщение # 26
|
Администраторы
Сообщений: 301
| Как вы могли заметить односвязные списки имеют один большой недостаток, который препятствует их широкому применению: такой список нельзя просматривать в обратном направлении. Для этих целей используются списки с двойной связью. Такой список представляет из себя ту же структуру, только теперь добавляется указатель на предыдущий элемент. Т.е. вот так: +------+<---| |---->+------+<---| |---->+------+ | data | | | | data | | | | data | +------+ | | +------+ | | +------+ | next | | | | next | | | | next | | ---------+--- | ---------+--- | -------------> nil +------+ | +------+ | +------+ | prev | | | prev | | | prev | nil<--------- | |------------- | |------------- | +------+ +------+ +------+ Соответственно наша структура преобразуется к следующей: type plist = ^tlist; tlist = record data : integer; next, prev : plist; end; Работа с двусвязным списком не на много сложнее работы с односвязным. А в некоторых местах даже легче. Кроме двусвязных списков я бы хотел обратить внимание на ещё одну деталь. Давайте создадим указатель на указатель звучит не очень, но всё таки. Для нашего примера тип указателя на указатель на tlist опишется так: p2list = ^plist; Что же это такое указатель на указатель ? Давайте вспомним что такое указатель. Указатель - это адрес. Значит указатель на указатель - это адрес указателя. Т.е. это адрес той ячейки памяти, где содержится адрес ячейки памяти, где содержится tlist. Помните: "...синица, которая ворует пшеницу, которая в чёрном чулане хранится, в доме который построил Джек." Мы можем создать и указатель на указатель на указатель и так далее. Но сегодня мы не будем так изощрятся и кроме двусвязных списков научимся использовать указатели на указатель. Для начала откажемся от глобальных переменных. Давайте представим, что мы будем писать функции и процедуры для кого другого, т.е. у нас не будет глобальных переменных - всё, что нам нужно мы будем получать из параметров. Начнём с добавления нового элемента в конец списка. Это очень просто procedure AddToEnd (list : p2list; d : integer); var prior : plist; begin prior := nil; { перематываем список до конца } while list^ <> nil do begin prior := list^; list := @list^^.next end; { добавляем новый элемент } New (list^); with list^^ do begin data := d; next := nil; prev := prior end end; Итак наша процедура получает 2 параметра: указатель на указатель на tlist и число, которое добавим в конец списка. Давайте обратимся к самой "страшной" строчке и попробуем разобраться: list := @list^^.next. Итак list указывает на plist, т.е. если разыменовать(получить значение по его адресу) указатель, то list^ - это указатель на tlist. Тогда, что бы обратиться к какому либо полю мы должны провести операцию разыменования ещё раз, т.е. list^^ - это уже структура tlist и мы можем спокойно обращаться с её полями. Теперь давайте разберём зачем мы здесь использовали @. Опять обратимся к определению указателя: указтель - это адрес. Значит в list у нас должен содержаться адрес указателя на tlist. Такой указтель у нас есть - list^^.next - указывает на tlist. Значит мы должны присвоить list адрес list^^.next, что мы благополучно и делаем. В остальном эта процедура ничем замечательным не обладает. Итак элемент добавлен, после работы его надо удалить. procedure DelFromEnd (list : p2list); var old : plist; begin if list^ = nil then exit; { список пуст, удалять нечего } { перемотка в конец } while list^^.next <> nil do list := @list^^.next; { list^ указывает на последний элемент } old := list^; { удаляем этот элемент логически ....} list^ := nil; { ... и физически - освобождаем память } Dispose (old) end; Так же необходимо написать процедуру для очиски списка. Это можно сделать 2 способами. 1. Преобразовать DelFromEnd в функцию, возвращающую значение false, если список пуст, тогда что бы его очистить можно создать пустой цикл, пока функцию не вернёт false 2. Воспользоваться специальной процедурой, текст которой приведён ниже. procedure Clear (list : p2list); var old : plist; begin if list^ = nil then exit; { перемотка в начало } while list^^.prev <> nil do list^ := list^^.prev; { перемотка в конец с уничтожением текущего элемента } while list^ <> nil do begin old := list^; list^ := list^^.next; Dispose (old) end end; Естественно, что нам может понадобится функция для поиска элемента. Поиск осуществляется тем же перебором от начала до конца, если поиск успешен, то функция вернёт значение true, а list будет указывать на найденный элемент. В противном случае функция вернёт false. function FindData (list : p2list; data : integer): boolean; var l : plist; begin if list^ = nil then begin FindData := false; exit end; while list^^.prev <> nil do list^ := list^^.prev; l := list^; while l <> nil do begin if l^.data = data then begin FindData := true; list^ := l; exit end; l := l^.next end; FindData := false end; Внимательный читатель должен был бы уже призадуматься, а почему это мы передаём параметры обычным способом и ожидаем, что в конце что-то изменится ? (Если этот вопрос вас не гложет или вы понимаете почему произойдёт изменение переходите сразу дальше). Ведь после выполнения такого кода a и b остануться равными 0 ! procedure bla (c,d : integer); begin c := 2; d := 3 end; var a, b : integer; begin a := 0; b := 0; bla (a, b) end. Однако, то что истинно для простых переменных не всегда правда для указателей. Давайте немного изменим код: type pint = ^integer; procedure bla (c,d : pint); begin c^ := 2; d^ := 3 end; var a, b : integer; begin a := 0; b := 0; bla (@a, @b) end. В результате a и b изменятся ! Давайте разберём почему. Мы передаём в bla указатели (читай адреса) c и d. Потом меняем значения по этим адресам (c^ := 2). При вызове в bla передадутся адреса a и b - bla (@a, @b). Когда процедура изменит значения по этим адресам, то изменятся значения a и b. Всё это конечно хорошо, но зачем тогда мы передаём в процедуры для работы со списком указатель на указатель, а не просто указатель! Тут всё дело в организации списка. Для односвязного списка мы использовали 2 указателя: начала и рабочий. Для двусвязного списка необходимость в указателе на начало отпадает, поэтому у нас будет один единственный указатель. С ним надо быть осторожнее! Не дай бог обратить его где-нибудь в nil! Односвязный список мы могли бы востановить по указателю начала. Поэтому например в функции поиска для перемотки используется дополнительная переменная. Однако я отвлёкся. Главная часть нашей программы будет выглядеть так: var list : plist; { это и есть наш список } begin ..... AddTolistEnd (@list, d); ..... Т.е. что бы изменить list, а значит и наш список нам надо в процедуры передавать указатель на plist, т.е. указатель на указатель на tlist. Но вернёмся к нашим баранам. Продолжим пополнять арсенал процедур и функций для работы со списком. Теперь, когда мы можем найти элемент в списке, то нужно с ним что-то делать. Например удалить или вставить новый после него. Начнём со вставки. Что бы вставить элемент нам надо соответствующим образом изменить указатели next у предшествующего и prev у следующего. Т.е. если вначале у нас был такой список: ...---->+------+<---| |---->+------+ | data | | | | data | +------+ | | +------+ | next | | | | next | | ---------+--- | ------------->.... +------+ | +------+ | prev | | | prev | ...<--------- | |------------- | +------+ +------+ тогда, изменив указтели мы получаем такую картинку: ...---->+------+ +------+ | data | | data | +------+ +------+ | next | | next | | ------| | ------------->... +------+ | +------+ | prev | | | prev | ...<--------- | | |---------- | +------+ | | +------+ ^ | \ / ^ | |->+------+ | | | data | | | +------+ | | | next | | | | ----------| | +------+ | | prev | |----------- | +------+ Текст процедуры, которая описывает это приведён ниже: procedure Add (list : p2list; d : integer); var l : plist; begin if list^ = nil then begin AddToEnd (list, d); exit end; New (l); l^.data := d; l^.prev := list^; l^.next := list^^.next; list^^.next := l end; Обратите внимание на "маленькую хитрость" к котороя я прибегнул: если добавляем первый элемент, то тогда вызываем AddToEnd. Попытайтесь понять почему мы так сделали. Процедуру удаления я оставляю на ваше собственное рассмотрение. Так же без комментариев останутся процедуры для добавления/удаления элемента в/из начала. Если у вас возникнут вопросы - пишите. procedure AddToBegin (list : p2list; d : integer); var l : plist; begin if list^ = nil then begin AddToEnd (list, d); exit end; while list^^.prev <> nil do list := @list^^.prev; New (l); l^.data := d; l^.prev := nil; l^.next := list^; list^^.prev := l end; procedure DelFromBegin (list : p2list); var old : plist; begin if list^ = nil then exit; while list^^.prev <> nil do list^ := list^^.prev; old := list^; list^ := list^^.next; list^^.prev := nil; Dispose (old) end; Вот так слово за слово мы и подошли к процедуре вывода списка на экран. Тут я хочу остановится подробнее. Вспомним примерную структуру нашей будующей программы для работы со списком: var list : plist; { это и есть наш список } begin ..... AddTolistEnd (@list, d); ..... В отличии от односвязного списка теперь у нас только один указатель - list. Он у нас и бегает по списку. При этом бегает в прямом смысле слова. Если вы где-то присвоите ему по ошибке nil, то список будет потерян. Давайте рассмотрим пример работы списка. Итак в начале список пуст, но тут мы добавляем первый элемент (* я буду показывать на что сейчас указывает list): *123 добавим элемент в начало: 321 *123 добавим элемент в конец: 321 *123 456 теперь добавим после текущего: 321 *123 0 456 . Теперь найдём элемент 456: 321 123 0 *456 Как вы видите list путешествует по списку (хотя особо не расходится и было бы неплохо добавить в процедуру вывода на экран именно указывать где сейчас list с помощью такой же звёздочки, что мы сейчас благополучно и сделаем: procedure Display (list : plist); var p : plist; begin if list = nil then writeLn ('Список пуст') else begin p := list; while p^.prev <> nil do p := p^.prev; while p <> nil do begin if p = list then write ('*'); write (p^.data, #32); p := p^.next end end end; именно для указания текущего положения мы и вводим дополнительную переменную p с помощь которой осуществляем перемотку. И когда p = list значит это текущий элемент и поэтому мы выводим звёздочку. Ну и наконец я привожу текст программы, работающей с двусвзяным списком. Как и в прошлый раз процедуры, которые я дал вам на домашнее рассмотрение не приведены.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:42 | Сообщение # 27
|
Администраторы
Сообщений: 301
| program listexample; uses CRT; type plist = ^tlist; tlist = record data : integer; next, prev : plist; end; p2list = ^plist; procedure Display (list : plist); var p : plist; begin if list = nil then writeLn ('‘ЇЁб®Є Їгбв') else begin p := list; while p^.prev <> nil do p := p^.prev; while p <> nil do begin if p = list then write ('*'); write (p^.data, #32); p := p^.next end end end; procedure AddToEnd (list : p2list; d : integer); var prior : plist; begin prior := nil; while list^ <> nil do begin prior := list^; list := @list^^.next end; New (list^); with list^^ do begin data := d; next := nil; prev := prior end end; procedure Add (list : p2list; d : integer); var l : plist; begin if list^ = nil then begin AddToEnd (list, d); exit end; New (l); l^.data := d; l^.prev := list^; l^.next := list^^.next; (list^)^.next := l end; procedure AddToBegin (list : p2list; d : integer); var l : plist; begin if list^ = nil then begin AddToEnd (list, d); exit end; while list^^.prev <> nil do list := @list^^.prev; New (l); l^.data := d; l^.prev := nil; l^.next := list^; list^^.prev := l end; procedure DelFromEnd (list : p2list); var old : plist; begin if list^ = nil then exit; while list^^.next <> nil do list := @ list^^.next; old := list^; list^ := nil; Dispose (old) end; procedure DelFromBegin (list : p2list); var old : plist; begin if list^ = nil then exit; while list^^.prev <> nil do list^ := list^^.prev; old := list^; list^ := list^^.next; list^^.prev := nil; Dispose (old) end; procedure Delete (list : p2list); begin writeLn ('Please make it yourself. Have fun ') end; function FindData (list : p2list; data : integer): boolean; var l : plist; begin if list^ = nil then begin FindData := false; exit end; while list^^.prev <> nil do list^ := list^^.prev; l := list^; while l <> nil do begin if l^.data = data then begin FindData := true; list^ := l; exit end; l := l^.next end; FindData := false end; procedure Clear (list : p2list); var old : plist; begin if list^ = nil then exit; while list^^.prev <> nil do list^ := list^^.prev; while list^ <> nil do begin old := list^; list^ := list^^.next; Dispose (old) end end; var list : plist; i : integer; begin while true do begin ClrScr; writeLn ('ЏаЁ¬Ґа а Ў®вл ¤ўгбўп§®Ј® бЇЁбЄ '); writeLn ('1 - ¤®Ў ўЁвм н«Ґ¬Ґв ў з «®'); writeLn ('2 - ¤®Ў ўЁвм н«Ґ¬Ґв ў Є®Ґж'); writeLn ('3 - ¤®Ў ўЁвм н«Ґ¬Ґв ў ⥪гйго Ї®§ЁжЁо'); writeLn ('4 - ©вЁ н«Ґ¬Ґв'); writeLn ('5 - г¤ «Ёвм ЇҐаўл©'); writeln ('6 - г¤ «Ёвм Ї®б«Ґ¤Ё© '); writeLn ('7 - г¤ «Ёвм ⥪гйЁ©'); writeLn ('8 - ®зЁбвЁвм бЇЁб®Є'); writeLn ('9 - ўл室'); writeLn ('>>>>'); writeln(#10, 'CЇЁб®Є:'); Display (list); gotoxy (5, 11); readLn (i); case i of 1: begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); AddToBegin (@list, i); end; 2: begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); AddToEnd (@list, i); end; 3: begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); Add (@list, i); end; 4: begin write ('‚ўҐ¤ЁвҐ зЁб«®: '); readLn (i); if not FindData (@list, i) then begin writeLn ('ќ«Ґ¬Ґв Ґ ©¤Ґ'); readLn end end; 5: DelFromBegin (@list); 6: DelFromEnd (@list); 7: Delete (@list); 8: Clear (@list); 9: break; end end; Clear (@list); end.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:42 | Сообщение # 28
|
Администраторы
Сообщений: 301
| Сегодня у нас будет очень короткий выпуск. Он посвящён использованию такой штуки, как Mem. Mem - это массив памяти. Т.е. с помощью указания номера элемента вы получаете доступ к определенной ячейке. существует 3 вида массива Mem: * сам Mem - каждый компонент такого массива типа byte. * MemW - каждый компонент типа word. * MemL - каждый компонент типа longint. Теперь рассмотрим примеры. Условно обозначим Seg - сегмент, Ofs - смещение в сегменте, b, w, l - переменные соответственно типов byte, word и longint. * b := Mem [Seg:Ofs]; - в b байт с указанного адреса. * w := MemW[Seg:Ofs]; - в w 2 байта с адреса * l := MemL[Seg:Ofs]; - в l 4 байта это мы читали из памяти. Для записи же в память надо присвоить элементу массива нужное число: * Mem[Seg:Ofs] := b; - запишет 1 байт по адресу * MemW[Seg:Ofs] := w; - запишет 2 байта по адресу * MemL[Seg:Ofs] := l; - запишет 4 байта по адресу Еще одна тонкость. Число ABCDEF12h будет записано в памяти в следующем виде: 12EFCDABh, т.е. наоборот. Вот и всё Теперь я рассмотрю несколько примеров для закрепления материала. Например помните, как мы с помощью указателя изменяли первый символ на экране? Давайте провернём тот же фокус с использованием Mem. begin Mem[$B800:0] := 1; Mem[$B800:1] := $84 end. -рисует вместо первого символа красную мигающую рожицу на чёрном фоне. Если вы забыли, как устроена видеопамять в текстовом режиме или не читали об этом, то вам лучше вернуться к выпуску #16 Путешествие по памяти. Продолжим и сделаем тоже самое, но с использованием MemW: begin MemW[$B800:0] := $8401 end. вот тут обратите внимание на тут тонкость о которой я говорил - запись идёт в обратном порядке. Если бы мы написали MemW[$B800:0] := $0184 - т.е. сначала символ, а потом аттрибут. То мы бы увидели синюю букву Д, а не красную рожицу. Давайте теперь изменим 4 байта в видеопамяти, т.е. 2 первых символа на экране с помощью MemL: begin MemL[$B800:0] := $24028401 end. Опять таки обратите внимание на обратный порядок записи - сначала идёт байт-аттрибут 2-го символа, потом второй символ, потом байт-аттрибут 1-ог символа и наконец сам 1-й символ. Теперь давайте считывать из памяти. Напомня, что слово по адресу 0:413 содержит информацию BIOS'a о наличной памяти в килобайтах. Давайте распечатаем это число. begin writeLn (MemW[0:$413]) end. в общем и целом тут нет ничего сложного. Поэтому думаю, что эту тему можно считать закрытой. colspanЗадание В прошлый раз я не дал объяснения как найти предыдущий элемент в списке с одной связью (list на входе указывает на текущий элемент списка, start на его начало). Ниже приводится текст процедуры с комментариями. procedure findPrior; var l : plist; begin if (start = list) or (list = nil)then exit; { предыдущего нет } l := list; { сохраняем текущий в переменной l } list := start; { перематываем список с начала } while list <> nil do begin if list^.next = l then exit; { нашли элемент, next которого указывает на текущий элемент, на выход } list := list^.next end end; если мы не смогли найти предыдущий элемент (например в случае если list принял ошибочное значение и вылез куда-то за границу списка), то при выходе list = nil. В случае удачного выполнения в list и окажется предыдущий элемент.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:43 | Сообщение # 29
|
Администраторы
Сообщений: 301
| До этого наши программы работали в текстовом режиме. Кроме него существует ещё и графический. В текстовом режиме вам доступны только 256 символов псевдографики (как бы графики), которые вы можете отобразить на экране. Этих символов достаточно для простых графических работ, например: нарисовать таблицу, написать карточную игру, оболочку (типа Norton Commander), многоконный тестовый редактор (взять даже BP) и для много другого. Графический же режим характеризуется возможностью задавать цвет любой точки экрана. И соответственно мы можем рисовать сложные геометрические фигуры. Для работы с графикой в комплект поставки BP входит модуль graph, в котором содержатся основные функции и процедуры. Так же для работы нужны специальные графические драйвера, которые тоже входят в BP и находятся в папке BP\BGI. Кстати модуль graph называется библиотекой BGI (Borland Graphic Interface). Прежде чем начать работу с графикой нам надо перейти в графический режим, т.к. наша программа запускается по умолчанию в текстовом. И соответственно перейти назад в текстовый после завершения работы. Поэтому каркас программы превращается в нечто более сложное, чем было до этого: uses Graph; var grDriver: Integer; grMode: Integer; Errcode: Integer; begin grDriver := Detect; InitGraph(grDriver, grMode,''); Errcode := GraphResult; if Errcode <> grOk then begin Writeln('Graphics error:'); Writeln(GraphErrorMsg(Errcode)); Writeln('Program aborted...'); Halt(1) end ..... do something ... Readln; CloseGraph end. ну что неплохое начало Эта программа "ничего" не делает. Просто переходт в графический режим, ждёт нажатия клавиши и выходит назад в текстовый. Теперь давайте разберём, что же мы тут использовали: procedure InitGraph(var GraphDriver:Integer; var GraphMode: Integer; PathToDriver: string); процедра инициализации графики. Т.е. по просту говоря для перехода в графический режим. GraphDriver - графический драйвер, которым мы хотим пользоваться. Мы задаём этому параметру значение Detect - авто определение(т.е. программа сама определит наилучший режим). Для современной техники это 640x480x16 GraphMode определяет графический режим, т.к. у нас стоит автоопределение он определится сам (это и будет режим 640х480 16 цветов). PathToDriver - путь к файлу драйвера. Если путь не задаётся, как это сделали мы, то драйвер ищётся в текущем каталоге программы. Если использовать автоопределение, то нам нужен файл egavga.bgi из папки bp\bgi. На всякий случай, вдруг кто не знает скажу кое-что об обозначениях что значит 640х480х16 - то точная характеристика графического режима: (число точек по горизонтали)х(число точек по вертикали)х(количество цветов). Т.е. 640 точек по горизонтали, 480 по вертикали и 16 цветов. В интернете сейчас можно найти версии файлов *.bgi для работы в более лучших режимах. Однако я всё же буду рассказывать про стандартную поставку, т.к. углубляться в графику мы пока особо не будем. Всему своё время. Следующая функция возвращает нам резльтат предыдущей функции или процедуры, если возникла ошибка: function GraphResult: Integer; Если возникла ошибка (например при инициализации графики программа не нашла файл с драйвером), то возвращаемое значение не равно grOk. Расшифровку ошибки можно получить вызвав: function GraphErrorMsg(Errorcode: Integer): string; возвращает строку, содержащую содержание ошибки, заданной переменной Errorcode. Не помню писал ли я про halt, так что если повторюсь, то ничего: procedure Halt ( Exitcode: Word ); останавливает выполнеие программы и передаёт управление операционной системе. Если параметр Exitcode = 1 значит это завершение программы с ошибкой. Ну и последняя процедура: procedure CloseGraph; переходит назад в текстовый режим. Обратите внимание на вызов ReadLn перед вызовом CloseGraph. Зачем мы это делаем? Если опустить этот вызов, то после выполнения программы сразу произойдёт выход в текстовый режим и соответственно экран очистится. Что бы посмотреть результаты выполнения программы мы вынужденны ожидать нажатия клавиши. Если вы думаете, что это сложно, то вы ошибаетесь, что бы создать простое пустое окошко в программе для Windows надо написать раза в три-четыре больше Этот код будем считать каркасом наших будующих графических программ. Теперь самое время поговорить о координатах. Всё дело в том, что в компьютерах координатные оси направленны по другому. Для примера: мы пользуемся декартовой системой координат ( © by Рене Декарт :), центром которой является точка (0,0) опять же извиняюсь за убогость, но картинку не приаттачить: ^ y | | | --------+-------> | x | | В компьютерах же используется несколько другая система +-----------------------> | x | | | | | | V y Как видите в ней отсутствуют отрицательные числа и ось ординат (y) направленна вниз. Вызвано это следующей причиной. Разрешение экрана определяют в пикселях. Пиксель (сокращение от Picture Element - элемент картинки) - это минимальная логическая точка (или если хотите единица) экрана. Т.е. разрешению экрана 800х600 означает, что по оси Х у нас максимально 800 пикселей, а по оси Y - 600. Один пиксель может состоять из множества физических точек (т.е. точек подсветкой которых можно управлять аппаратно). Так вот левому верхнему углу экрана соответствеут пиксель с координатами (0,0), а правому нижнему в зависимости от разрешения. Если оно 800х600 тогда (799, 599). Пиксель - это всегда целое положительное число. У вас не может быть 1/3 пикселя или пиксель с координатой (-5, -5). Так как мы не можем посветить на экране 1/3 пикселя и мы не можем высветить пиксель (-5, -5) т.к. это выпадает за границу экрана. Кстати количесто цветов на экране определяется количеством бит видеопамяти, отводимой под 1 пиксель. Так если под один пиксель отводится 1 байт (8 бит) тогда это режим с 256 цветами (напомню, что столько может быть различных комбинаций бит в байте). Теперь рассмотрим самую важную процедуру в графике - подсветка пикселя: procedure PutPixel(X, Y: Integer; color: Word); соответственно х, у - координаты. Color - цвет. Кстати о цветах. Для первых 16 цветов введены специальные константы. Ниже приводится таблица соответствия цветов и номеров (для текстовой версии к сожалению графа пример останется незаполненной, т.к. нет такой возможности): Номер цвета Название константы Русское название (для текстовой версии) Примерчик 0 BLACK чёрный 1 BLUE синий 2 GREEN зелёный 3 CYAN голубой 4 RED красный 5 MAGENTA фиолетовый 6 BROWN коричневый 7 liGHTGRAY светло серый 8 DARKGRAY тёмно серый 9 liGHTBLUE светло синий 10 liGHTGREEN светло зелёный 11 liGHTCYAN светло голубой 12 liGHtrED розовый 13 liGHTMAGENTA светло фиолетовый 14 YELLOW жёлтый 15 WHITE белый поэтому putpixel (10, 10, 15) и putpixel (10, 10, WHITE) произведёт одинаковый эффект. Таким образом мы можем рисовать любые фигуры, используя посветку точек на экране. Однако в BGI есть специальные процедуры для рисования графических примитивов - линий, окружностей, прямоугольников, вывода текста, работы с изображениями и многим другим. К сожалению в формат выпусков не влезет описать все возможности BGI, да многое и не понадобится. Поэтому проведу обзор самых нужных функций и процедур. function GetPixel(X,Y: Integer): Word; возвращает цвет точки с координатами х,у procedure line(x1, y1, x2, y2: Integer); рисует линию от точки (x1,y1) к точке (x2, y2). Цвет линии устанавливается с помощью следующей процедуры: procedure SetColor(Color: Word); устанавливает текущий цвет для рисования графических объектов. По умолчанию цвет белый. procedure SetBkColor(ColorNum: Word); устанавливает цвет фона, по умолчанию цвет фона чёрный. Цвет фона - это цвет экрана на котром мы рисуем. Что-то типа цвета рабочего стола в Windows procedure Circle(X,Y: Integer; Radius: Word); рисует окружность с центром в точке (x,y) и радиусом Radius. Цвет окружности задаётся с помощью SetColor. procedure Ellipse(X, Y: Integer; StAngle, EndAngle: Word; XRadius, YRadius: Word); рисует элипс с центром в точке (x,y), полу-осями xradius, yradius, от угла stangle до угла endangle. Угол считается против часовой стрелки (т.е. 00 - 3 часа, 900 - 12 часов). Цвет задаётся с помощью SetColor. procedure Bar(x1, y1, x2, y2: Integer); рисует закрашенный прямоугольник. левый верхний угол (х1,у1) - правый нижний (х2, у2). Стиль кисти и цвет закраски определяется следующей процедурой: procedure SetFillStyle(Pattern: Word; Color: Word); устанавливает текущий стиль кисти (pattern) и её цвет (color). Цвет, который задаётся setcolor и setfillstyle не совпадают и применяются к разным графическим объектам. Первый к графическим примитивам, второй к закрашенным объектам. Существует несколько предопределённых стилей, которые приведены в нижеследующей таблице: Констнанта Численное значение Описание EMPTY_FILL 0 Закрашивает цветом фона (несмотря на цвет, который вы указали в setfillstyle) SoliD_FILL 1 Равномерная закраска liNE_FILL 2 Закрашивает --- LTSLASH_FILL 3 Закрашивает /// SLASH_FILL 4 Закрашивает ///, толстые линии BKSLASH_FILL 5 Закрашивает \\\, толстые линии LTBKSLASH_FILL 6 Закрашивает \\\ HATCH_FILL 7 В "клеточку" XHATCH_FILL 8 В клеточку под углом INTERLEAVE_FILL 9 Чередование WIDE_DOT_FILL 10 Широко расположенные точки CLOSE_DOT_FILL 11 Близко расположенные точки USER_FILL 12 Стиль определять вам! что бы лучше понять напишите программу, которая бы в цикле выводила прямоугольники и меняла стиль, тогда вы поймёте что к чему. procedure FillEllipse(X, Y: Integer; XRadius, YRadius: Word) рисует закращенный элипс с центром (х,у) и полу-осями xradius, yradius. Стиль и цвет определяются Setfillstyle procedure ClearDevice; очищает экран (заполняет его цветом фона). procedure OutTextXY(X,Y: Integer; TextString: string); выводит строку textstring, начиная с точки (х,у). В графическом режиме для вывода нужно обязательно использовать эту процедуру. procedure GetImage(x1, y1, x2, y2: Integer; var BitMap); копирует картинку в память. Картинка определяется координатами (x1, y1) - левый верхний угол; (x2, y2) - правый нижний угол. BitMap - это область куда мы должны копировать картинку. Размер области памяти складывается из количества необходимого под картинку + 6 байт. Два первых слова этих байт хранят ширину и высоту картинки, третье слово зарезервировано и зачем оно нужно это охраняемая тайна компании Borland procedure PutImage(X, Y: Integer; var BitMap; BitBlt: Word); соответственно обратная процедура. Рисует изображение из BitMap на экран, начиная с координат (х,у). BitBlt - определяет способ вывода картинки. Ниже даны его возможные значения: Константа Численное значение Смысл NormalPut 0 Копирует изображение на экран (цвет каждого пикселя сохраняется) XORPut 1 Результат получается как XOR, применённый к точке на экране и точке изображения OrPut 2 как OR, применённый к точке на экране и точке изображения AndPut 3 как AND, применённый к точке на экране и точке изображения NotPut 4 Цвет каждого пикселя инвертируется (заменяется на противоположный) function ImageSize(x1, y1, x2, y2: Integer): Word; эта функция используется, что бы вычислить размер памяти BitMap в процедуре GetImage, т.е. размер необходимый под сохранение области + 3 слова. function GetMaxX: Integer; / function GetMaxY Integer; возвращает максимальное значение Х / Y координаты. procedure FloodFill(X, Y: Integer; Border: Word); закрашивает область, которая содержит точку (x,y) и ограничена кривой с цветом Border. Стиль и цвет определяются Setfillstyle это далеко не полный список процедур и фунций из библиотеки BGI. Что бы понять как они работают лучше всего, что бы вы попробовали вызвать их поочереди и посмотреть что будет. Кстати хороший пример работы этих и других функций и процедур находится в программе BP\EXAMPLES\DOS\BGI\bgidemo.pas. Сейчас я как раз разберу пример от туда. Это пример с НЛО запустите и посмотрите, так как надо объяснять пользуясь картинкой. Если НЛО летает медленно, тогда в самом конце измените задержку (например на delay (1000) - в 10 раз быстрее) так как на моём P4 за НЛО не уследить этот параметр в примере равен кстати 70 program demo; uses Crt, Graph; const r = 20; StartX = 100; StartY = 50; var grDriver, grMode, Errcode : integer; MaxX, MaxY : word; Saucer : pointer; X, Y : integer; ulx, uly : word; lrx, lry : word; Size : word; I : word; procedure MoveSaucer(var X, Y : integer; Width, Height : integer); var Step : integer; begin Step := Random(2*r); if Odd(Step) then Step := -Step; X := X + Step; Step := Random®; if Odd(Step) then Step := -Step; Y := Y + Step; if X > MaxX then X := MaxX else if (X < 0) then X := 0; if Y > MaxY then Y := 1 else if Y < 0 then Y := 0 end; begin grDriver := Detect; InitGraph(grDriver, grMode,''); Errcode := GraphResult; if Errcode <> grOk then begin writeLn ('Graphics error:', GraphErrorMsg(Errcode)); halt (1) end; ClearDevice; MaxX := getmaxx; MaxY := getmaxy; { рисуем НЛО } Ellipse(StartX, StartY, 0, 360, r, (r div 3)+2); Ellipse(StartX, StartY-4, 190, 357, r, r div 3); line(StartX+7, StartY-6, StartX+10, StartY-12); Circle(StartX+10, StartY-12, 2); line(StartX-7, StartY-6, StartX-10, StartY-12); Circle(StartX-10, StartY-12, 2); SetFillStyle(SolidFill, WHITE); FloodFill(StartX+1, StartY+4, GetColor); { вычисляем границы прямоугольника в который вмещается НЛО } ulx := StartX-(r+1); uly := StartY-14; lrx := StartX+(r+1); lry := StartY+(r div 3)+3; Size := ImageSize(ulx, uly, lrx, lry); GetMem(Saucer, Size); GetImage(ulx, uly, lrx, lry, Saucer^); PutImage(ulx, uly, Saucer^, XORput); { рисуем звёздное небо } for I := 1 to 1000 do PutPixel(Random(MaxX), Random(MaxY), random (WHITE)); X := MaxX div 2; Y := MaxY div 2; repeat PutImage(X, Y, Saucer^, XORput); Delay (10000); PutImage(X, Y, Saucer^, XORput); MoveSaucer(X, Y, lrx - ulx + 1, lry - uly + 1); until Keypressed; FreeMem(Saucer, size); ReadLn; closegraph end. Сначала одна неизвестная до этого момента функция function Odd(X: Longint): Boolean; проверяет является ли число нечётным. и возвращает true в случае успеха. Так вот думаю вы ужу понаблюдали за НЛО, теперь разберём как это происходит. Процедура MoveSaucer не осуществляет ничего сверх естественного и просто изменяет координаты НЛО, которые хранятся в глобальных переменных Х,Y. На ней подробно останавливаться думаю не стоит. Так же я не буду комментировать процесс рисования объекта, так как это просто последовательный вызов графических функций безо всяких алгоритмов. Я бы хотел обратить ваше внимание на следующие строки: Size := ImageSize(ulx, uly, lrx, lry); - вычисляем память, которая нам нужна для хранения картинки НЛО GetMem(Saucer, Size); - выделяем эту память GetImage(ulx, uly, lrx, lry, Saucer^); - захватываем изображение НЛО в буффер Saucer. PutImage(ulx, uly, Saucer^, XORput); - стираем НЛО тут придётся вспомнить логические операции (как неужели вы их забыли !!!). Если забыли, тогда вернитесь к выпуску "#0E А сила - она, брат, в правде". Так вот что будет, когда мы число xor'им само с собой (a xor a) ??? Правильно 0. То же самое происходит и когда мы вызываем PutImage(ulx, uly, Saucer^, XORput). Ведь в Saucer у нас находится изображение НЛО, когда же мы накладываем изображение само на себя используя операцию xor, то получается 0, т.е. изображение цвет всех точек которого равен 0. А так как цвет фона у нас чёрный (0) то получается эффект стирания, а не перерисовки. Продолжим: repeat PutImage(X, Y, Saucer^, XORput); - рисуем НЛО Delay (10000); - пауза PutImage(X, Y, Saucer^, XORput); - стираем MoveSaucer(X, Y, lrx - ulx + 1, lry - uly + 1); - двигаем НЛО until Keypressed; вроде бы ничего сложного, но задумайтесь на минутку почему остаются звёзды ? Давайте поставим задержку (delay) совсем большой и присмотримся к НЛО .... через него просвечивают звёзды! (что бы лучше это рассмотреть увеличте количество звёзд ). Всё из-за того что способ применённый здесь не является правильным. Хотя согласитесь рассмотреть эти маленькие точки тяжело и искажения не очень заметны. Теперь рассмотрим внимательнее исходник. Когда мы рисуем НЛО мы опять применяем операцию xor - поэтому если под НЛО находится звёздочка, то она просвечивает, причём с искажением цвета. Однако когда мы стираем НЛО мы вновь применяем xor и НЛО стирается, а звездочке возвращается свой "первозданный" цвет. Давайте обратимся к цифрам. Пускай у нас будет звезда жёлтого (14) цвета. 14 = 1110b 1111b xor 1110b = 0001b - синий цвет. это в момент рисования. 0001b xor 1111b = 1110b - жёлтый. в момент стирания. Как говорят математики ЧТД. Данную анимацию можно применять для взаимно обратных цветов, т.е. по цветам с одинаковым отступом от концов палитры (палитра - набор цветов, каждому цвету соответствует номер в палитре, помните таблицу констант?). Такими цветами и являются: чёрный-белый (0 -15), синий-жёлтый (1-14) и т.д. Про правильную анимацию я раскажу как нибудь в другой раз.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:43 | Сообщение # 30
|
Администраторы
Сообщений: 301
| Сегодня мы поговорим о связи этих двух языков - Паскаля и Ассемблера. Начну как всегда с неких общих понятий. Наши программы пишутся не только под определённую ОС, но и под определённый процессор. В нашем случае это Intel и совместимые сним. У каждого процессора есть своя система команд. Он не понимает чего либо другого. На компьютере с процессором не совместимым с Intel (т.е. имеющим другую систему команд) наши программы работать не будут. В exe файле помимо заголовка, который определяет, что это за программа, и содержаться комманды процессору. Это и есть занятие транслятора - перевести с языка высокого уровня (в нашем случае Паскаль) на машинный язык, понятный процесссору. Поэтому мы можем писать программы прямо на понятном процессору языке машинных кодов. Давайте ради прикола напишем такую програмку. Создайте пустой файл например с именем my_low_level_prog.com (именно com, так как в нём нет заголовка, характерного для ехе файла) И запишите в него последовательность символов с номерами CD-20h. Для этого откройте её блокнотом и впишите два символа: "Н ". Русская буква Н и пробел (в кодировке Win разумеется). После этого можете сохранить файл и запустить программу на исполнение. Правда результата работы вы не увидете. Эта программа ничего не делает (если это понятие можно применить к программе). Единственная команда процессору в этой программе закодирована двумя байтами CD-20h. Это команда выхода из программы Своебразный аналог end. в Паскале. Продолжая далее можно попробовать написать что-то большее, однако думаю что делать этого не стоит. Для того что бы не писать команды процессору таким вот простым, но мало понятным способом и был придуман язык низкоуровнего программирования ассемблер. Он переводит команды процессора в понятный и приятный для глаза вид. Например CD-20h на языке ассемблер выглядит как int 20h - согласитесь глаз радуется, душа поёт Однако что бы ещё более облегчить жизнь программистам придумали более сложные языки, которые назвали языками высокого уровня. В этих языках программирования вы не обращаетсь непосредственно к процессору - вы пишите программы на языке более приближенном к человеческому. Это имеет свои плюсы - вам сразу понятна программа. И свои минусы - при вам неизвестно в какие машинные коды переводит вашу программу компилятор. Программируя на языке ассемблер вы можете получить существенный выйгрыш в скорости выполнения и в объёме выполняемого файла. На данный момент очень мало программ пишутся целиком на ассемблере - в основном это вирусы и трояны. А сам язык применяется совместно с языком высокого уровня в критических местах программ, требующих очень большой скорости выполнения. Мне например думается, что часть таких программ как MS Office или Adobe Photoshop была написана именно на языке ассемблер. Сегодня я и собираюсь рассказать как встраивать куски кода на ассемблере в ваши программы. BP 7 подерживает набор инструкций 2-х процессоров - 8086 и 80286. По умолчанию используется именно 8086. Что бы включить набор 286 надо влезть в Options -> Compiler и поставить галочку 286 instructions. Для вставки команд в тело программы надо использовать следующий оператор: asm текст на языке ассемблер end Например напишем простую программку выводящую на экрана строку Hello world!: program test; label Next, Msg; begin asm jmp Next Msg: db 'Hello world!$' Next: push ds push cs pop ds mov ah, 9 mov dx, offset Msg int 21h pop ds end end. Вот такая вот программка. Теперь давайте разберём что к чему и зачем. Думаю все догадались, что ассемблерный код вставится непосредственно в сегмент кода. А что это значит? Это значит, что процессор попытается исполнить все инструкции, даже те которые мы ему не давали. Строка db 'Hello world!$' запишет в ехе файл строку Hello world!$ (можете убедится в этом сами, открыв его в текстовом редакторе). А помните как процессор выполнил сочетание буквы Н и пробела? Правильно надо не дать ему выполнить инструкции, которые имеют коды 'Hello world!$'. Поэтому надо обойти эту строку. Поэтому мы используем команду jmp Next. Синтаксис её таков: jmp МЕТКА переходит на метку. Аналог уже знакомого нам goto. Думаю что с этой командой всё понятно jmp Next - переходит к строчке, помеченной Next. Дальше мы ставим метку Msg. Это нам понадобится, что бы как-то пометить строку db 'Hello world!$', что бы к ней вернуться. Следубщие строки пока пропускаем и переходим сразу к mov ah, 9. Тут мы сделаем долгую остановку. Вы уже привыкли пользоваться переменными, создавать их столько сколько нужно и давать им любые имена. Однако сюрприз. При программировании на ассемблере для навороченных вычислений разрешается использовать только несколько переменных с фиксированными именами собственными и имеющих фиксированный размер. Эти переменные называются регистрами. Например процессор 8086 имеет 14 регистров (посмотреть на них можно выбрав Debug -> Register). На все них мы не будем останавливаться подробно. Сейчас я раскажу о регистрах общего назначения. К ним относятся регистры с именами AX, CX, DX, BX. Каждый регистр имеет размер в 1 слово (2 байта). Обычно в этот момент рисуют вот такую схемку: AX BX CX DX AH AL BH BL CH CL DH DL О чем она нам говорит? О том, что каждый дву байтовый регист состоит из 2-х однобайтовых (логично не правдо ли :). Левый байт называют старшей частью, а правый младшей. Соответственно и буква после имени H (High - старший) и L (Low - младший). Поэтому команда mov ah, 9 делает что-то со старшей частью ax, а mov dx, offset Msg с регистром dx. Очевидно, что присвоить значение регистру можно 2-мя способами: 1. присвоить сразу значение: AX = 32D2h 2. присвоить значения старшей и младшей части отдельно: AH = 32h, AL=D2h Точно также значение старшей (младшей) части можно присвоить двумя способами: 1. присвоить непосредственно: AH = 32h 2. или через AX = 3200h Кстати так как язык ассемблера представляет собой команды процессора, то команды равно ( = ) нет. Для присвоения регистру какого либо значения надо пользоваться командой mov (от англ. move - движение, хотя в данном контексте переводится, как загружать). Т.е. когда мы пишем mov ah, 9 - мы загружаем в AH 9. По русски это так AH = 9. Пропустим пока mov dx, offset Msg и перейдём сразу к строчке int 21h. Команда int - имеет такой синтаксис: int [номер] при вызове этой команды происходит прерывание (от англ. Interruption - прерывание). Что такое прерывание? Прерывание - это своего рода подпрограмма, которая находится постоянно в памяти и может вызываться в любое время из любой программы. Т.е. выполнение нашей программы прерывается и осуществляется переход на обработку прерывания. Прервание хотя и записывается одной командой на самом деле состоит из нескольких. После обработки (выполнения) прерывания программа продолжит работу. Если вы не поняли привожу замечательный пример из рассылки "Низкоуровневое программирование для дZенствующих": Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу. И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете). Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то, включить свет ... вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы остановились перед выполнением программы "поход в туалет". В данном случае наш мозг выполняет роль процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание проводится "программой-навыком", заложенным в процессе нашего развития и (хм!) воспитания. В нашей программе мы вызываем int 21h - прерывание 21h (читается двадцать первое прерывание). Это прерывание DOS. Каждое прерывание имеет несколько функции. Т.е. совершает различные действия в зависимости от номера функции. Номер функции должен быть помещён перед вызовом прерывания в ah (что мы и делаем mov ah, 9). Функция 9 прерывания 21h - это вывод строки на экран. Так же в зависимости от нужд прерывание может получать дополнительные параметры через другие регистры. Вполне логично, что для функции вывода строки на экран нам надо передать эту строку в прерывание. Этим и занимается mov dx, offset Msg. Так как dx может содержать всего 2 байта (а строка наверно будет длинее :), поэтому в dx должен быть адресс строки. Так вот offset Msg - это смещение до метки Msg (помните мы ей пометили Hello world). Однако нужно вспомнить, что в адресе помимо смещения есть ещё и сегмент. Откуда он берётся? Для этого существуют специальные сегментные регистры. Их имена - CS DS SS ES. Эти регистры уже не делятся на младшую и старшую часть, так они несут адрес сегмента. При этом подразумеваются следующее их назначение: * Регистр CS служит для хранения сегмента кода программы (code Segment - сегмент кода); * Регистр DS - для хранения сегмента данных (Data Segment - сегмент данных); * Регистр SS - для хранения сегмента стека (Stack Segment - сегмент стека); * Регистр ES - дополнительный сегментный регистр, который может хранить любой другой сегмент (например, сегмент видеобуфера). Сразу скажу, что загрузка напрямую в сегментный регистр запрещена! Делать это можно например так: mov ax, 0B800h mov es, ax Так вот, возвращаясь к 9 функции 21h прерывания - по умолчанию предпологается, что строка у нас находится в сегменте данных. Т.е. адресс строки на входе выглядит так - DS:DX. Однако наша то строка находится в сегменте кода. Т.е. нам надо присвоить DS значение CS. Делаем мы это через стек. Стек - область памяти, используемая программой для временного хранения данных. Работа со стеком заключается в простом правиле - первый пришёл, последний ушёл Для работы со стеком нам понадобятся всего 2 команды: push и pop. Синтаксис у них такой: PUSH приемник POP приемник PUSH - записывает в стек, POP - извлекает из стека. Соответственно: push AX - ПОМЕЩАЕТ В СТЕК значение регистра AX pop AX - ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX Вернёмся к нашей программе: push ds push cs pop ds ........ pop ds Итак первым push'eм мы сохраняем в стеке значение регистра ds (который сейчас указывает на сегмент данных). Вторым push'eм - значение cs (который указывает на сегмент кода). Что же мы извлекаем первым pop'ом? Давайте вспомним правило - первый пришёл, последний ушёл или же если сказать нооборот последний пришёл - первый ушёл. Значит первым pop'ом мы извлечём из стека значение cs. Т.е. после pop ds регистр DS = CS (что нам и было нужно). Второй pop нам нужен, что бы во первых вернуть стек в его первоначальное состояние (т.е. состояние до выполнения нашего кода). Во вторых что бы востановить значение DS. Здесь нам были не нужны никакие переменные, а если они будут, то программа будет лезть в сегмент данных (т.е. в DS), а он у нас указывает на код.... вообщем получится полная лабуда. Несмотря на кажащуюся простоту (всё так просто не правда ли со стеком надо обращаться аккратно.Ведь помимо нас со стеком работают и другие фукции программы. Например вы никода не задумывались как после выполнения процедуры программа знает куда ей вернуться? Оказывается адресс возврата предварительно записывается в стек и после выполнения процедуры извлекается от туда, и осуществлляется переход на него. А если внутри процедуры мы положили в стек число 3 и забыли его извлечь? Программа то не разберётся и решит, что это адрес возврата и перейдёт на него... что произойдёт дальше я боюсь предсказывать. По этотму не забывайте извлекать из стека, то что в него занесли. И что бы укрепить пройденное рассмотрим несколько примеров. Допустим, мы помещаем в стек следующие регистры: AX, BX, CX: push ax push bx push cx Восстанавливать со стека нужно в обратном порядке: pop cx pop bx pop ax Если вы поменяете местами регистры при восстановлении, то ничего страшного не произойдет, только содержать они будут другие числа. Например: mov ax,1234h mov bx,5678h push ax push bx pop ax pop bx В итоге AX будет равен 5678h, а BX - 1234h. И тепрь ещё раз в кратце прокоментирую каждую строчку программы: jmp Next - переходим на метку Next Msg: db 'Hello world!$' - наша строка, содержащаяся в сегменте кода! Next: push ds - сохраняем в стеке адрес сегмента данных push cs - сохраняем в стеке адрес сегмента кода pop ds - извлекаем его из стека и записываем в ds, т.е. DS = CS mov ah, 9 - записываем в ah - номер функции, AH = 9 mov dx, offset Msg - в dx смещение до строки int 21h - вызываем 21h прерывание pop ds - восстанавливаем ds в первоначальное значение Чуть не забыл Вы обратили внимание на строку: Hello world!$. Зачем тут $ ? Просто 9-ая функция 21h прерывания считает $ признаком конца строки. Т.е. она будет выводить на экран символы начиная с адреса DS:DX пока не встретит $. Например уберите его из строки и вы увидите как редко $ встречается в памяти Итак теперь вы знаете 8 регистров. Назову ещё несколько: * Регистр командного указателя IP Он содержит смещение на команду, которая должна быть выполнена. Обычно этот регистр в программе не применяется. * Индексные указатели SI и DI Регистры SI (Source Index register - индекс источника) и DI (Destination Index register - индекс приемника) применяются для расширенной адресации и для использования в операциях сложения, вычитания и в некоторых строковых. Например адрес видеобуффера занесём в es, а смещение в di... ну и изменим первый символ: mov ax,0B800h mov es,ax - записываем в es адресс начала видеобуффера mov di,0 - в di смещение относительно начала mov ah,31 - байт аттрибут mov al,1 - код символа mov es:[di],ax Придётся опять вспомнить организацию видеопамяти. Квадратные скобки ( [ ] ) в команде mov es:[di],ax указывают на то, что надо загрузить число не в регистр, а по адресу, который содержится в этом регистре (в данном случае - это 0B800:0000). * Регистры-указатели SP и BP о них я раскажу позже. * Флаговый регистр (или регист флажков). Очередной сюрприз - этот регист не имеет имени. Так как нас интересует не сам регист, а состояние некоторых его бит, которые имеют свои имена. Если посмотреть окно Registers, то они занимают 2 нижние строчки. О них я тоже раскажу в следующий раз.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:44 | Сообщение # 31
|
Администраторы
Сообщений: 301
| В прошлый раз мы остановились на регистре флажков. Это регистр содержит такие флаги (* - не используемые биты): Номер бита 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 Флаг С * P * A * Z S T I D O * * * * Некоторые команды устанавливают определённые значения флагов при ошибках или для других надобностей. Например команда сранения cmp (CoMPare - сравнить). Сравнивает 2 операнда. В зависимости от результата устанавливает флаги. Например сравним bx и 0: cmp bx, 00 Как же узнать какой флаг установился, а какой нет? Для этого есть команды для перехода на метку, если флаг установлен. Всех их писать я не буду. Но например, если в предыдущем примере BX = 0, тогда сравнение вызовет установку флага ZF (Zero Flag - флаг нуля). Команда jz вызывает переход на метку, если установлен ZF. Для нашего примера: cmp bx, 00 jz Label1 ....это будет выполняться, если bx не равно 0 ..... Label1: ... здесь если bx равно 0 Во встроенном ассемблере могут использоваться три предопределённых имени: * @code - текущий сегмент кода * @Data - текущий сегмент данных * @Result - ссылка, куда надо занести результат выполняемой функции Остановимся подробнее на @Result. Например напишем функцию, возвращающую наименьшее из 2-х чисел. function Min (x,y : integer): integer; begin asm mov ax, x cmp ax, y jl @less mov ax, y @less: mov @Result, ax end end; Обратим взгляд на неизвестную нам команду jl - переход если меньше. Т.е. в нашем случае если AX меньше Y, то совершается переход на метку @less. Кстати откуда такое странное название (начинается с @)? Это так называемые локальные метки - метки в которые можно передать управление только внутри ассемблерной вставки. Т.е. вы не можете прыгнуть на неё с помощью goto. С другой стороны нам не надо объявлять такую метку в разделе label, так что начинайте свои метки с символа @. Этот символ говорит компилятору, о том что метка локальная. Так вот значит, если AX меньше Y (т.е. если X меньше Y), тогда мы переходим на метку @less. И записываем в @Result значение АХ (а там у нас Х). Если же Y меньше Х, тогда переход на метку не состоится и мы сначала занесём в АХ Y, а потом значение АХ в @Result. Вот таким образом компилятор преобразовывает наши высоко уровневые конструкции if-else к машинным cmp. Естесственно в силу превосходства человека над машиной, наш код меньше, чем генерируемый компилятором. Однако давайте обратим свой взор на использование функций процедур, написанных на ассемблере. Во перых объявление таких подпрограмм должно идти с директивой assembler. Во вторых исполняемая чать вместо привычного begin - end обрамляется asm - end. Т.е. шаблон выглядит так: procedure NAME; assembler; asm .... текст на ассемблере end При этом ассемблерные функции должны возвращать свои значения следующим образом: * если возвращаемый тип занимает 1 байт (тип Byte, Char) - в регистре AL * 2 байта (Integer, word) - в AX * 4 байта (pointer, longint) - в регистре DX (старшее слово) и AX (младшее слово) * типа Real - в регистрах DX, BX, AX (DX - старшее слово и AX - младшее слово) * других вещественных чисел - в регистре ST (0) сопроцессора (об этом ниже) * строкового типа - в области памяти на которую ссылается @Result Как видите в ассемблерных функциях разрешено использовать @Result только, если возвращаемое значение строкового типа. Так как наша функция Min не содержит "паскалевских" строк перепишем её в виде ассемблерной подпрограммы: function Min (x,y : integer): integer;assembler; asm mov ax, x cmp ax, y jl @less mov ax, y @less: end; так как мы возвращаем значение типа integer, то нам надо запихать это значение в АХ (что мы и делаем). Обратите внимание, что наш код сократился ещё на одну строку. Нам не надо записывать возвращаемоео значение в @Result. Метка @less теперь у нас соответствует выходу из подпрограммы. Следующие строки будут целиком понятны только для людей знакомых с ассемблером более чем поверхностно. Однако и вам полезно это знать. Итак на входе и выходе в ассемблерную подпрограмму генерируется следующий код (он добавляется в ехе файл): colspanВход в процедуру: push bp - сохраняем значение bp mov bp, sp - bp теперь содержит текущую границу стека sub sp, local - резервируем часть стека для локальных данных, local - длинна в байтах всех объявленных в подпрограмме переменных colspanВыход из процедуры: mov sp, bp - восстанавливаем границу стека pop bp - восстанавливаем bp ret param - удаляем из стека все параметры и выходим из подпрограммы, param - длинна всех параметров Если входные и выходные параметры отсутствуют, то создаётся код содержащий единственную инструкцию ret. Как я уже когда-то писал все параметры передаются в подпрограммы черз стек. Так же все локальные переменные хранятся в стеке. Поэтому при входе в процедуру создается локальный стек: в bp помещается текущая граница стека, а сама эта граница сдвигается на суммарную длинну всех локальных переменных что бы работа со стеком не разрушила локальные переменные. Например: procedure some; assembler; var x : integer; y : byte; asm mov X, ax - скомпилируется в mov [bp - 2], ax mov al, Y - скомпилируется в mov al, [bp - 3] end; Как я написал выше, результат функции других вещественных чисел возвращается в регистре ST (0) сопроцессора. Что же такое сопроцессор? Как следует из названия это "дополнительный" процессор. Сопроцессор - это процессор для работы с числами с плавающей запятой. Вообще-то полное название - арифметический сопроцессор. Этот сопроцессор имеет свою систему команд разработанную специально для ускорения математических расчётов. Раньше не все компьютеры были оснащены сопроцессором. Сейчас он неизменно присутствует на всех машинах. Поэтому компилятор Турбо Паскаля содержит 2 режима компиляции с поддержкой сопроцессора : Options->Compiler раздел Numeric processing. Галочка у 8087/80287 - при компиляции будут использованы команды сопроцессора. Галочка у Emulation - будет использован режим эмуляции. Т.е. при компиляции будет эмулироваться сопроцессор. Помимо арифметического сопроцессора существует куча сопроцессоров. Самый яркий пример - графический, для ускорения работы с графикой (он содержит в себе функции для рисования графических примитивов). Кул-][ацкерам Хотите почувствовать себя героем фильма Матрица и иже с ними? Нет ничего проще! Правда во времена создания BP никто о существовании Матрицы не подозревал, но такую возможность предоставили. Итак возьмём простенькую програмульку из прошлого выпуска: begin asm jmp @next @msg: db 'Hello world!$' @next: push ds push cs pop ds mov ah, 9 mov dx, offset @msg int 21h pop ds end end. сохраняем, делаем ехе файл и выходим из Паскаля. теперь запускаем bp\bin\td.exe - это Turbo Debugger. Отладчик фирмы Borland. Выбираете File->Change Dir В появившемся окне вводите путь к программе (без имени!). Помните, что путь должен быть короче 8 символов (имеется ввиду длинна имени каждой папки). Если сразу затрудняетесь, то введите только имя диска, на котором находится ехе файл. Дальше File->Open и выбираем нужный нам ехе файл (видите именно ехе, а не pas). На все появшиеся сообщения (если такие будут) ответьте Ок. Окно у вас должно представлять собой нечто такое: +-[_]-CPU 80486-------------------------------------------------------1----[]-+ | cs:0000>9A0000925D call 5D92:0000 ax 0000 |c=0| | cs:0005 55 push bp | bx 0000 |z=0| | cs:0006 89E5 mov bp,sp | cx 0000 |s=0| | cs:0008 31C0 xor ax,ax | dx 0000 |o=0| | cs:000A 9ACD02925D call 5D92:02CD | si 0000 |p=0| | cs:000F EB0D jmp 001E | di 0000 |a=0| | cs:0011 48 dec ax | bp 0000 |i=1| | cs:0012 656C insb gs: | sp 4000 |d=0| | cs:0014 6C insb | ds 5D7E | | | cs:0015 6F outsw | es 5D7E | | | cs:0016 20776F and [bx+6F],dh | ss 5E16 | | | cs:0019 726C jb 0087 | cs 5D8E | | | cs:001B 642124 and fs:[si],sp | ip 0000 | | | cs:001E 1E push ds | | | | cs:001F 0E push cs | | |___________________________________________________________+----------------| | ds:0000 CD 20 00 A0 00 9A F0 FE = а ЪЁ_ | ss:4002 0000 | | ds:0008 1D F0 E4 01 56 24 AE 01 ЁфV$о | ss:4000>0000 | | ds:0010 56 24 80 02 B1 1E 1F 10 V$А_ | ss:3FFE 0000 | | ds:0018 01 01 01 00 02 FF FF FF ___ | ss:3FFC 0000 | | ds:0020 FF FF FF FF FF FF FF FF ________ | ss:3FFA 0000 | +------------------------------------------------------------------------------+ Нечто похожее показывают во всех фильмах о компьютерах. Итак что мы имеем. Сейчас я вам проведу обзорную экскурсию. Большую часть окна (вверху слева) занимает наша программа. Как, а где begin, где end? Я этого не писал ! - такие мысли наверняка уже посетили вас. Скажу сразу. Это машинный код нашей программы переделанный в "понятные" нам команды ассемблера. Вот в это и преобразовывает наши прекрасные программы компилятор. Чуть правее вы видите уже знакомые нам имена регистров. Ну и в самую правую часть окна выведен регист флажков по битам (выведены сответственно имена битов и их состояние). Под кодом программы распологается дамп сегмента данных. Процитирую в очередной раз Serrgio "Картинка", которую вы увидели, называется "дамп памяти" (что в переводе с английского означает "свалка") и она насыщена не только важной информацией, но и специальной низкоуровневой энергетикой. Да чего уж там греха таить - каждый ассемблерщик знает, что рассматривание дампа памяти поднимает настроение, жизненный тонус и другие, не менее важные вещи Так что поднимайте ваш жизненный тонус и отправимся изучать подробнее этот дамп. Итак слева как можно догадаться адресс (вернее только смещение, т.к. сегмент задаётся DS). В центре идут hex числа, справа их расшифровка в коды символов. Так например первый байт по адресу DS:0000 имеет значение5 CDh, а символ с номером CD это = (равно). Следующий за ним байт со значением 20h - это пробел и т.д. Последнюю часть окна мы пока трогать не будем. Теперь давайте посмотрим как вставился наш ассемблерный код ... ищем в коде программы jmp @next.... как вы не нашли ? Правильно. Ведь при компиляции имена меток заменяются адресами. Так что мы пойдём другим путём. Итак давим до боли знакомую клавишу F8 (нам надо именно Step Over, так что именно F8 иначе можете оказаться где-то в месте далёком от нашего кода Обратите внимание: зажглись регистры - только, те которые измениись. Это запустилась наша программа. Итак мы дошли до места cs:001E 1E push ds похоже на то, что мы писали ? но ведь текст начинался с jmp @next, где же он? Давайте рассмотрим текущую строчку: вначале идёт смещение текущей команды относительно сегмента кода (посмотрите кстати на регистр ip). Дальше идёт цифра означающая код команды в hex виде. Т.е. для команды push ds это 1E. Ну и дальше идёт соответственно команда ассемблера. Итак нам надо искать jmp, который ссылается на строчку с адресом 001E - ведь именно её мы пометили меткой @next. Такой jmp находится по адресу cs:000F - jmp 001E - это и есть наш jmp @next Теперь смотрите на строку mov dx, offset @msg- эта строка скомпилировалась в mov dx, 0011 - 'эта адрес строки в сегменте кода, на который у нас ссылается ds. Выберете View -> Dump и посмотрите на дамп - там и правда будет строка Hello World! Теперь обратим взгляд на код нашей програмки именно с адреса 0011 - там находятся команды, которые мы перепрыгнули - а ведь, это то, во что скомпилировался наш Hello World!. А именно в это: dec ax insb gs: insb outsw and [bx+6F],dh jb 0087 and fs:[si],sp
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:44 | Сообщение # 32
|
Администраторы
Сообщений: 301
| Мы уже не однократно использовали в своих программах модуль CRT и пару раз модуль Graph. Наверняка вы догадываетесь, что можно самим написать модуль. Разработка модулей существенно повысит вашу производительность как программиста. Например написав однажды кучу функций можно собрать их в один модуль и не копировать из файла в файл. Хочу оговориться, что модули это исключительна фишка Турбо Паскаля. В стандартном Паскале такого нет. Модули так же полезны, если код не влезает в 64 Кб (это актуально только для ДОС программ), т.к. компилятор Турбо Паскаля загоняет текст модуля в отдельный сегмент. По своей сути модуль представляет из себя ту же программу с несколько иным началом. Структура модуля такова: UNIT имя; INTERFACE интерфейсная часть IMPLEMENTATION исполняемая часть BEGIN часть инициализации END. Начнём по порядку. ИМЯ - задаёт имя модуля (это то, которое мы потом будем объявлять в USES). Для того что бы избежать лишних проблем следует называть модуль и файл с его кодом одним именем. Т.е. если мы хотим назвать модуль так: unit myunit; то текст его должен распологаться в файле с именем myunit.pas. Интерфейсная часть несёт в себе описание типов, переменных, констант, подпрограмм, которые можно будет использовать (вызывать) из головной программы. При описании подпрограмм нужно написать просто их заголовок. Например начнём писать простенький модуль для работы с комплексными числами. (для тех кто в танке - комплексные числа - это числа вида a + b*i, где i число, такое что i2 = -1, a и b - вещественные числа... вообщем литературу на эту тему найти легко, было бы желание). Для этого опишем запись с полями re и im (это a и b) и заголовок процедуры для сложения 2-х комплексных чисел. unit Cmplx; interface type complex = record re, im : real; end; procedure AddComplex (x, y : complex; var z : complex); Итак теперь в основной программе, подключив наш модуль, мы можем объявить переменные типа complex и вызывать процедуру AddComplex. Следует так же отметить (ведь наши программы пока для ДОСа), что все переменные объявленные в интерфейсной части модуля и все переменные основной программы помещаются в единный сегмент данных и следовательно не могут превышать 64 Кб. Переходим к исполняемой части. В ней должно содержаться описание подпрограмм, объявленных в интерфейсной части. Так же можно помещать локальные для модуля объекты (т.е. к ним нельзя обратиться из головной программы) - типы, константы, подпрограммы, переменные. Продолжим наш модуль: unit Cmplx; interface type complex = record re, im : real; end; procedure AddComplex (x, y : complex; var z : complex); implementation procedure AddComplex; begin z.re := x.re + y.re; z.im := x.im + y.im end; Обратите внимание на объявление процедуры AddComplex в исполняемой части модуля. Очередная халява - можно опускать список параметров (и тип результата для функции), так как они уже описаны в интерфейсной части. При этом халява не обязательная - вы вполне можете написать полный заголовок. Переходим к последней части - части инициализации. Она может быть, а может и не быть. Если она есть - тогда обрамляется begin-end. Если её нет, тогда просто пишется end. (с точкой!!). В части инициализации помещается код, который выполняется до передачи управления главной программы. Например здесь программист может присвоить переменным модуля какие-то стартовые значения. В нашем модуле нет никаких тайных махинации и поэтому мы можем опустить инициализацию и вот он - наш первый законченный модуль: unit Cmplx; interface type complex = record re, im : real; end; procedure AddComplex (x, y : complex; var z : complex); implementation procedure AddComplex; begin z.re := x.re + y.re; z.im := x.im + y.im end; end. Ещё один важный момент - если нечего инициализировать, то тогда лучше вообще не писать часть инициализации (т.е. слово begin) т.к. получится так называемый пустой оператор. Что может в некоторых случаях привести к проблемам. Давайте напишем модуль с инициализацией и скрытой переменной: unit some; interface procedure iplus; implementation uses CRT; var i : integer; procedure iplus; begin i := i + 1; writeLn ('I = ', i) end; begin randomize; i := random (256); writeLn ('Start I = ', i) end. i - это переменная, доступная только внутри модуля! Тепрь напишем головную программу, использующую этот самый модуль: program test; uses some; begin writeLn ('Yo-ho-ho!'); iplus; iplus; iplus end. Запускаем и что мы видим? Start I = 16 Yo-ho-ho! I = 17 I = 18 I = 19 О чём нам говорит эта картинка? Правильно: о том, что часть инициализации модуля выполнится раньше чем головная программа. Вот вообщем то и всё. Программа В начале я хотел "отписаться" стандартным в этом случае модулем для работы с комплексными числами. Но, подумал я, неужели мы зря учили ассемблер, нет. Давайте лучше напишем модуль для работы с мышой. Думаю у вас уже неоднократно возникало желание подключить грызуна к своим программам. Сегодня мы это и сделаем. Работа с ушастым реализуется через 33h прерывание. Программа осуществляет это прерывание, передавая в регистрах необходимые параметры, и в них же получает возвращаемые значения от драйвера мыши. Наш модуль не будет поддерживать все возможности мыши, а только самые основные. Итак текст модуля: unit mouse; interface const MB_LEFT = 1; MB_RIGHT = 2; MB_MIDDLE = 4; type tPoint = record x, y : integer end; tMouseState = record loc:tPoint; but:integer end; function ResetMouse:integer; procedure ShowMouse; procedure HideMouse; procedure GetMouseState (var s:tMouseState); procedure MoveMouseCursor (p :tPoint); procedure SetMouseVert (max, min : integer); procedure SetMouseHorz (max, min : integer); implementation function ResetMouse:integer;assembler; asm xor ax, ax int 33h end; procedure ShowMouse;assembler; asm mov ax, 1 int 33h end; procedure HideMouse;assembler; asm mov ax, 2 int 33h end; procedure GetMouseState;assembler; asm mov ax, 3 int 33h push es push di les di, dword ptr s mov es:[di], cx mov es:[di+2], dx mov es:[di+4], bx pop di pop es end; procedure MoveMouseCursor;assembler; asm mov ax, 4 mov cx, p.x mov dx, p.y int 33h end; procedure SetMouseVert;assembler; asm mov ax, 8 mov cx, min mov dx, max int 33h end; procedure SetMouseHorz; assembler; asm mov ax, 7 mov cx, min mov dx, max int 33h end; end. Итак наш модуль поддерживает всего 7 из 31 возможных функций мыши. Если хотите ознакомится, то в новостях сайта дана ссылка на соответствующий документ. Я не буду рассматривать, как написаны эти процедуры и функция, т.к. это простые вызовы прерываний. Если вы немнго разбираетесь в ассемблере, то вам всё понятно. Если есть вопросы - тогда пишите. Я же рассмотрю этот модуль со стороны языка Паскаль. Мы имеем общедоступными 3 константы - они определяют кнопки мыши. 2 типа: * tPoint - описывает точку на экране * tMouseState - описывает состояние мыши. loc - координаты, but - состояние кнопок (имено те 3 константы). И наконец 6 подпрограмм: * function ResetMouse:integer; Инициализирует мышь, если таковой не имеется вернётся нулевое значение. * procedure ShowMouse; Показывает курсор мыши на экране. При этом курсор следует за перемещениями мыши. * procedure HideMouse; Прячет курсор. Полезно когда надо что-то нарисовать под курсором, последовательность действий при этом: спрятали курсор, нарисовали, показали курсор. * procedure GetMouseState (var s:tMouseState); Читает и возвращает состояние мыши в виде записи tMouseState * procedure MoveMouseCursor (p :tPoint); Передвигает курсор мыши в точку p * procedure SetMouseVert (max, min : integer); Устанавливает минимальную/максимальную вертикальную границу передвижения мыши * procedure SetMouseHorz (max, min : integer); Устанавливает минимальную/максимальную горизонтальную границу передвижения мыши Никаких скрытых подпрограмм или переменных у нас нет. Секция инициализации так же опущена, за ненадобностью. Теперь напишем головную программу для работы с мышью. Я написал такой примерчик: program mousetest; uses graph, mouse, crt; var Gd, Gm: Integer; flag : boolean; ms : tMouseState; begin Gd := Detect; flag := false; InitGraph(Gd, Gm, ''); if GraphResult <> grOk then Halt(1); Randomize; ShowMouse; while not flag do begin GetMouseState (ms); if ms.but = MB_RIGHT then flag := true else if ms.but = MB_LEFT then PutPixel (ms.loc.x - 1, ms.loc.y - 1, Random (WHITE)) end; CloseGraph end. Итак мы в цикле опрашиваем мышь, если нажата правая кнопка (ms.but = MB_RIGHT), тогда выходим из программы. Если нажата левая кнопка, тогда ставим точку случайного цвета на экране. Вот и всё!.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:44 | Сообщение # 33
|
Администраторы
Сообщений: 301
| В своё время, расказывая о типах я совсем упустил из виду множества. Теперь настало время наверстать упущенное. Множества - это наборы логически связанных объектов одного типа. Логическая связь между элементами контролируется только программистом. Количество элементов множества может меняться от 0 до 256. Множество, не содержащее элементов называется пустым. Вообщем эта терминология пришла из математики. Но на всякий случай я продолжу: Два множества считаются эквивалентными ттогда и только тогда, когда все их элементы равны, при этом порядок их следования не имеет значения. Если все элементы одного множества входят в другой, то говорят, что первое множество включается во второе. (соответственно пустое множество включается в любое). Описание множества имеет вид: set of БАЗОВЫЙ_ТИП Например: Days : set of 1..31; Digit : set of '0'..'9'; Leters : set of Char; Для задания множества надо использовать специальный конструктор: значения множества перечисляются через запятую (или диапозон через две точки), в квадратных скобках. Например: Days := [8, 9, 13, 20 .. 25]; Digit := ['1', '3', '2', '9', '0']; Leters := [ ]; - пустое множество Рассмотрим ещё один пример, что бы понять что такое эквивалентность и включение: type num = set of 0 ..9; digit = set of '0' .. '9'; var s1, s2, s3 : digit; s4, s5, s6 : num; begin s1 := ['1', '2', '3']; s2 := ['2', '1', '3']; s3 := ['1', '3']; s4 := [0..3]; s5 := [4 .. 6]; s6 := [3, 5 .. 9]; Множества s1 и s2 эквивалентны. Множество s3 включается в s2 и в s1 соответственно. Над множествами определны следующие операции: * пересечение множеств - результат содержит общие для обеих множеств элементы. s4 * s6 cодержит [3], а s4 *s5 пустое множество + объединение множест - результат содержит элементы первого множества, дополненные недостоющими элементами второго множества. s4 + s5 содержит [0,1,2,3,4,5, 6] - разность множеств - результат содержит элементы из первого множества, которые не принадлежат второму. например s6 - s5 содержит [3,7,8,9] = эквивалентность - результат true, если множества эквивалентны и false в другом случае <> не эквивалентность - true если множества не эквивалентны >=, <= * вхождение (проверка на подмножество) <= - true если первое множество включено во второе * >= - true, если второе множество включено в первое in проверка на принадлежность. Синтаксис: ЗНАЧЕНИЕ in МНОЖЕСТВО возвращает true, если ЗНАЧЕНИЕ принадлежит множеству, например 3 in s6 - true; 2*2 in s1 - flase Так же для множеств определены 2 процедуры: * procedure Include(var S: set of T;I:T); Включает новый элемент I в множество S * procedure Exclude(var S: set of T;I:T); Исключает элемент I из множества S Несмотря на возможность использовать для этих операций +/- использование процедур более предпочтительней, т.к. они отличаются более высокой скоростью выполнения. Ну вот и всё, а вы боялись! © дантист, после вырывания зуба Теперь пришло время рассмотреть несколько примерчиков. program test; var S : set of char; c : char; begin s := []; repeat read ©; include (s, c); until c = '.'; for c := '0' to '9' do if c in s then writeLn © end. Итак что же делает эта программа? Для начала пользователь вводит в цикле символы, пока не нажал точку. При этом мы заполняем множество S этими символами. После этого выводим на экран те символы, которые оказались цифрами. Для этого нам надо проверить вхождение цифр в это множество. Что мы благополучно и делаем. Ещё один пример использования множеств - стандартный запрос Y / N - раньше надо было писать 2 условия if (key = 'Y') or (key = 'y') теперь же всё гораздо проще - if key in ['Y', 'y'], так же можно добавть проверку, не нажал ли пользователь букву У вместо Y key in ['Y', 'y', 'У', 'у'] или же добавить ещё проверку на букву Д .... Программа Сегодня мы напишем програмку, позволяющую нам находить простые числа среди натуральных. В основу её будет заложен метод, названный решето Эратосфена. Суть этого метода очень проста - из множества чисел ( в начале оно является множеством [2 .. N]) берётся первое число (назовём его next) - оно является простым, потом из множества чисел удаляются само next и все кратные ему числа, т.е. next*2, next*3 и т.д. И так повторяем до тех пор, пока не исключим из исходного множества все элементы. Сам текст программы приведён ниже: program pnum; const N = 255; type SetOfNumber = set of 1 .. N; var n1, next, i : word; BeginSet, PrimerSet : SetOfNumber; begin BeginSet := [2 .. N]; { начальное множество } PrimerSet := [1]; { это множество простых чисел, в нём первое простое число 1} next := 2; {следующее простое } while BeginSet <> [] do begin n1 := next; { исключаем все числа кратные next } while n1 <= N do begin Exclude (BeginSet, n1); n1 := n1 + next end; { включаем next в множество } Include (PrimerSet, next); {получаем в next следующее простое, которое есть первое в BeginSet} repeat next := next + 1 until (next in BeginSet) or (next > N) end; for i := 1 to N do if i in PrimerSet then write (i:8) end. Эту программу нельзя использовать для произвольного набора чисел, т.к. в множестве не может быть больше 256 элементов. Кстати в нашем множестве SetOfNumber 255 элементов, двайте ка добавим ещё один SetOfNumber = set of 1 .. 256 - компилируем и получаем жизнерадостное сообщение об ошибке, под номером 23 - Set base type out of range - тип множества выходит за границы. Эта ошибка происходит из-за того. что можно использовать целочисленный тип с минимальной границей 0 и максимальной 255 или любой перечисляемый тип, но не более чем с 256 элементами.
|
| |
|
|
|
| semen
|
Дата: Суббота, 30.10.2010, 17:45 | Сообщение # 34
|
Администраторы
Сообщений: 301
| Итак сегодня мы продолжим разговор о константах. Первый раз мы с ними столкнулись в теперь уже далёком выпуске #08 - давненько это было. Однако нам необходимо вернуться к теме констант. Там ведь не всё так просто, как могло показаться с первого раза... Оказывается (вот так сюрприз :))) константа может иметь тип (такого подвоха вы не ожидали?)... такие константы называются типизированными. Полное объявление констант выглядит так: имя : тип = значение Например, name : string = 'Вася Пупкин'; x : real = 0.123; days : 1..31 = 1; Ну это было просто. Гораздо интереснее использовать чего-нить более сложное. Например константы-массивы. Для задания значения необходимо написать их в круглых скобках () через запятую: someconst : array [1..5] of byte = (0, 1, 2, 3, 4); Для масивов типа char есть очередная халява. Два следующих объявления идеентичны: a1 : array [0..9] of char = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); a2 : array [0..9] of char = '0123456789'; Но иногда может понадобится создать константу -запись. Что ж не проблемма. Для значений полей используется список из последовательности вида имя поля, двоеточие и константа. Например: point = record x, y : real; end; const p1 : point = (x : 0; y : 12); Поля должны указываться в той последовательности, в какой они перечислены в определении типа. Если в записи есть поле файлового типа, то такая запись не может быть константой. Константы множества задаются с помощью конструктора множества: type day = set of 1 .. 31; const someday : day = [1..5, 8, 10]; Вот вообщем и всё про константы. Так же сегодня по просьбе одного из чиателей я раскажу об запуске внешних программ. Иногда просто необходимо перед (или даже во время) исполнения вашей запустить другую. Например ну лениво вам писать текстовый редактор, а надо что б в программе можно было отредактировать файл - пожалуйста запускайте любой внешний! Для этого надо использовать процедуру exec из модуля Dos. procedure Exec(Path, Cmdline: string); Процедура получает две строки: Path - путь и имя файла с программой и Cmdline - командная строка. Однако не всё так просто, как хотелось бы. Использование этй процедуры имеет некоторые особенности. Во первых вызывающая программа остаётся в памяти, поэтому она не должна съесть всю доступную память. Объём памяти для программы можно установить здеся Options -> Memory Sizes. По умолчанию вся доступная память отводится вашей программе, поэтому внешней программе будет некуда грузиться Так же размеры памяти можно установить с помощью директивы $M, которая имеет следующий формат: {$M stack, low, hight} где stack - размер стека, low - нижняя граница динамической памяти, hight - верхняя граница. Все размеры указываются в байтах. Особенности исполнения программ на Паскале так же требует изменения некоторых стандартных векторов прерываний. К ним относятся: 00h 02h 18h 23h 24h 34h 35h 36h 37h 38h 39h 3Ah 3Bh 3Ch 3Dh 3Eh 3Fh 75h Начальные значения хранятся в переменных SaveIntXX (где XX - номер прерывания в Hex виде). Поэтому перед вызовом внешней программы и после него рекомендуется вызывать процедуру SwapVectors, которая обменивает содержимое векторов прерываний и перечисленных перменных. Вот и все. Теперь рассмотрим пример {$M 1024, 0, 0} uses Dos; begin writeLn ('переходим в режим командной строки'); writeLn (' для выхода в программу введите exit'); SwapVectors; Exec (GetEnv ('COMSPEC'), ''); SwapVectors; writeLn ('наша программа завершена') end. Я не стал придумывать ничего свехъестественного и запустил командный процессор command.com. Для получения имени файла я использовал функцию GetEnv function GetEnv(EnvVar: string): string; которая возвращает значение переменной среды. Для того что бы увидеть все возможные переменные напишем такую программу: uses Dos; var I: Integer; begin for I := 1 to EnvCount do Writeln(EnvStr(I)); end. Посмотрите что выведет на экран эта программа. И увидите, что у значение параметра COMSPEC - это путь к файлу command.com. В этой програмке я использовал соответственно функции function EnvCount: Integer; возвращает число переменных ДОС. И функцией function EnvStr(Index: Integer): string; Которая возвращает строку типа Name = Value, где Name - имя переменной, Value - значение. Так вот вызов Exec (GetEnv ('COMSPEC'), '') - означает, что мы вызвали command.com без параметров, а значит просто появится командная строка с которой можно будет работать. Например так: переходим в режим командной строки для выхода в программу введите exit Microsoft® Windows 98 ©Copyright Microsoft Corp 1981-1999. D:\>E: E:\>cd bp E:\BP>bla-bla Имя команды или файла указано неправильно E:\BP>exit наша программа завершена Вот в очередной раз и всё.
|
| |
|
|
|