Скачиваний:
79
Добавлен:
02.05.2014
Размер:
1.32 Mб
Скачать

71

Министерство общего и профессионального образования РФ

®

Тульский государственный университет

Технологический факультет

Кафедра "Автоматизированные станочные системы"

Троицкий Д.И.

КУРС ЛЕКЦИЙ ДЛЯ СТУДЕНТОВ НАПРАВЛЕНИЯ 522900

ТУЛА 1997-99

УСЛОВНЫЕ ОБОЗНАЧЕНИЯ:

__ - определение понятия;

__ - важная информация, ее надо запомнить;

__ - потенциальный источник ошибок и неприятностей1;

__ - пример.

Автор курса лекций - кандидат технических наук, доцент кафедры "Автоматизированные станочные системы" Тульского государственного университета. E-mail troitsky@uic.tula.ru, WWW www.geocities.com/eureka/3120/koi_vic.html

ЛЕКЦИЯ №1

Классификация языков программирования. Язык Лисп

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

__ Стиль - совокупность правил, лежащих в основе синтаксиса и семантики языка программирования. Различают следующие стили:

  1. неструктурный;

  2. структурный;

  3. логический;

  4. объектно-ориентированный;

  5. функциональный.

Рассмотрим перечисленные стили подробнее.

__ Неструктурное программирование допускает использование в явном виде команды безусловного перехода (в большинстве языков GOTO). Типичные представители неструктурных языков - ранние версии Бейсика и Фортрана. Данный стиль вызван особенностями выполнения машиной программы в кодах и унаследован от программ на языке ассемблера, поскольку там команда перехода является обязательной. Однако в языках высокого уровня наличие команды перехода влечет за собой массу серьезных недостатков: программа превращается в “спагетти” с бесконечными переходами вверх-вниз, ее очень трудно сопровождать и модифицировать. Фактически неструктурный стиль программирования не позволяет разрабатывать большие проекты. Ранее широко практиковавшееся первоначальное обучение программированию на базе неструктурного языка (обычно Бейсика) приводило к огромным трудностям при переходе на более современные стили. Как отмечал известный голландский ученый Э. Дейкстра, “программисты, изначально ориентированные на Бейсик, умственно оболванены без надежды на исцеление”.

__ Структурный стиль был разработан в середине 60х - начале 70х гг. В его основе лежат две идеи:

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

  • как доказал Э. Дейкстра, любой алгоритм можно реализовать, используя лишь три управляющие конструкции: последовательное выполнение, ветвление и цикл (рисунок 1.1; обратите внимание на обозначения в соответствии с действующим стандартом). Данное обстоятельство позволяет при наличии соответствующих операторов исключить из языка команду перехода GOTO.

а) Последовательное б) Ветвление в) Цикл

выполнение

Рисунок 1.1 - Основные управляющие конструкции.

Принципы структурного программирования были реализованы в языке Алгол, но наибольшую популярность завоевал язык Паскаль, созданный в 1970 швейцарским ученым Н. Виртом. Паскаль получил широчайшее распространение и может считаться образцовым языком программирования, наиболее популярным и сейчас (например, в версии Delphi фирмы Imprise).

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

__ БРАТЬЯ ИМЕЮТ ОДНОГО ОТЦА

ДЖОН - ОТЕЦ ДЖЕКА

МАЙК - БРАТ ДЖЕКА

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

ДЖОН - ОТЕЦ МАЙКА.

Хотя работы по логическому программированию ведутся с 50-х гг., в настоящее время данное направление несколько потеряло свою актуальность в связи с отсутствием реальных результатов, поскольку большинство реализованных на принципах логического программирования систем оказались практически непригодными.

__ Объектно-ориентированное (ОО) программирование, разработанное в середине 70х гг. Керниганом и Риччи и реализованное в ОО-версиях языков Си и Паскаль, представляет собой отображение объектов реального мира, их свойств (атрибутов) и связей между ними при помощи специальных структур данных. Структурное программирование подразумевает наличие ряда встроенных структур данных: целых, вещественных и строковых переменных, массивов, записей - при помощи которых и производится отображение свойств объектов реального мира. При объектно-ориентированном подходе для объекта создается своя структура данных (класс), содержащая как свойства объекта (поля), так и процедуры для управления объектом (методы).

__ Например, рассмотрим простейший объект - точку на экране. Она имеет как минимум три поля (координаты и цвет) и методы вроде "ChangeColor" (поменять цвет), "MoveXY" (переместиться в точку х, у) и т.д. Объектно-ориентированный подход является в настоящее время доминирующим и позволяет сократить время разработки и увеличить надежность больших проектов. Однако программы в данном стиле отличаются громоздким синтаксисом; в целом идеология объектно-ориентированного подхода весьма неочевидна часто воспринимается с трудом (особенно это характерно для языка Си, который и в своем первоначальном виде отличается крайне неудобочитаемым синтаксисом) и переход к его использованию труден или невозможен для большого числа программистов.

