Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

osn_progr_final

.pdf
Скачиваний:
37
Добавлен:
12.02.2016
Размер:
3.27 Mб
Скачать

визначень тощо. Згаданий механізм був розроблений з метою зменшення залежності С-програми від архітектури комп’ютера та версії операційної системи.

Використовуються наступні директиви препроцесору:

#define, #include, #if, #elif, #else, #endif, #ifdef, #ifndef, #error, #line

4.6.1 Директива препроцесору #define

Директива #define –використовується для заміни ключових слів, констант , операторів осмисленими ідентифікаторами. Існує два види директиви #define:

1. Макровизначення

#define <ідентифікатор1> <ідентифікатор2>

При наявності відповідної директиви відбувається тривіальна текстова заміна першого ідентифікатора другим у випадку, коли перший становить окрему лексему. Після визначення деякого ідентифікатора він знову може входити в інші директиви #define:

#define WIDTH 180 #define LENGS (WIDTH+10)

Взагалі, прийнято макровизначення та макропідстановки записувати в верхньому регістрі. Після інтерпретації однієї макропідстановки препроцесор переходить послідовно до слідуючої інтерпретації. Остання обставина призводить до того, що директива виду #define x x не зациклює препроцесор. Директива #define може бути розміщена в будь-якій частині програми. Область дії відповідної директиви –від точки визначення до кінця файлу. В директиві #define допускається перенесення текстового рядочка:

#define якщо студен\

ти РЕГІ не будуть уважно слухати викладача на парі,\ то вони можуть не зрозуміти важливої деталі.

За допомогою #define можна управляти регістровими змінними:

#define REG1 register #define REG2 register #define REG3

REG1 int i; REG2 int j; REG3 int k;

Тут REG3 компілятор ігнорує.

2. Макроси (макропідстановки).

#define <ідентифікатор1> (<список параметрів>) <текст>

71

Текст в даному випадку може містити ідентифікатори, які розміщені в списку параметрів.

Приклад:

#define ADD(x,y)

((x)+(y));

#define MAX(x,y)

((x)>(y))?(x):(y)

 

int i;

i=ADD(5,4)*3 //це еквівалентно ((5)+(4))*3 i=ADD(b-3,6)*c//це еквівалентно ((b-3)+(6))*c

Визначимо функцію ADD в описаному вище прикладі так:

#define ADD (x,y) x+y

Тоді у відповідному рядку отримаємо 5+4*3. Тобто у випадку випадку відсутності дужок можна не досягти бажаного результату внаслідок пріоритету операцій.

Отже, макропідстановки (макроси) в деякому розумінні є аналогами функцій. Однак, є суттєва відмінність між макросами та функціями. Оскільки макроси - це звичайні текстові підстановки, то в місце їх виклику вставляється відповідний текст. Тому вони будуть працювати швидше, ніж функції. Адже в точці виклику функції відбувається генерація виклику, передача значень параметрів, повернення в точку виклику після роботи функції– всі ці дії вимагають час. Однак використання макросів значно збільшує текст самої програми.

З використанням директиви #define можна здійснити конкатенацію текстових елементів, тобто можна утворити нові ідентифікатори. В директиві #define конкатенація здійснюється синтаксично з використанням ##:

#define concat(x,y) x##y

concat (x,3)=5; //еквівалентно x3=5

Приклад:

#define AB ”стандарт”

#define A ”відхилення від”

#define B ”стандарту”

char *s=concat(A,B); printf(“s=%s”,s);

В прикладі надрукується “відхилення від стандарту”.

В макросах спочатку відбувається підстановка замість формальних параметрів їх фактичних значень, а потім послідовність дій, описаних

72

в макросі, тому надрукується не ”стандарт”, а “відхилення від стандарту”.

Використання формального параметру, перед яким стоїть значок # (тобто #<параметр>) приводить до перетворення цього параметру в текстовий рядок ”параметр”. Припустимо, що нам необхідно вивести на екран значення деякого параметра макросу в формі “параметр”=<значення>. Тоді можемо написати фрагмент програми:

#define write(x) printf(#x”=%d\n”,x) int a=5,b=3,c=2;

