Беря на себя взаимодействие с внешними устройствами Windows позволяет создавать более надежное и совместимое программное обеспечение.
Вторым преимуществом операционной системы Windows является ее многозадачность. Все задачи, запускаемые в ОС оказываются совершенно равноправными по отношению к рессурсам микропроцессора. Замечательно и то, что многозадачность возможна и в рамках одной задачи, когда две функции могут выполняться параллельно и независимо друг от друга.
Еще одной особенностью програмирования в среде Windows является присутствие только одной модели памяти. в Windows используется так называемая линейная или плоская модели памяти. Суть этой модели заключается в том, что содержание всех сегментных регистров фиксируется, а адресация осуществляется с помощью 32-битных регистров. Такая модель основывается на так называемой страничной адресации в защищенном режиме. Для программирования это дает значительные преимущества, заключающиеся в том, что поскольку сегментом теперь является вся память, то снимаются все ограничения на размер кода, данных, стека и объема отводимого под локальные переменные.
К особенностям программирования в Windows мы еще вернемся в Главе 4, а пока познакомимся с тем, как можно непосредственно использовать системные вызовы в программах для операционной системы MSDOS.
Ниже приведена программа, которая для печати строки использует системный вызов (так называемое 21-е прерывание).
#include <stdio.h>
#include <dos.h>
voidmain()
{
char *s="Печать с помощью системного вызова ";
structREGPACKr;
r.r_ax=0x0200; /*функция 2, 21-его прерывания*/
r.r_ds=FP_SEG(s);
r.r_bx=FP_OFF(s);
while (*(char *)MK_FP(r.r_ds,r.r_bx)!=0)
{
r.r_dx=*(char *)MK_FP(r.r_ds,r.r_bx);
intr(0x21,&r);
r.r_bx++;
}
}
Прокоментируем программу. В программе используется функция INTR, позволяющая осуществлять системные вызовы операционной системы MSDOS. Эти системные вызовы называются прерываниями и имеют свой номер. Наиболее часто используемое прерывание имеет номер 21 (шестнадцатиричный). В нашей программе мы используем функцию 2 этого прерывания, которая позволяет печатать один символ, код которого помещен в регистр DX. Для работы с регистрами используется предопределенная структура REGPACK. Особо обращаем внимание на функции FP_SEG, FP_OFF, MK_FP. FP_SEG и FP_OFF позволяют получать по указателю сегментный адрес и смещение. Функция MK_FP наоборот формирует указатель исходя из сегментного адреса и смещения. Напоминаем, что на конце строки стоит символ с кодом 0, на чем и основывается выход из цикла.
1.12.4 Использование языка ассемблера в программах на Си
Для оптимизации программ часто используют язык ассемблера (далее просто ассемблер). Поскольку этот язык практически в чистом являет собой язык микропроцессора, то получаемый с помощью него код весьма компактен и выполняется гораздо быстрее кода, получаемого из фрагмента на языке высокого уровня.
При работе с языком Си можно использовать как встроенный ассемблер, так и язык ассемблера во внешних модулях.
Рассмотрим в начале встроенный ассемблер. Достоинством его является возможность писать ассемблерные фрагменты прямо среди фрагментов на языке Си. Основным недостатком является то, что часто встроенный ассемблер обладает меньшими возможностями реального ассемблера (отсутсвие некоторых команд, директив).
Основой встроенного ассемблера является ключевое слово asm, после которого может идти или команда на языке ассемблера или блок команд, заключенных в фигурные скобки. Ниже приводится простой пример.
#include <stdio.h>
void main()
{
char * s="Печать из ассемблерного блока";
/*далее идут команды на языке ассемблера*/
asm lds bx,s
asm mov ah,2
l1:
asm mov dl,[bx]
asm cmp dl,0
asm jz l2
asm int 21h
asm inc bx
asm jmp short l1
l2:
}
Мы намеренно взяли программу из предыдущего параграфа и переписали ее на языке ассемблера. Прокоментируем ее не вдаваясь в особенности выполнения ассемблерных команд. Для вывода символа на экран его помещают в регистр dl и вызывается функция 2 21-его прерывания. На очередной символ строки указывает регистр bx. Процесс вывода символов заканчивается когда в регистр dl попадает код 0 (конец строки).
Перейдем теперь к случаю, когда к программе на языке Си подключается модуль, написанный на языке ассемблера. Подключение осуществляется на втором этапе трансляции (см. параграф 1.12.1). Ниже приведены модуль на языке ассемблера и модуль на языке Си. Причем первый содержит процедуру, вызываемую из второго.
;модуль на языке ассемблера
CODE SEGMENT
ASSUME CS:CODE
PUBLIC _PRI_STR ;процедура будет вызываться из другого модуля
_PRI_STR PROC FAR
PUSH BP
MOV BP,SP
;получаем адрес начала строки
LDS BX,[BP+6]
;номер вызываемой функции
MOV AH,2
CONT:
;очередной символ поместить в регистр dl
MOV DL,DS:[BX]
;проверяем - не конец ли строки
CMP DL,0
JZ _en
;вызов 21-его прерывания
INT 21H
;переходим к следующему символу
inc bx
;на начало цикла
JMP SHORT CONT
_en:
POP BP
;возвращаемся в вызывающий модуль
RET
_PRI_STR ENDP
CODE ENDS
END
/*Модуль на языке Си*/
#include <stdio.h>
extern void far PRI_STR(char *);
void main()
{
char * st="Печать из ассемблерного модуля.";
PRI_STR(st);
}
Коментарий.
Прежде всего, отметим, что модули должны быть согласованы по модели памяти (см. 1.12.2). Мы предполагаем, что модуль на языке Си компилируется в модели Large. В модуле на языке ассемблера согласование по модели заключается в том, что вызываемая из другого модуля процедура имеет тип Far. Оба модуля можно просто включить в проект (модуль на языке Си должен быть первым, а модуль на языке ассемблера должен иметь расширение asm) при этом для ассемблерного модуля при трансляции автоматически будет вызываться транслятор tasm.exe. Ассемблерный модуль может быть отранслирован и отдельно, тогда в проекте он должен иметь расширение obj.
Второй тип согласования - согласование имен. Мы должны учесть:
1. Трансляторы Си различают заглавные и прописные буквы, поэтому вызываемая процедура должна быть написана одинаково в обоих модулях.
2. При трансляции к именам Си впереди добавляется символ подчеркивания, что следует учесть в ассемблерном модуле.
Наша программа выполняет те же действия, что и предыдущая программа этого параграфа, т.е. печатает строку. Печать осуществляется процедурой PRI_STR, которой передается как параметр указатель на эту строку. Обращаем ваше внимание на то, что вызываемая процедура в ассмблерном модуле объявлена как PUBLIC, т.е. ее имя будет помещено в объектный модуль. В свою очередь в модуле на языке Си эта процедура объявлена как extern.
На этом мы заканчиваем рассмотрение аспектов связанных с зыком ассемблера. Подробны об языке ассемблера и его использовании в языках высокого уровня можно найти в книге [], написанной одним из авторов этих.
Глава 2. Примеры использования языка Си
2.1 Сортировка
Практически каждый алгоритм сортировки можно разбить на три части:
- сравнение, определяющее упорядоченность пары элементов;
- перестановку, меняющую местами пару элементов;
- собственно сортирующий алгоритм, который осуществляет сравнение и перестановку элементов до тех пор, пока все элементы множества не будут упорядочены.
М е т о д п у з ы р ь к а ( обменная сортировкой с выбором).
Идея этого метода отражена в его названии. Самые легкие элементы массива "всплывают" наверх, самые "тяжелые" - тонут. Алгоритмически это можно. Реализуется так - будем просматривать весь массив "снизу вверх" и менять стоящие рядом элементы в том случае, если "нижний" элемент меньше, чем "верхний". Таким образом, мы вытолкнем наверх самый "легкий" элемент всего массива. Теперь повторим всю операцию для оставшихся неотсортированными N-1 элементов (т.е. для тех, которые лежат "ниже" первого).
#include <stdio.h>
#define swap(a,b) { int tmp; tmp=a; a=b; b=tmp; }
main()
{
int a[10], dim=10;
int i, j;
for (i=0;i<dim;i++)
{
printf("Элемент\n");
scanf("%d",&a[i]);
}
printf("Было\n");
for (i=0;i<dim; i++)
printf("%d\n",a[i]);
/* Проход массива "вниз", начиная с нулевого элемента */
for (i = 0; i < dim; i++)
/* Проход массива "вверх", начиная с последнего до i-го элемента */
for (j = dim-1; j > i; j--)
/* Сравнение двух соседних элементов и их обмен */
if(a[j-1] > a[j]) swap(a[j-1], a[j]);
printf("Стало\n");
for (i=0;i<dim; i++)
printf("%d\n",a[i]);
}
С о р т и р о в к а в ы б о р о м.
На этот раз при просмотре массива мы будем искать наименьший элемент, сравнивая его с первым. Если такой элемент найден, поменяем его местами с первым. Затем повторим эту операцию, но начнем не с первого элемента, а со второго. И будем продолжать подобным образом, пока не рассортируем весь массив.
#include <stdio.h>
#define swap(a,b) { int tmp; tmp=a; a=b; b=tmp; }
main()
{
int a[10], dim=10;
int i, j, k;
for (i=0;i<dim;i++)
{
printf("Элемент\n");
scanf("%d",&a[i]);
}
printf("Было\n");
for (i=0;i<dim; i++)
printf("%d\n",a[i]);
/* Проход массива, начиная с 0-го до предпоследнего элемента */
for (i = 0; i < dim-1; i++)
{
/* Проход массива, начиная с (i+1)-го до последнего элемента */
for (k = i, j = i+1; j < dim; j++)
if(a[j] < a[k]) k = j; /* Поискнаименьшего k-гоэл-та */ swap(a[k], a[i]); /* Перемещениенаименьшего "вверх" */
}
printf("Стало\n");
for (i=0;i<dim; i++)
printf("%d\n",a[i]);
}
М е т о д Ш е л л а.
Этот метод предложил Donald Lewis Shell в 1959 г. Основная идея алгоритма заключается в том, чтобы вначале устранить массовый беспорядок в массиве, сравнивая далеко стоящие друг от друга элементы. Как видно, интервал между сравниваемыми элементами (gap) постепенно уменьшается до единицы. Это означает, что на поздних стадиях сортировка сводится просто к перестановкам соседних элементов (если, конечно, такие перестановки являются необходимыми).