__ В основе функционального стиля лежит понятие функции как "черного ящика", имеющего вектор параметров (аргументов) на входе и результат r (скаляр) на выходе:

f()=r (1.1)

Допустим случай , т.е. вектор параметров отсутствует. В функциональных языках программирования отсутствуют операторы: все действия, в том числе и управляющие конструкции, выполняются при помощи вызовов функций. Поскольку каждая функция возвращает значение, ее можно подставить в качестве аргумента другой функции, что позволяет записывать сложные выражения в функциональной форме. Одним из первых функциональных языков стал язык Лисп, созданный в конце 50-х гг. как язык искусственного интеллекта.

К языкам искусственного интеллекта (сокращенно обозначается AI - artificial intelligence) относят такие языки, которые способны в зависимости от набора исходных данных модифицировать алгоритм работы, т.е. "на ходу" менять программу (поговорка гласит, что на языках искусственного интеллекта программируют те, кому не хватает интеллекта естественного).

Рисунок 1.1 - Создатель языка Лисп Джон Маккарти.

Рассмотрим язык Лисп, созданный американским ученым Джоном Маккарти (рисунок 1.1) в 1957 в Массачусетском технологическом институте (там он и работает по сей день) и нашедший широкое применение.

Название языка официально означает LISt Processing, а неофициально - Lots of Idiotic Silly Parentheses. Лисп отличается высокой компактностью - ядро интерпретатора занимает 4..10Кб, функциональным стилем программирования и использованием обратной польской нотации.

Основное понятие языка Лисп - список.

__ Список - перечень атомов или списков, отделенных друг от друга пробелами и заключенных в скобки.

__ Как видно из определения, списки могут быть вложенными. А что же такое атом в Лиспе? Это простой (в отличие от списка) тип данных: число, символьная строка, функция.

__ Внимание: в Лиспе нет различия между текстом программы и обрабатываемыми ею данными!

В других языках (например, в Паскале) программа (PROCEDURE, FUNCTION) и данные (VAR, CONST) жестко разделены. В Лиспе же и данные, и текст программы являются списками. Разумеется, список, являющийся программой, включает в себя атомы-функции, которые и вызываются при выполнении программы. То, что с программой можно работать, как с данными, и определяет возможность динамической модификации текста программы, что является свойством языков искусственного интеллекта. Как же отличить список-программу от списка-данных?

__ В Лиспе по умолчанию любой список является программой и интерпретатор будет пытаться ее выполнить. Если список - не программа, а данные, надо явно отключить его интерпретацию.

Из того, что текст программы - тоже список, вытекает необходимость использования специальной системы его записи - обратной польской нотации (названа так по причине изобретения ее польским математиком Яном Лукасевичем). Вызов любой функции в данной нотации записывается как список следующего вида:

__ (1.2)

где - аргументы функции.

__ Например, если функция сложения двух чисел имеет имя "+", то операция 2+3 запишется как ( + 2 3 ). В качестве аргументов могут фигурировать другие функции, что позволяет записывать сколь угодно сложные формулы в обратной польской нотации.

Пусть дана функция . В обратной польской записи она приобретет следующий вид:

( * ( / ( + ( * 2 x ) 3 ) ( - y 1 ) ) ( - y x ) ).

П

римечание: аналогичная нотация используется в программируемых калькуляторах со стеком типа МК52, МК61, TI59 и других. Там для выполнения операции сложения 2+3 нажимаются клавиши .

Теперь рассмотрим конкретную реализацию языка Лисп - встроенный в САПР AutoCAD интерпретатор языка AutoLISP (Автолисп). Выбор столь экзотического языка в качестве встроенного для столь популярной САПР вызван тем, что список - оптимальный способ представления графической информации, а также легкостью реализации и небольшими размерами интерпретатора. Однако следует признать относительно большую трудоемкость разработки программ на Автолиспе. Зато программы на Автолиспе отличаются чрезвычайно высокой надежностью; за многие годы работы ошибок в работе интерпретатора не выявлено.

Начнем с атомов.

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

Каждый атом имеет имя, дающееся по следующим правилам - допускаются английские буквы, цифры, большинство имеющихся на клавиатуре знаков за исключением ";", "(", ")", ".", "," ," ", строчные и заглавные буквы не различаются, первым символом должна быть буква.

