русс | укр

Мови програмуванняВідео уроки php mysqlПаскальСіАсемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование


Linux Unix Алгоритмічні мови Архітектура мікроконтролерів Введення в розробку розподілених інформаційних систем Дискретна математика Інформаційне обслуговування користувачів Інформація та моделювання в управлінні виробництвом Комп'ютерна графіка Лекції


Зовнішні змінні


Дата додавання: 2014-11-28; переглядів: 1529.


Програма на C складається також з набору зовнішніх об'єктів — змінних або функцій. Прикметник «зовнішній» використовується як протиставлення «внутрішньому», який описує аргументи та змінні, визначені всередині функцій. Зовнішні змінні — означені поза межами будь-якої функції і, таким чином, потенційно доступні багатьом функціям. Самі функції завжди залишаються зовнішніми, оскільки C не дозволяє означати функції всередині інших функцій. Стандартно, зовнішні змінні і функції мають одну властивість, що полягає у тому, що всі звертання до них за тим самим ім'ям — навіть з функцій, компільованих окремо — посилаються на ту саму річ. (Стандарт називає це зовнішньою сполучністю.) У цьому сенсі, зовнішні змінні схожі до блоків COMMON мови Fortran або змінних з найвіддаленішого блока в Pascal. Ми дізнаємось пізніше, як означувати зовнішні змінні та функції, видимі тільки з того самого вихідного файла.

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

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

Зовнішні змінні також корисні через більшу область дії і тривалість життя. Автоматичні змінні являються внутрішніми для функцій; вони з'являються при вході у функцію і зникають при виході з неї. Напротивагу, зовнішні змінні — постійні, а отже утримують значення від одного виклику функції до іншого. Таким чином, якщо дві функції мусять спільно використовувати якісь дані, але жодна з них не викликає іншої, часто найзручніше, щоб спільні дані зберігалися у зовнішніх змінних, замість передачі їх туди-сюди у вигляді аргументів.

Дослідімо це питання глибше на більшому прикладі. Завдання полягатиме в написанні програми-калькулятора, яка б забезпечувала операторами +, -, * і /. Оскільки це легше втілити - калькулятор використовуватиме зворотній польський запис замість інфіксного (звичайного). (Зворотня польська нотація використовується у деяких кишенькових калькуляторах а також у таких мовах як Forth або Postscript.)

У зворотньому польському записі, кожний оператор слідує за операндами; інфіксний вираз

(1 - 2) * (4 + 5)

буде введено як

1 2 - 4 5 + *

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

Втілення — просте. Кожний операнд проштовхується у стек; коли надходить оператор, належне число операндів (двоє — для бінарних операторів) виштовхуються зі стеку, і застосовано оператор; результат проштовхується назад у стек. У прикладі вище, скажімо, 1 і 2 проштовхуються, а потім замінюються наслідком їхнього віднімання: -1. Згодом, 4 і 5 проштовхуються, і замінюються 9. Результат множення -1 на 9, що дорівнюватиме -9, замінить і їх у стеку. Нарешті, коли досягнуто кінця рядка вводу, значення з верхівки стеку виштовхується і виводиться на екран.

Таким чином, структура програми складатиметься із циклу, що виконуватиме відповідну дію, залежно від оператора чи операнда, що надійшов:

while (наступний оператор чи операнд не є кінцем файла)

if (число)

проштовхути його

else if (оператор)

виштовхнути операнди

виконати дію

проштовнути результат

else if (новий рядок)

виштовхнути і вивести верхівку стеку

else

помилка

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

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

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

#include (файли, що включаються)

#define (означення)

 

оголошення функцій для main

 

main() { ... }

 

зовнішні змінні для проштовхування (push) і виштовхування (pop)

 

void push( double f ) { ... }

double pop(void) { ... }

 

int getop(char s[]) { ... }

функції, викликані getop

Пізніше ми розглянемо, як це можна розбити на два або більше вихідних файли.

Функція main складатиметься з циклу, що міститиме великий перемикач (switch) типу оператора чи операнда; це — типовіше використання switch ніж те, що ми бачили у Розділі 3.4.

#include <stdio.h>

#include <stdlib.h> /* для atof() */

 

#define MAXOP 100 /* максимальний розмір операнда або оператора */

#define NUMBER '0' /* сигналізувати, що номер знайдено */

 

int getop(char []);

void push(double);

double pop(void);

 

/* калькулятор зі зворотньою польською нотацією */

main()

{

int type;

double op2;

char s[MAXOP];

 

while ((type = getop(s)) != EOF) {

switch (type) {

case NUMBER:

push(atof(s));

break;

case '+':

push(pop() + pop());

break;

case '*':

push(pop() * pop());

break;

case '-':

op2 = pop();

push(pop() - op2);

break;

case '/':

op2 = pop();

if (op2 != 0.0)

push(pop() / op2);

else

printf("error: zero divisor\n");

break;

case '\n':

printf("\t%.8g\n", pop());

break;

default:

printf("error: unknown command %s\n", s);

break;

}

}

return 0;

}

Оскільки + й * це переставні (комутативні) оператори, послідовність у якій виштовхнуті операнди поєднуються не має значення, але у випадку - та /, правий і лівий операнд повинні розрізнятися. Наприклад, у