write(a); /*надрукується a=5*/ write(b); /*надрукується b=3*/ write(c); /*надрукується c=2*/

Тут спочатку відбувається перетворення конструкції #x в текстовий рядок #x->”x”, послідовність двох текстових рядків в функції printf сприймається як один

printf(“x””=%d\n”,x) printf(“x=%d\n”,x)

4.6.2 Директива #undef [<ім’я>]

Відмінює дію макровизначення чи макропідстановки, ім’я може бути відсутнім. Така директива не є помилкою в більшості компіляторів. Не вважається помилкою також використання імені, яке раніше не було визначене. У випадку використання макросів параметри можуть бути опущені:

#define ADD (x,y) (x)+(y) #define width 80

main ( )

{printf (“проба \n”); int i=ADD (5,4); printf(“i=%d \n”,i);

#undef ADD

{float i=ADD(3.5,4);

}}

4.6.3Директива #include

Єдва основні види:

#include “шлях”

#include<шлях>

Якщо використовуються подвійні лапки та вказується повний шлях до відповідного файлу, то пошук здійснюється у відповідності з

73

вказаним шляхом. Якщо шлях вказаний не повністю, то пошук здійснюється наступним чином:

1.в робочому біжучому каталозі, в якому знаходиться файл відповідної С-програми;

2.в каталогах, вказаних в командному рядку ;

3.в стандартних каталогах.

Якщо використовуються кутові дужки, то послідовність пошуку відрізняється тим, що ігнорується пошук в робочому каталозі, тобто файл шукається в каталогах, вказаних в командному рядку компіляції та в стандартних. Шлях в цій директиві також може задаватися з використанням макропідстановок:

#define myway “e:\bc\bin\bc\mylib.h”

#include myway

4.6.4 Директиви умовної компіляції: #if #elif #else #endif.

#if <обмежувально-константний вираз> <текст> [#elif<ОКВ> <текст>]