__ Буква T зарезервирована и не должна использоваться в качестве имени атома. Слово NIL зарезервировано и не должно использоваться в качестве имени атома. Использование в качестве имени атома имени встроенной функции приведет к потере работоспособности этой функции до перезагрузки AutoCAD'а.

__ Длина имени формально не ограничена, но для экономии памяти настоятельно рекомендуется не превышать длину в шесть знаков.

Итак, программист в тексте программы дает атому имя, а интерпретатор Автолиспа сам динамически выделяет память под содержимое атома и автоматически определяет тип и размер хранящейся в атоме информации (рисунок 1.2)

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

Адрес

Содержимое

Имя

Number

0010:0020

0

Адрес

0010:0020

0010:0021

5

Тип

Целое число

0010:0022

45

Размер

2 байта

0010:0023

18

Рисунок 1.2 - Хранение в памяти атома с именем Number, представляющего собой ссылку на целое число (в данном случае 5).

__ Автолисп поддерживает следующие типы данных:

  • целое число со знаком от -32768 до 32767 или от 0 до 65535 (2 байта) без знака;

  • вещественное число, записываемое через десятичную точку: 10.52 или в экспоненциальном формате: 2.52Е-12;

  • строка символов длиной до 127 знаков, заключенная в двойные кавычки. Символ "\" является служебным и, если он нужен в тексте, должен удваиваться: текст "3\2" запишется как "3\\2". "\" используется для обозначения перевода строки: "\n", возврата каретки: "\r" и табуляции: "\t";

  • логический тип, принимающий два возможных значения: истина (обозначатся Т) или ложь.(обозначается NIL);

  • ссылка на встроенную функцию языка;

  • ссылка на созданный программистом список (программу или данные);

  • ссылка на переменную;

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

ЛЕКЦИЯ №2

Присваивание значений в Автолиспе.

Математические функции. Работа с интерпретатором

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

__ Переменная - указатель на область динамической памяти, имеющий имя.

__ Значение переменной - данные, записанные в динамической памяти начиная с адреса, записанного в переменной.

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

Для присваивания значений переменным в Автолиспе имеются две функции - SET и SETQ.

__ Функция SETQ (set by quote) меняет значение переменной, а не саму переменную. Аргументами функции является перечень пар "переменная" - "значение". Функция возвращает результат последнего присваивания.

Например, запись ( SETQ a 10 ) помещает число 10 в область памяти, на которую указывает переменная а и одновременно задает тип переменной а - целое число. Можно в одной функции присвоить несколько переменных:

__ ( SETQ a 10 b "213" )

Порядок выполнения нескольких присваиваний в функции SETQ определен слева направо. Если мы напишем:

__ ( SETQ a 10 b ( + a 10 ) )

то значением переменной b станет 20.

__ Изменение значения переменной на NIL освобождает занимаемую ее значением область памяти.

( SETQ a NIL ) сохраняет саму переменную а, но теперь она указывает "в никуда" (для ее значения памяти не выделено).

SETQ - наиболее часто используемая функция присваивания. Она аналогична по своему действия оператору присваивания ":=" в Паскале и в принципе позволяет не задумываться об особенностях организации памяти в Автолиспе.

__ Вторая функция присваивания - SET - работает не со значениями, а с самими переменными:

( SET a b ) заставляет переменную а ссылаться на ту же область памяти, что и переменная b.

__ Как записать в значение переменной атом (число, текстовую строку...) понятно. А как быть, если в переменную нужно записать список? Например, имеется список с данными вида: ( 0.25 "АБВ" 46 ). Проблема заключается в том, что, как мы уже знаем, по умолчанию любой список считается программой и первый же элемент списка, стоящий сразу после открывающей скобки, рассматривается как имя функции. Если мы напишем функцию присваивания вида:

( Setq a ( 0.25 "абв" 46 ) )

то интерпретатор попытается вызвать функцию с именем "0.25" и аргументами "АБВ" и "46", что приведет к возникновению ошибки "Неверное имя функции", поскольку функции с именем "0.25" нет и быть не может. Таким образом, необходимо как-то сказать Автолиспу: не пытайся выполнить этот список, это не программа, а данные. Для этого используется функция подавления вычисления списка QUOTE:

__ Функция QUOTE запрещает вычисление списка, являющегося ее аргументом и возвращает сам этот список.

В рассматриваемом примере нужно написать:

__ ( SETQ a ( QUOTE ( 0.25 "АБВ" 46 ) ) )

и в значением переменной а станет список ( 0.25 "АБВ" 46 ). Поскольку данные в программе встречаются весьма часто, введена сокращенная запись функции QUOTE в виде апострофа:

