Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
knrc (1).pdf
Скачиваний:
12
Добавлен:
12.02.2016
Размер:
925.69 Кб
Скачать

4.5. ФАЙЛИ ЗАГОЛОВКА

91

extern int sp; extern double val[];

void push(double f) { ... }

double pop(void) { ... }

у другому файлi:

int sp = 0;

double val[MAXVAL];

Оскiльки оголошення extern у першому файлi знаходяться попереду i зовнi визначень функцiй, вони стосуються всiх функцiй. Одного оголошення вистачить для цiлого першого файла. Така сама органiзацiя була би потрiбною, якби sp i val слiдували за своїм використанням у тому самому файлi.

4.5Файли заголовка

Давайте розглянемо тепер подiл програми-калькулятора на декiлька вихiдних файли, так нiби кожна з її складових була значно бiльшою. Функцiя main опинилася би в одному файлi, який ми назвали би main.c; push, pop i їхнi змiннi розмiщено би у другому, stack.c; getop у третьому — getop.c. Ми роздiлили їх одне вiд одного, оскiльки вони складали би окремо скомпiльовану бiблiотеку у справжнiй програмi.

Iснує ще одна рiч, про яку варто потурбуватися — це визначення i оголошення, спiльно використовуванi цими файлами. Наскiльки це можливо, ми би хотiли зосередити цi речi у одному мiсцi, тож iснуватиме тiльки одна копiя, яку буде прочитано i яку слiд пiдтримувати у робочому станi пiд час розвитку програми. Вiдповiдно, ми помiстимо цей спiльний матерiал у файлi заголовка calc.h, який буде включено у разi потреби. (Рядок #include описано у Роздiлi 4.11.) Отримана в результатi програма може виглядати так:

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

92

РОЗДIЛ 4. ФУНКЦIЇ ТА СТРУКТУРА ПРОГРАМ

4.6Статичнi змiннi

Змiннi sp i val зi stack.c а також buff i bufp iз getch.c iснують для приватного використання у функцiях вiдповiдних вихiдних файлiв i не призначенi для доступу чимось iншим. Оголошення static зовнiшньої змiнної або функцiї обмежує зону дiї цього об’єкту рештою вихiдного файла, який буде скомпiльовано. Зовнiшнiй static, таким чином, дає можливiсть приховати такi назви як buf i bufp у комбiнацiї з getchungetch, якi в свою чергу, теж повиннi бути зовнiшнiми для спiльного їхнього використання, але чиї подробицi не повиннi бути видимими користувачам getch i ungetch. Статичний тип зберiгання даних вказується шляхом додання слова static перед звичайним оголошенням. Якщо двi функцiї i двi змiннi скомпiльовано у одному файлi,

4.7. РЕГIСТРОВI ЗМIННI

93

як от

 

static char buf[BUFSIZE];

/* буфер для ungetch */

static int bufp = 0;

/* наступне вiльне положення в buf */

int getch(void) { ... }

 

void ungetch(int c) { ... }

 

тодi жодна iнша функцiя не в змозi буде звернутися до buf i bufp i цi назви не створюватимуть конфлiкту з такими самими назвами в iнших файлах тiєї самої програми. Аналогiчно, змiннi, якi використовуються функцiями push i pop для манiпулювання стеком, можна приховати, якщо оголосити sp i val як static.

Зовнiшнi статичнi змiннi найчастiше вживаються зi змiнними, але їх можна застосовувати також до функцiй. Звично, назви функцiй є глобальними, тобто їх видно з будь-якої частини програми. Проте, якщо функцiю оголошено як static, її назва стане невидимою поза межами файла, в якому її було оголошено.

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

Вправа 4-11. Змiнiть getop, таким чином, щоб їй не потрiбно було використовувати ungetch. Пiдказка: застосуйте внутрiшню статичну змiнну.

4.7Регiстровi змiннi

Оголошення регiстрової змiнної вказує компiлятору, що дана змiнна буде iнтенсивно використовуватись. Iдея полягає у тому, що регiстровi змiннi помiщено у регiстри процесору, що призводить до менших i швидших програм. Але компiлятори вiльнi iгнорувати цю пораду. Оголошення регiстрових змiнних може виглядати як

register int x; register char c;

i так далi. Регiстровi оголошення можуть застосовуватись до автоматичних змiнних i до формальних параметрiв функцiї. У останньому випадку, це може мати вигляд

f(register unsigned m, register long n)

{

register int i;

94

РОЗДIЛ 4. ФУНКЦIЇ ТА СТРУКТУРА ПРОГРАМ

...

}

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

4.8Структура блокiв

C не є блоково-структурованою мовою на зразок Pascal чи iнших мов, оскiльки функцiю не можна означити всерединi iншої функцiї. З iншого боку, змiннi можуть бути описанi у блок-структурованому виглядi всерединi функцiї. Оголошення змiнних (включаючи iнiцiалiзацiю) може слiдувати за лiвою фiгурною дужкою, яка започатковує будь-яке складене твердження, а не тiльки починає функцiю. Змiннi, оголошенi таким чином, приховано вiд однаково названих змiнних у зовнiшнiх блоках i залишаються iснувати до вiдповiдної правої фiгурної дужки. Наприклад, у

if (n >

0)

{

int

i;

/* оголошення нової i */

for (i

= 0; i < n; i++)

 

...

 

}

 

 

зоною дiї змiнної i є iстинне вiдгалуження if; це не матиме жодного стосунку до жодного i поза межами цього блока. Автоматичну змiнну, оголошену та iнiцiйовану у блоцi, iнiцiйовано кожний раз при входженнi у цей блок.

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

int x; int y;

f(double x)

{

double y;

}

4.9. IНIЦIАЛIЗАЦIЯ

95

змiнна x, як параметр функцiї f типу double, немає нiчого спiльного з зовнiшньою x типу int. Те саме стосується змiнної y. Але, загалом, краще уникати назв змiнних, що збiгаються з назвами з iнших зон дiї, занадто велика ймовiрнiсть плутанини i помилок.

4.9Iнiцiалiзацiя

Про iнiцiалiзацiю мимоходом вже згадувалося багато разiв, але завжди периферiйно, як частина iншої теми. Цей роздiл пiдводить пiдсумок деяких правил, пiсля того як ми оговорили рiзноманiтнi типи зберiгання. У випадку вiдсутностi явної iнiцiалiзацiї, зовнiшнi i статичнi змiннi буде гарантовано надано значення нуль. Автоматичнi i регiстровi змiннi матимуть невизначене початкове значення (тобто непотрiб).

Скалярнi змiннi може бути iнiцiйовано пiд час їхнього визначення шляхом додачi до їхньої назви знака рiвностi i виразу:

int x = 1;

char squota = ’/’’;

long day = 1000L * 60L * 60L * 24L; /* мiлiсекунд у день */

Для зовнiшнiх i статичних змiнних, iнiцiалiзатор повинен бути сталим виразом; iнiцiалiзацiя вiдбувається один раз, концептуально до того як програма починає виконуватись. Для автоматичних i регiстрових змiнних, iнiцiалiзатор не обов’язково повинен бути константою: це може бути будь-який вираз, включаючи попередьно-визначеннi значення, навiть виклики функцiй. Так, наприклад, iнiцiалiзацiю в програмi бiнарного пошуку з Роздiлу 3.3 може бути написано як

int binsearch(int x, int v[], int n)

{

int low = 0;

int high = n - 1; int mid;

...

}

замiсть

int low, high, mid;

low = 0; high = n - 1;

96

РОЗДIЛ 4. ФУНКЦIЇ ТА СТРУКТУРА ПРОГРАМ

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

Масив може бути iнiцiйовано через додання до оголошення списку iнiцiалiзаторiв, включених у фiгурнi дужки i роздiлених комою. Як приклад — iнiцiалiзацiя масиву days з кiлькiстю днiв кожного мiсяця:

int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

Коли розмiр масиву не вказано, компiлятор обчислить його, перелiчивши iнiцiалiзатори, 12 у цьому випадку.

Якщо надано менше iнiцiалiзаторiв, нiж вказаний розмiр масиву, рештi буде присвоєне значення нуль у випадку зовнiшнiх, статичних i автоматичних змiнних. Помилка виникне у випадку зайвих iнiцiалiзаторiв. Не iснує способу вказати повторнi iнiцiалiзатори, так само як елемент посерединi масиву, без вказiвки попереднiх значень також.

Символьнi масиви — це спецiальний випадок iнiцiалiзаторiв: можна використати ланцюжок замiсть нотацiї з фiгурних дужок i ком:

char pattern[] = "ould";

що є скороченням для довшого, але рiвнозначного

char pattern[] = { ’o’, ’u’, ’l’, ’d’, ’\0’ };

В обох випадках, масив складатиметься з п’яти елементiв: чотири лiтери i кiнцевий

’ 0’.

4.10Рекурсiя

Функцiї у C можуть використовуватись рекурсивно; тобто, функцiя може викликати сама себе безпосередньо або опосередково. Розглянемо вивiд числа як символьний ланцюжок. Як ми вказали ранiше, цифри генеруються у неправильнiй послiдовностi: молодшi числа доступнi ранiше за старшi числа, хоча їх потрiбно виводити навпаки.

Iснує два способи розв’язання цiєї проблеми. Одним буде збереження цифр у масив пiд час їхнього генерування, потiм видрукувати їх у зворотнiй послiдовностi, як ми це здiйснили з itoa у Роздiлi 3.6. Альтернативою є рекурсивне вирiшення, у якому printd спочатку викликає себе щоби впоратись з початковими цифрами, пiсля чого виводить кiнцевi цифри. Майте на увазi, що ця версiя може зазнати невдачi на найбiльшому вiд’ємному числi.

4.10. РЕКУРСIЯ

97

#include <stdio.h>

/* printd: вивести n як десяткове */ void printd(int n)

{

if (n < 0) { putchar(’-’); n = -n;

}

if (n / 10) printd(n / 10);

putchar(n % 10 + ’0’);

}

Пiд час виклику функцiєю самої себе рекурсивно, кожне звернення отримує свiжий набiр усiх автоматичних змiнних, незалежних вiд попереднього набору. Таким чином, у printd(123), перша printd отримує аргумент n = 123. Вона передає 12 другому printd, яка в свою чергу передає 1 третьому. printd третього рiвня виводить 1 пiсля чого повертається до printd другого рiвня, яка виводить 2, потiм повертається до першого рiвня. Вiн виводить 3 i завершується.

Iнший хороший приклад рекурсiї, це quicksort, алгоритм сортування, розроблений C.A.R. Hoare у 1962 роцi. Маючи масив, вибирається один елемент i решта розбивається на двi групи — тi, що меншi за вибраний елемент i тi, що бiльше або дорiвнюють йому. Той самий процес потiм застосовано рекурсивно до цих двох груп. Коли пiдгрупа залишилась з менше нiж двома елементами, вона не потребує жодного сортування; це зупиняє рекурсiю.

Наша версiя quicksort не є найшвидшою з можливих, але це одна з найпростiших. Ми використовуємо середнiй елемент кожної частини масиву для подальшого подiлу.

/* qsort: сортує v[left]...v[right] у послiдовностi зростання */ void qsort(int v[], int left, int right)

{

int i, last;

void swap(int v[], int i, int j);

if (left >= right)

/*

нiчого не робити,

якщо масив */

return;

/*

мiстить менше нiж

два елементи */

swap(v, left, (left + right)/2); /* перемiстити елемент подiлу */

last =

left;

/* до v[0]

*/

for (i

= left + 1; i <= right; i++)

/* подiлити

*/

if (v[i] < v[left])

 

 

 

swap(v, ++last, i);

 

 

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