[#elif<ОКВ> <текст>] [#else <текст>]

#endif

Можна компілювати елементи програми в залежності від умов. ОКВ - це вираз, який не може містити цілих констант, змінних переліковного типу, операції sizeof (в компіляторах фірми Microsoft). У випадку істинного значення цього виразу відбувається компіляція тексту до найближчої директиви elif чи else. Далі знову здійснюється компіляція тексту у випадку істинності відповідного ОКВ , Відмітимо, що #endif-обов’язкова директива. Приклад:

# if sizeof(void*)==2 #define void2

#else

#define void more2 #endif

В ОКВ допускається використання препроцесорної операції, яка має синтаксис defined(<ім’я>):

#if defined (void2) printf (“ void=2”);

#endif

Приклад умовної компіляції виклику функції:

74

#include <stdio.h> double f(double x) {return x;}

double g(double x) {return x;}

double pf(double eps,double(*)(double)); tf(double ,double(*)(double));

main( ) {int i,j;

#if (i>0) pf(0,0001;f) #else tf(0,001;g) #endif }

Директиви умовної компіляції можуть бути вкладеними. Аналогічно, як і в звичайному операторі if, область дії кожної директиви визначається від endif до найближчої директиви if.

Приклад на вкладеність директиви if:

#if DLEVEL>5 #define SIGNAL 1 #if stackuse ==1 #define stack 200 #else

#define stack 100 #endif

#else

#define SIGNAL 100 #if stackuse==1 #define stack 100 #else

#define stack 300 #endif

#endif

З використанням відповідних директив можна здійснювати гнучке управління, наприклад, регістровими змінними:

#define REG3

#if defined (LILA)

#define REG1 register

#define REG 2

#else

#define REG2 register

#define REG1

#endif main( )

75

{

REG1 int a; REG2 int b; REG3 int c;

}

4.6.5 Директива #line

Директива #line<номер> <ім’я файла>-впливає на повідомлення компілятора про помилку у відповідному файлі чи рядку. Після відповідної директиви ім’я файлу в повідомленні компілятора буде змінене і відповідно номер рядка також буде встановлено іншим.

Біжучий номер рядка та ім’я вихідного файлу може бути отриманий в програмі з використанням змінних _line_ та file_. Ці змінні можуть бути корисними для побудови деяких діагностичних повідомлень:

#line 10 “myfile.cpp”

#define errormessage (cond) if(!cond)

printf (“помилка в рядку %d файла %s”,_line_,_file_);

4.6.6 Директива #error <повідомлення>

Перериває роботу препроцесора з видачею відповідного повідо-

млення

 

Fatal line file

<повідомлення>.

Приклад:

 

#if (MYZM !=0 || MYZM!=1)

#error “MYZM повинна=0 або 1”

#endif

4.7 ОПИСУВАЧІ З МОДИФІКАТОРАМИ.

4.7.1 Моделі пам’яті

Оперативна пам‘ять розбивається фізично на сегменти довжиною 64 Кб. Кожен з сегментів має певну адресу( номер), і кожна змінна визначається по деякому зміщенню всередині сегмента, тобто адреса змінної складається з двох частин - номер сегмента та зміщення. Програмі виділяються два сегменти: один сегмент для коду програми та один сегмент для статистичних та зовнішніх даних. Але в деяких випадках виникає необхідність виділяти більше місця для даних чи коду програми. В таких випадках для організаціі роботи програми необхідно вибирати іншу модель пам’яті.

76

Існують наступні моделі пам’яті: huge, large, compact, medium, small, tiny (microsoft C).

Інформація про можливий розмір даних та коду програми при різних моделях пам’яті наводиться в наступній таблиці:

 

HUGE

LARGE

COMPACT

MEDIUM

SMALL

TINY*

Код

>1

>1

1

>1

1

Записується

Дані

>1

>1

>1

1

1

в одному

 

 

 

 

 

 

сегменті

 

 

 

 

 

 

 

Однак, використовуючи певні модифікатори, можна визначати розміщення в пам’яті конкретних об’єктів, що відрізняється від загальноприйнятого в межах вибраної загальної моделі пам’яті.

4.7.2 Модифікатори типу доступу в пам’яті

Відповідно до механізму адресації, існують вказівники трьох типів (і відповідно, модифікатори):

-near -far -huge

Вказівники типу near використовуються лише у стандартних сегментах (моделі пам’яті small і tiny). Специфіка цих вказівників полягає в тому, що у всіх операціях використовується лише зміщення всередині сегмента, а адреса сегменту не використовується . Це приводить до того, що всі операції виконуються значно швидше.

Вказівники типу far використовують і адресу сегмента і зміщення, але адреса сегмента використовується лише для знаходження сегмента. Всі операції над вказівниками виконуються лише з врахуванням зміщення. Значення вказівника far циклічно змінюється в рамках одного сегмента.

Для вказівників типу huge у всіх операціях використовується і адреса сегмента і зміщення. Тому з вказівниками типу huge завжди корректно будуть працювати операції порівняння, додавання.

Модифікатори можуть стояти, як перед знаком *, так і безпосередньо перед ідентифікатором. Якщо модифікатор стоїть перед ідентифікатором, то він модифікує тип адреси, за якою даний об’єкт записується в пам’яті. Якщо модифікатор стоїть перед знаком *, то це означає, що відповідний вказівник, має тип, встановлений модифікатором, та вказує на дані, які розміщені в пам’яті згідно з даним модифікатором:

Приклад: main{

77

static int far p; static near int *x; x=&p;

}

В прикладі в останньому рядочку виникає помилка за рахунок використання даних з різною адресацією. Безпомилковий варіант:

main{

static int far p; static far int *x; x=&p;

}

В Microsoft C існує можливість модифікувати безпосередньо сам вказівник, тобто допускається конструкція виду:

int *far x; int far *far x;

int huge *near x;

В MSC крім описаних можливостей, існує чотири спеціальні вказівники типу near:

-cs –сегмент коду; -ds –сегмент даних; -ss–сегмент стеку;

-es–динамічна пам’ять.

4.7.3 Модифікатори const, volatile, cdecl, pascal, interrupt

Крім описаних модифікаторів, які модифікують тип доступу в пам’яті, використовують ряд інших модифікаторів: const, volatile, cdecl, pascal, interrupt.

Модифікатор const забороняє змінювати значення модифікованого об’єкту:

const int pi=3.14;

pi=3.15; // в цьому рядку буде помилка;

Модифікатор pascal використовується у випадках, коли виникає необхідність об’єднувати об’єктні коди різних мов програмування. С- компілятор при попередній обробці модифікує константи шляхом дописування перед кожною константою символу підкреслювання. Крім того, розрізняються великі та малі літери. В мові С існує специфічна форма передачі параметрів функціям. Параметри передаються, починаючи з останнього аргументу. Модифікатор pascal, записаний в С- програмі, забезпечує форму ідентифікатора та механізм передачі па-

78

раметрів в функцію, які прийняті в мові Паскаль та аналогічних мовах.

Існує опція компіляції, яка присвоює всім об’єктам тип pascal. При цьому виникає необхідність для деяких об’єктів залишити стандартний механізм мови С. Для цього використовується модифікатор cdecl.

Модифікатор interrupt використовується для оголошення функцій, які працюють з перериваннями процесора.

Модифікатор volatile вказує на те, що значення певної змінної може бути змінене не лише програмно, але й через певні переривання процесора. Така змінна є абсолютно нестабільною.

Приклад: volatile int t;

void interrupt timer( ) {t++;}

void wait (int interval) {t=0;

while (t<interval);} main( )

{ for (int i=0; i<20;i++) timer( );

wait (10);}

Модифікатори можуть використовуватися при побудові складених описувачів:

char far* (far* geting)(int far*);

Тут geting є вказівником на far функцію, що має аргумент вказівника на far int і повертає вказівник на far char. Порядок слідування модифікатора не суттєвий: far pascal==pascal far

79

5 ПРИНЦИПИ ТИПІЗАЦІЇ ДАНИХ.

Застосування програм у самих різних областях людської діяльності привело до необхідності підвищення надійності всього програмного забезпечення. Одним з напрямків підвищення надійності і, відповідно, вдосконалення мов програмування став розвиток типізації даних. Теорія типів даних виходить з того, що кожне з даних, які використовуються в програмі, належить одному і тільки одному типу. Тип даного визначає множину його можливих значень і набір операцій, що можуть виконуватись над цим даним. Дані конкретного типу в ряді випадків можуть бути перетворені в дані іншого типу. Такі перетворення можуть бути явними чи здійснюватись в ряді мов неявно.

Надійність програмного забезпечення є мірою ступеня автоматичного виявлення помилок, яке може бути виконаним або компілятором, або системою, що підтримує виконання скомпільованої програми.

Розглянемо один такий приклад. Припустимо, що масив а, що складається з 20 цілих чисел, індексується цілою змінною i таким чином, що i-я компонента масиву а позначається a[i]. Очевидно, що надійна програма повинна забезпечити виконання умови 1 i 20 у будь-якому місці програми, де зустрічається a[i].

Для цього є дві можливості. Традиційний підхід полягає в простому внесенні в готову програму перед кожним звертанням до масиву додаткового коду, що перевіряє виконання необхідної умови. Більш сучасний підхід полягає в перевірці значення i не під час його використання, а скоріше під час присвоювання значення змінній. Це вимагає додаткових зусиль програміста, що повинен специфікувати область значень i при описі змінної. Знаючи, що заданий тип змінної i обмежує можливі значення областю від 1 до 20, немає необхідності взагалі перевіряти значення індексу при звертанні до a[i]. Замість цього перевіряються значення, що присвоюються змінній i . Дуже часто законність таких присвоювань може бути перевірена під час компіляції, так що загальний обсяг фактично необхідного об'єктного коду перевірки допустимих значень i істотно зменшується. Ще однією перевагою такого підходу є те, що при цьому зростає ясність програм, адже область значень, які набуває кожна змінна, встановлюється явно.

Всі універсальні мови програмування, незважаючи на розходження в синтаксисі і ключових словах, реалізують ті самі канонічні структури: оператори присвоювання, цикли і розгалуження. В усіх

80

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]