( Setq a '( 0.25 "абв" 46 ) )

__ Применять функцию QUOTE к атому бессмысленно: ( QUOTE 0.25 ) и 0.25 - одно и то же.

А что будет, если применить функцию QUOTE к переменной? Она вернет саму переменную, а не ее значение:

__ ( SETQ a 5 )

( SETQ b a )  значением переменной b будет 5

( SETQ b 'a )  значением переменной b будет переменная а.

Поэтому функцию SET можно использовать и для изменения значения переменной:

__ ( SET 'a 10 ) делает то же, что и ( SETQ a 10 )

поскольку запись 'a означает значение переменной а.

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

Предположим теперь, что мы записали в значение переменной как данные список, который на самом деле является программой:

( SETQ a '( + 5 10 ) )

Как теперь заставить Автолисп вычислить этот список? Для этого есть очень полезная функция EVAL (от evaluation):

__ Функция EVAL вычисляет список, являющийся ее аргументом, и возвращает вычисленное значение.

В нашем случае ( EVAL a ) вернет 15. Таким образом, функция EVAL обратна по отношению к функции QUOTE. Ее использование позволяет "на ходу", в ходе выполнения программы, сформировать список-программу (содержащий вызовы функций) и исполнить его. В этом и заключается динамическая модификация текста программы, необходимая для решения задач искусственного интеллекта.

Теперь рассмотрим математические и логические функции Автолиспа, которые для удобства сведены в таблицу 2.1.

Таблица 2.1 - Основные математические функции Лиспа.

Имя функции

Аргументы

Возвращаемое значение

+

a1 a2 ... an

a1+a2+...+an

-

a1 a2 ... an

a1-a2-...-an

*

a1 a2 ... an

a1a2...an

/

a1 a2 ... an

a1/a2/.../an

1+

a

a+1

1-

a

a-1

ABS

a

SQRT

a

EXP

a

EXPT

a b

GCD

a b

НОД чисел а, b

LOG

a

Ln(a)

MIN

a1 a2 ... an

Минимальное из чисел

a1 a2 ... an

MAX

a1 a2 ... an

Максимальное из чисел

a1 a2 ... an

REM

a b

Остаток от деления a/b

Тригонометрические функции

SIN

a

Sin(a), a - в радианах

COS

a

cos(a), a - в радианах __

ATAN

a

atn(a) в радианах

В языке также предусмотрена встроенная переменная PI, со значением, равным числу .

__ Пример: Вычислить площадь треугольника S со сторонами a, b, c по формуле Герона:

Запишем программу вычислений:

( SETQ p ( / ( + a b c ) 2 ) )

( SETQ s ( SQRT ( * p ( - p a ) ( - p b )

( - p c ) ) ) )

__ Для того, чтобы эту программу выполнить, ее можно либо построчно набирать в ответ на приглашение Автокада "Команда:", либо записать в текстовый файл с расширением *.LSP и загрузить его. Функции будут выполнены в процессе загрузки файла.

__ Внимание! Автокад не загружает автоматически файл с программой после того, как в него внесены изменения. Это надо делать принудительно при помощи меню или функции LOAD. В противном случае можно долго искать ошибку: программа исправлена, а работает, как и прежде.

__ Чтобы просмотреть значение переменной, надо в командной строке набрать восклицательный знак, а за ним - имя переменной:

Команда:

выведет на экран значение переменной а.

При наборе программ на Автолиспе легко ошибиться с парностью скобок, поэтому рекомендуется использовать текстовый редактор, отслеживающий скобки (простейший из них - The Norton Editor).

__ Если неверное по скобкам выражение введено с командной строки Автокада, то приглашение "Команда:" меняется на "1>", где 1 - число незакрытых правых скобок. Следует прервать ввод (нажав + или +) и попробовать еще раз.

ЛЕКЦИЯ №3

Создание собственных функций.

Область видимости переменных.

Организация диалога с пользователем

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

__ Стандартный (но не единственный!) способ создания пользовательской функции заключается в использовании функции DEFUN (DEFine FUNction). В общем виде она записывается следующим образом:

(DEFUN name ( a1 a2 ... an / v1 v2 ... vm )

( выражение1 )

( выражение2 )

....

( выражение N )

)

где name - имя функции;

ai - i-й аргумент функции;

vi - i-я локальная переменная.

__ Функция DEFUN создает в памяти пользовательскую функцию с именем name и списком аргументов a1 a2 ... an. При этом сама функция name еще не выполняется, а только помещается в память - для выполнения она должна быть явно вызвана.

__ Пользовательские функции не могут иметь произвольное число аргументов, как встроенные функции типа "+", "-" и др. Вызов пользовательской функции с неверным числом аргументов приведет к возникновению ошибки "Неверное число аргументов".

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

  1. Найти закрывающую скобку, парную открывающей перед словом DEFUN (т.е. последнюю);

  2. Найти предпоследнюю закрывающую скобку;

  3. Найти парную ей открывающую скобку;

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

Иначе говоря, возвращаемое значение есть результат вычисления последнего списка, находящего внутри DEFUN.

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

__ Пример: В файле написано следующее:

( DEFUN f1 ()

......

( f2 )

)

( UN f2 ()

......

)

__ Это совершенно корректная ситуация, не вызывающая у Автолиспа вопросов. Паскаль бы в такой ситуации стал сильно ругаться.

А теперь попробуем наконец-то создать собственную полезную функцию.

__ Пример: в Автолиспе нет функции, вычисляющей тангенс угла. Введем функцию с именем tan следующего вида:

(DEFUN tan ( a )

( / ( SIN a ) ( COS a ) )

)

Выполнение данного списка приведет к появлению в памяти переменной tan типа "определенная пользователем функция", значением которой является список-программа вычисления тангенса.

Текст пользовательской функции можно вывести на экран при помощи "!", поскольку он также является значением переменной.

Закономерный вопрос: а куда делись косая черта и загадочные "локальные переменные" v1, v2..vm ?

__ Все переменные в Автолиспе делятся на два вида: глобальные и локальные.

__ Глобальные переменные постоянно находятся в оперативной памяти и, следовательно, доступны из любой функции. Глобальные переменные создаются автоматически при присваивании им значения.

__ Например, функция ( SETQ a 5 ) создает глобальную переменную a.

__ Локальные переменные явно описываются в заголовке функции DEFUN и видны только внутри соответствующей пользовательской функции. В начале работы пользовательской функции локальные переменные имеют значение NIL. По окончании работы этой функции они автоматически удаляются из памяти.

__ Например, опишем функцию с именем s следующего вида:

(DEFUN s ( a / p )

( SETQ p 5 )

)

( SETQ p "ABC" )

__ Здесь p, которой присваивается значение 5 - локальная переменная. Она не будет доступна из других функций. Поэтому функция ( SETQ p "ABC" ) создает глобальную переменную с именем p, никаким образом не связанную с локальной переменной р.

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

__ Использование глобальных переменных крайне нежелательно. Они занимают память; вызывают необходимость отслеживания имен переменных (а локальных переменных с одинаковыми именами в разных пользовательских функциях может быть сколько угодно). В любом языке программирования использование глобальных переменных - признак низкой квалификации программиста. Данные следует хранить в локальных переменных и передавать через параметры вызываемых функций.

__ Если глобальные переменные все же используются, как только необходимость в них отпадает, следует освобождать занимаемую ими память, присваивая им значение NIL (вопрос нехватки памяти в Автолиспе в его реализации для Автокада 10 стоит очень остро).

__ Итак, пользовательская функция может не иметь как локальных переменных, так и аргументов. Поэтому возможны записи:

(DEFUN s

( a b / cd )

(DEFUN s

( ab )

(DEFUN s

( / c d )

(DEFUN s ()

__ В любом случае пара скобок после имени функции сохраняется.

Попробуем теперь написать более сложную функцию.

__ Пример: написать функцию вычисления площади треугольника со сторонами a, b, c.

Очевидно, снова нужно воспользоваться формулой Герона. Назовем нашу функцию trisqu (triangle square), помня о правиле шести символов. Ее аргументами, очевидно, будут стороны треугольника. Введем также локальную переменную p для записи полупериметра:

( DEFUN trisqu ( a b c / p )

( SETQ p ( / ( + a b c ) 2 )

( SQRT ( * p ( - p a ) ( - p b ) ( - p c ) ) )

)

__ Обратите внимание, что нет необходимости записывать площадь в переменную - мы сразу сделали ее возвращаемым значением.

При вызове функции trisqu ей обязательно должны быть переданы три аргумента, значения которых запишутся в локальные переменные a, b, c, т.е. мы должны написать:

( trisque 3 4 5 ) или ( trisque a 4.5 ( / 2 b ) ) или...

__ Рассмотрим теперь способы занесения пользовательских функций в память и их вызова. Как мы уже знаем, наиболее удобный путь - записать пользовательские функции в текстовый файл с расширением .LSP и загрузить его функцией LOAD, которую можно набрать в командной строке Автокада:

Тут вы можете оставить комментарий к выбранному абзацу или сообщить об ошибке.

Оставленные комментарии видны всем.