Турбо Паскаль, безусловно, крайне удобная система (в том числе и для обучения программированию), но некоторые его особенности не позволяют наглядно продемонстрировать суть тех или иных вопросов. В частности, ребята, привыкшие к тому, что в Турбо Паскале под термином "компиляция" понимается получение исполняемого кода (т.е. при компиляции "на диск" сразу получается ЕХЕ-файл), не сразу понимают, почему в Турбо Си понятия компиляции и компоновки (линковки) разделены. Конечно, мы не посвящаем школьников во все тонкости (и уж тем более невозможно сделать это в рамках данного краткого курса). Как и во всех других случаях, наша цель — продемонстрировать суть.
Начинаем мы с того, что предлагаем ребятам следующую программу:
#include <stdio.h> int f(int) ;
void main(void)
{ int x;
scant("%d",&x);
printf("f(%d)=%d",x, f (x) ) ;
}
— и задаем вопрос: пройдет ли данная программа компиляцию? Наш опыт говорит о том, что подавляющее большинство детей, изучающих Си после Паскаля, ответят, конечно, нет, ведь функция f не описана! После этого можно предложить им убедиться, что они ошибаются, — компиляцию эта программа проходит, А вот линковку — нет. После этого можно считать, что почва для объяснения того, что происходит при компиляции и линковке, подготовлена. Так. а что же происходит?
Ну, во-первых, в результате компиляции в качестве вещественного доказательства создается OBJ-файл (обычно этот файл называют "объектным", хотя это просто буквальный и неинформативный перевод соответствующего английского слова на русский язык). А что у него [OBJ-файла] внутри? Внутри находится "почти" готовая программа, только вместо конкретных адресов (в частности, вместо адреса функции f) в OBJ-файле находятся символические имена объектов.
Замечание. Адреса, разумеется, отсчитываются внутри исполняемого кода и окончательно формируются при загрузке программы в память для запуска на исполнение, но нас это сейчас не интересует.
Ведь что происходит при вызове функции? В стек (можно сказать проще — в некоторую область памяти) помещаются ее параметры, если они есть), далее управление передается функции, которая эти параметры со стека снимает, выполняется и, возможно, каким-то образом возвращает значение. Для того чтобы описать этот процесс в момент компиляции не надо знать точного адреса функции, достаточно в точке вызова и в точке начала функции поставить одну и ту же метку, которую можно потом заменить конкретным адресом. (Зато важно знать вид — прототип — функции, ведь нужно точно знать, какие параметры она передает и какое значение возвращает.) Замена таких меток конкретными адресами и составляет суть линковки. Разумеется, кроме замены меток, при линковке к исполняемому коду добавляются и недостающие части. Именно поэтому наша программа линковку и не проходит — функции f ведь действительно нет. Но ведь и printf нет, скажете вы. Так это в нашей программе нет. Дело в том, что Турбо Си тоже кое-что "припрятывает", и не все механизмы его работы лежат на поверхности. В частности, он лишь быстрым мельканием названий библиотек при линковке (их конкретные названия и состав зависят от используемой модели памяти и некоторых настроек) информирует нас о том, что при построении исполняемого файла используется не только наша программа, но и ряд стандартных библиотек. В одной из них и содержится функция printf (а прототип ее, как мы уже обсуждали, находится в файле stdio.h). Теперь мы подходим к самому главному: имеется возможность "собирать" (компоновать) программу из нескольких файлов практически так же, как это делает Турбо Си с нашей программой и своими стандартными библиотеками. Для этого требуется создать файл, который называется файлом проекта (и обычно имеет расширение PRJ), и поместить в него список файлов, которые надо использовать при компоновке программы. Когда файл проекта создан (и задан в соответствующем пункте меню системы, можно "одним движением" откомпилировать входящие в него файлы и скомпоновать из них исполняемый файл (обычно ЕХЕ-файл, но имеется возможность получать и СОМ-программы), который получит имя файла проекта. Продолжая рассматривать пример, которым мы начали этот раздел, приведем соответствующие файлы:
sdigits.prj
f.c
main.c
f.c
int f(int n)
{ if (!n/10) return n%10;
else return n%10+f (n/10);}
main.c
#include <stdio.h>
int f (int) ;
void main(void)
{ int x;
scanf("%d",&x) ;
printf("f(%d)=%d",x,f(x));
}
Если теперь задать файл sdigits.pij в качестве файла проекта и выполнить команду "Make", получится файл sdigits.exe.
В различных системах программирования на Си (в частности, в различных версиях Турбо Си) работа с файлами проекта реализована по-разному. В частности, в файлах проекта может содержаться некоторая дополнительная информация (например, зависимости файлов), кроме того, мы совсем не обсудили, чем отличаются OBJ-файлы от файлов библиотек (LIB) и как получать вторые из первых. Это уже все "шелуха" (кроме того, соответствующая информация легкодоступна). Мы постарались кратко изложить суть использования файлов проекта.
10.1. Пример использования файлов проекта. Небольшая библиотека функций для работы в режиме VGA 13h (320 x 200 x 256 цветов)
Режим VGA 13h (13h — это номер данного режима, записанный в шестнадцатеричной системе счисления, режимы видеоадаптера обычно записывают именно так) 320 х 200 х 256, вообще говоря, является стандартным для видеоадаптера VGA, но поскольку BGI его не поддерживает, в учебных задачах этот режим используется редко. А жаль, ведь даже при низком разрешении экрана (всего 320 х 200) 256 цветов — это не 16. В режиме VGA 13h можно показывать картинки вполне приличного качества. Мы напишем совсем небольшую "библиотечку", которая будет содержать функции установки данного режима (мы также включим в нее функцию установки текстового режима, чтобы можно было все-таки в него вернуться ), функцию установки палитры (всех 256 цветов сразу) и функцию вывода точки. Прототипы всех функций поместим в файл vga256.h. Вот его содержимое:
typedef struct {unsigned char r,g,b;}
RGB[256j; /* Описание типа "палитра" */
void set256(void); /*
Установка режима 13h */
void setTEXT(void); /*
Установка текстового режима */
void putpixel256(int,int,unsigned char); /* Поставить точку */
void setVgaDAC(RGB); /*
Установить палитру */
Видно, что, помимо прототипов функций, мы включили в заголовочный файл описание типа RGB, который представляет собой массив структур для хранения палитры 256-цветного режима. Скажем несколько слов о том, как в этом режиме устроена палитра. Всего в ней, разумеется, 256 элементов, при этом каждый элемент палитры описывает один цвет (его красную, зеленую и синюю составляющие). В связи с особенностями видеоадаптера в каждой составляющей используются лишь шесть младших битов, т.е. каждая составляющая может принимать значения от 0 до 63.
Таким образом, всего в данном режиме можно получить б43 = 218 цветов (но одновременно можно использовать лишь 256!), Приведем теперь файл vga256.c:
#include <dos.h>
#include "vga256.h"
void set256(void)
{
_AL=Oxl3;
_AH=0;
geninterrupt (0х10);
}
void setTEXT(void)
{
_AL=Ox3;
_AH=0;
geninterrupt(0х10) ;
}
void putpixel256(int x, int y,unsigned char c)
{ unsigned char far *vmeiri= (unsigned char far *) (MK_FP(OxAOOO,0)) ;
vmem[y*320+x]=c;
}
void setVgaDAC(RGB pal)
{
_ES=FP_SEG(pal) ;
_DX=FP_OFF(pal) ;
_AH=OxlO;
_AL=Oxl2;
_BX=0;
_CX=256;
geninterrupt(0х10);
}
Мы написали данный файл таким образом, чтобы он компилировался в самых ранних системах Турбо Си. Если вы используете компиляторы более поздних версий, то запись с использованием asm {...} будет удобнее и короче.
Замечание. Объяснять или не объяснять детям, как устроены функции в файле vga256.c? Мы решаем этот вопрос применительно к каждому конкретному классу. Обычно — объясняем. К сожалению, мы не можем обсуждать "начинку" этих функции в рамках данного краткого курса. Однако и "запасной" вариант — просто выдать детям данную библиотеку и объяснить назначение ее функций — работает. Вы ведь не "лезете внутрь" функций из BGI (да и всех других стандартных), но это не мешает вам их использовать.
Теперь приведем файл demo256.c, в котором демонстрируются возможности нашей библиотеки:
#include "vga256.h"
#include <stdio.h>
#include <dos.h>
void main(void)
{ RGB palette;
int i, x, y;
set256();
palette [0].r=palette[0].g=palette[0].b=0;
/* Тест 1. Серая
градиентная заливка экрана - от черного к
белому */
for (i=1;i<=64;i++) palette[i].r=palette[i].g=palette[i].b=i-1;
setVgaDAC(palette) ;
for (x=0;x<320;x++)
for (y=0;y<200;y++) putpixel256(x,y,x/5+1) ;
getch() ;
/* Тест 2. Красная, зеленая
и синяя градиентные заливки экрана */
for (i=1;i<=64;i++) {palette[i].r=i-1;palette[i].g=palette[i].b=0;}
setVgaDAC(palette);
getch () ;
for (i=l;i<=64;i++) {palette[i].g=i-1;palette[i].r=palette[i],b=0;}
setVgaDAC(palette) ;
getch() ;
for (i=1;i<=64;i++) {palette[i].b=i-l;palette[i].r=palette[i].g=0;}
setVgaDAC(palette) ;
getch ();
/* Тест 3. Градиентная заливка
Красный—Зеленый—Синий—Красный */
for (i=l;i<=64;i++) {palette[i].r=64-i;palette[i].g"i-l;palette[i].b=0;}
for (i=65;i<=128;i++) {palette[i].r=0;palette[i],g=128-i;palette
[i].b=i-65;}
for (i=129;i<=192;i++) {palette[i].r=i-129;palette[i].g=0;palette[i].b-192-i;}
setVgaDAC(palette);
for (x=0;x<320;x++)
for (y=0;y<200;y++) putpixel256(x,y,(x<192)?x+l:0);
getch () ;
/* Тест 4. Переливающаяся
градиентная заливка */
while (IkbhitO)
{ palette[1]=palette[192];
for (i=sl92;i>1;i--) palette[i]=palette[i-1];
setVgaDAC(palette) ;
delay(100) ;
}
getch();
setTEXT();
}
Файл проекта demo256.prj будет содержать всего две строчки (кстати, мы забыли сказать, что порядок строк в файле проекта значения не имеет):
demo256.с
vga256.с
Замечание. При выполнении четвертого теста изображение на экране будет немного подергиваться (для уменьшения этого эффекта можно варьировать значение задержки в функции delay). Это связано с тем, что мы написали простейший и не лучший вариант функции setVgaDAC. Правильный вариант этой функции, позволяющий динамически менять палитру без подергивания экрана, требует использования ассемблерной вставки, Мы привели его в приложении.