push(pop() - pop()); /* НЕПРАВИЛЬНО */

послідовність, в якій два виклики pop обчислено, не визначено. Щоб забезпечити правильну послідовність, потрібно виштовхнути перше значення у тимчасову змінну, як ми це зробили вmain:

#define MAXVAL 100 /* максимальна глибина стеку */

 

int sp = 0; /* наступна вільна позиція у стеку */

double val[MAXVAL]; /* стек значень */

 

/* push: проштовхування у стек значень */

void push(double f)

{

if (sp < MAXVAL)

val[sp++] = f;

else

printf("error: stack full, can't push %g\n", f);

}

 

/* pop: виштовхнути і повернути верхне значення зі стеку */

double pop(void)

{

if (sp > 0)

return val[--sp];

else {

printf("error: stack empty\n");

return 0.0;

}

}

Змінна вважається зовнішньою, якщо її означено поза межами будь-якої функції. Таким чином, стек та індекс стеку, які спільно використовуватимуться push і pop, визначено зовні цих функцій. Але сама main не звертається до стеку або положення у стеку, тож представлення може бути прихованим.

Тепер, звернімо нашу увагу на втілення getop — функції, що видобуває наступний оператор чи операнд. Це просте завдання. Пропустити пробіли і табуляцію. Якщо наступний символ не скидається на число або десяткову точку, повернути його. У протилежному випадку, зібрати ланцюжок чисел (які можуть включати десяткову точку) і повернути NUMBER, яке сигналізуватиме, що число — успішно здобуто.

#include <ctype.h>

 

int getch(void);

void ungetch(int);

 

/* getop: отримати наступний знак або числовий операнд */

int getop(char s[])

{

int i, c;

 

while ((s[0] = c = getch()) == ' ' || c == '\t')

;

s[1] = '\0';

if (!isdigit(c) && c != '.')

return c; /* не є числом */

i = 0;

if (isdigit(c)) /* зберегти частину, що є цілим */

while (isdigit(s[++i] = c = getch()))

;

if (c == '.') /* зберегти дробову частину */

while (isdigit(s[++i] = c = getch()))

;

s[i] = '\0';

if (c != EOF)

ungetch(c);

return NUMBER;

}

Що таке getch і ungetch? Часто бувають випадки, коли програма не може визначити, що вона прочитала досить вводу доти, доки не прочитано забагато. Один з прикладів — коли програма збирає знаки, що складають число, і поки не зустрінеться перший не-цифровий знак, число не вважатиметься повним. Але тоді програмою прочитано один зайвий знак, до якого вона не була готова.

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

Як вони співпрацюють разом — не складно. Функція ungetch поміщає виштовхнутий знак у спільно використовуваний буфер — символьний масив. Функція getch читає з буфера, якщо там щось є, і викликає getchar, якщо буфер порожній. Має існувати також індексна змінна, яка би реєструвала положення поточного знака в буфері.

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

#define BUFSIZE 100

 

char buf[BUFSIZE]; /* буфер для ungetch */

int bufp = 0; /* наступна вільна позиція у buf */

 

int getch(void) /* отримати (можливо виштовхнутий) знак */

{

return (bufp > 0) ? buf[--bufp] : getchar();

}

 

void ungetch(int c) /* виштовхнути знан назад у ввід */

{

if (bufp >= BUFSIZE)

printf("ungetch: too many characters\n");

else

buf[bufp++] = c;

}

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

Вправа 4-3. Маючи основний каркас, розширте програму-калькулятор. Додайте оператор частки (%) і можливість використання від'ємних чисел.

Вправа 4-4. Додайте команди для виводу верхніх елементів стеку без їхнього виштовхування, копіювання цих елементів і перестановки місцями перших двох. Додайте команду очистки стеку.

Вправа 4-5. Додайте доступ до таких функцій бібліотеки, як sin, exp і pow. Подивіться<math.h> у Додатку Б, Розділі 4.

Вправа 4-6. Додайте команди для оперування змінними. (Це не складно, надати двадцять шість змінних з назвами, що складаються з однієї літери.) Додайте змінну для найостаннішого виведеного значення.

Вправа 4-7. Напишіть функцію ungets(s), яка би виштовхувала цілий ланцюжок у ввід. Чи повинна ungets знати про buf і bufp, чи їй достатньо лише використовувати ungetch?

Вправа 4-8. Припустімо, що ніколи не буде більш ніж одного виштовнутого знака. Змінітьgetch і ungetch відповідно.

Вправа 4-9. Наші getch і ungetch не справляються належним чином із виштовнутим EOF. Виріште, яким буде їхнє поводження у випадку виштовхнутого EOF, після чого перепишіть їх.

Вправа 4-10. Альтернативною організацією програми буде використання getline для зчитування цілого ввідного рядка; це робить getch і ungetch зайвими. Переробіть калькулятор із застосуванням цього підходу.


<== попередня лекція | наступна лекція ==>
Функції, які не повертають цілих | Правила області дії


Онлайн система числення Калькулятор онлайн звичайний Науковий калькулятор онлайн