Функціональний підхід


Як визначити термін "програма"? Взагалі це послідовність операцій над структурами даних, що реалізують алгоритм розв'язання конкретної задачі. На початку проектування задачі ми розмірковуємо відносно того, що повинна робити наша програма, які конкретні задачі вона повинна розв'язувати, та які алгоритми при цьому повинні бути реалізовані. Буває, і це характерно для більшості задач, вихідна задача досить довга та складна, у зв'язку з чим програму складно проектувати та реалізовувати, а тим більше супроводжувати, якщо не використовувати методів керування її розмірами та складністю. Для цього потрібно використати відомі прийоми функціонально-модульного програмування для структурування програм, що полегшує їх створення, розуміння суті та супровід.
Розв'язання практичної задачі проходить у кілька етапів, зміст яких подає таблиця 1.16.
Організація програми на Сі досить логічна. Мова Сі надає надзвичайно високу гнучкість для фізичної організації програми. Нижче наведена типова організація невеликого програмного проекту на Сі:

Нижче піде мова про процедурне (функціональне) програмування на Сі. Існують досить добре розвинуті методи процедурного програмування, що базуються на моделі побудови програми як деякої сукупності функцій. Прийоми програмування пояснюють, як розробляти, організовувати та реалізовувати функції, що складають програму.
Структура кожної функції співпадає зі структурою головної функції програми main(). Функції іноді ще називають підпрограмами.
Основу процедурного програмування на будь-якій мові програмування складає процедура (походить від назви) або функція (як різновид, що саме відповідає мові програмування Сі).
Функція - модуль, що містить деяку послідовність операцій. Її розробка та реалізація у програмі може розглядатися як побудова операцій, що вирішують конкретну задачу (підзадачу). Однак взагалі функція може розглядатися окремо як єдина абстрактна операція, і, щоб її використовувати, користувачеві необхідно зрозуміти інтерфейс функції - її вхідні дані та результати виконання. Легко буде зрозуміти ту функцію, що відповідає абстрактним операціям, необхідним для рішення задачі. Функцію та її використання у програмі можна у такому разі представляти у термінах задачі, а не в деталях реалізації. Припустимо, необхідно розробити функціональний модуль, що розв'язує наступне завдання: існує вхідний список певних даних, який необхідно відсортувати, переставляючи його елементи у визначеному порядку. Ця функція може бути описана, як абстрактна операція сортування даних, що може бути частиною вирішення деякої підмножини задач. Функція, що реалізує цю операцію, може бути використана у багатьох програмах, якщо вона створена як абстракція, що не залежить від реалізації (контексту програми).

Таблиця 1.16. Типові етапи розв'язання задач

Етапи Опис
Постановка задачі та її змістовний аналіз 1. Визначити вхідні дані, які результати необхідно отримати і в якому вигляді подавати відповіді. 2. Визначити за яких умов можливо отримати розв'язок задачі, а за яких - ні. 3. Визначити, які результати вважатимуться вірними.
Формалізація задачі, вибір методу її розв'язання. (математичне моделювання задачі) 1. Записати умову задачі за допомогою формул, графіків, рівнянь, нерівностей, таблиць тощо. 2. Скласти математичну модель задачі, тобто визначити зв'язок вихідних даних із відповідними вхідними даними за допомогою математичних співвідношень з урахуванням існуючих обмежень на вхідні, проміжні та вихідні дані, одиниці її виміру, діапазон зміни тощо. 3. Вибрати метод розв'язку задачі.
Складання алгоритму розв'язання задачі Алгоритм більшою мірою визначається обраним методом, хоча один і той самий метод може бути реалізований за допомогою різних алгоритмів. Під час складання алгоритму необхідно враховувати всі його властивості.
Складання програми Написання програми на мові програмування
Тестування і відлагодження програми Перевірка правильності роботи програми за допомогою тестів і виправлення наявних помилок. Тест - це спеціально підібрані вхідні дані та результати, отримані в результаті обробки програмою цих даних.
Остаточне виконання програми, аналіз результатів Після остаточного виконання програми необхідно провести аналіз результатів. Можлива зміна самого підходу до розв'язання задачі та повернення до першого етапу для повторного виконання усіх етапів.

Функції мають параметри, тому їх операції узагальнені для використання будь-якими фактичними аргументами відповідного типу. Що є вхідними даними для функції ? Вхідними даними для неї є аргументи та глобальні структури даних, що використовуються функцією. Вихідними даними є ті значення, які функція повертає, а також зміни глобальних даних, модифікації.
Функціональний модуль, що не використовує глобальні дані, параметризується вхідними параметрами. Функція - це операція над будь-якими аргументами відповідного типу, адже вона не оперує конкретними об'єктами у програмі. Тому її можна використовувати безліч разів з різними параметрами, і не тільки в одній програмі, а й в інших із структурами даних того ж типу. Інтерфейс буде зрозумілий з опису прототипу функції, а об'єкти даних, описані в його реалізації, зрозумілі з локальних оголошень функції. Тому при параметризації входу та локалізації описів функція представляє собою тип самодокументованого модуля, який легко використовувати. Крім цього, функціям притаманна модульність. Її широко використовують для надання функціям більшої ясності, можливості повторного використання, що, таким чином, допомагає скоротити витрати, пов'язані з її реалізацією та супроводом.

1.16.1 Функції
Як було сказано вище, функції можуть приймати параметри і повертати значення. Будь-яка програма на мові Сі складається з функцій, причому одна з яких обов'язково повинна мати ім'я main().
Синтаксис опису функції має наступний вигляд :
тип_поверт_значення ім'я_функції ([список_аргументів])
{
оператори тіла функції
}

Рис. 1.20.Синтаксис опису функції

Слід чітко розрізняти поняття опису та представлення функцій.
Опис функції задає її ім'я, тип значення, що повертається та список параметрів. Він дає можливість організувати доступ до функції (розташовує її в область видимості), про яку відомо, що вона external (зовнішня). Представлення визначає, задає дії, що виконуються фунцією при її активізації (виклику).
Оголошенню функції можуть передувати специфікатори класу пам'яті extern або static.
extern - глобальна видимість у всіх модулях (по замовчуванню);
static - видимість тільки в межах модуля, в якому визначена функція.
Тип значення, яке повертається функцією може бути будь-яким, за виключенням масиву та функції (але може бути покажчиком на масив чи функцію). Якщо функція не повертає значення, то вказується тип void.

1.16.2 Функції, що не повертають значення
Функції типу void (ті, що не повертають значення), подібні до процедур Паскаля. Вони можуть розглядатися як деякий різновид команд, реалізований особливими програмними операторами. Оператор func(); виконує функцію void func() , тобто передасть керування функції, доки не виконаються усі її оператори. Коли функція поверне керування в основну програму, тобто завершить свою роботу, програма продовжить своє виконання з того місця, де розташовується наступний оператор за оператором func().
/*демонстраційна програма*/
#include<stdio.h>
void func1(void);
void func2(void);
main()
{
func1();
func2();
return 0;
}
void func1(void)
{
/* тіло */
}
void func2(void)
{
/* тіло */
}
Звернемо увагу на те, що текст програми починається з оголошення прототипів функцій - схематичних записів, що повідомляють компілятору ім'я та форму кожної функції у програмі. Для чого використовуються прототипи? У великих програмах це правило примушує Вас планувати проекти функцій та реалізовувати їх таким чином, як вони були сплановані. Будь-яка невідповідність між прототипом (оголошенням) функції та її визначенням (заголовком) призведе до помилки компіляції. Кожна з оголошених функцій має бути визначена у програмі, тобто заповнена операторами, що її виконують. Спочатку йтиме заголовок функції, який повністю співпадає з оголошеним раніше прототипом функції, але без заключної крапки з комою. Фігурні дужки обмежують тіло функції. В середині функцій можливий виклик будь-яких інших функцій, але неможливо оголосити функцію в середині тіла іншої функції. Нагадаємо, що Паскаль дозволяє працювати із вкладеними процедурами та функціями.
Надалі розглянемо приклад програми, що розв'язує відоме тривіальне завдання - обчислює корені звичайного квадратного рівняння, проте із застосуванням функціонального підходу:
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <math.h>
float A,B,C;
/*функція прийому даних*/
void GetData()
{
clrscr();
printf("Input A,B,C:");
scanf("%f%f%f",&A,&B,&C);
}
/*функція запуску основних обчислень*/
void Run()
{
float D;
float X1, X2;
if ((A==0) && (B!=0))
{
X1 = (-C)/B;
printf("\nRoot: %f",X1);
exit(0);
}
D = B*B - 4*A*C;
if (D<0) printf("\nNo roots...");
if (D==0)
{
X1=(-B)/(2*A);
printf("\nTwo equal roots: X1=X2=%f",X1);
}
if (D>0)
{
X1 = (-B+sqrt(D))/(2*A);
X2 = (-B-sqrt(D))/(2*A);
printf("\nRoot X1: %f\nRoot X2: %f",X1,X2);
}
}
/*головна функція програми/
void main()
{
GetData();
Run();
}

Якщо в описі функції не вказується її тип, то по замовчуванню він приймається як тип int. У даному випадку обидві функції описані як void, що не повертають значення. Якщо ж вказано, що функція повертає значення типу void, то її виклик слід організовувати таким чином, аби значення, що повертається, не використовувалося б.
Просто кажучи, таку функцію неможливо використовувати у правій частині виразу. В якості результату функції остання не може повертати масив, але може повертати покажчик на масив. У тілі будь-якої функції може бути присутнім вираз return; який не повертає значення. І, насамкінець, усі програмні системи, написані за допомогою мови Сі , повинні містити функцію main(), що є вхідною точкою будь-якої системи. Якщо вона буде відсутня, завантажувач не зможе зібрати програму, про що буде отримано відповідне повідомлення.

1.16.3 Передача параметрів
Усі параметри, за винятком параметрів типу покажчик та масивів, передаються за значенням. Це означає, що при виклику функції їй передаються тільки значення змінних. Сама функція не в змозі змінити цих значень у викликаючій функції. Наступний приклад це демонструє:
#include<stdio.h>
void test(int a)
{
a=15;
printf(" in test : a==%d\n",a);
}
void main()
{
int a=10;
printf("before test : a==%d\n",a);
test(a);
printf("after test : a==%d\n",a);
}
При передачі параметрів за значенням у функції утворюється локальна копія, що приводить до збільшення об'єму необхідної пам'яті. При виклику функції стек відводить пам'ять для локальних копій параметрів, а при виході з функції ця пам'ять звільняється. Цей спосіб використання пам'яті не тільки потребує додаткового її об'єму, але й віднімає додатковий час для зчитування. Наступний приклад демонструє, що при активізації (виклику) функції копії створюються для параметрів, що передаються за значенням, а для параметрів, що передаються за допомогою покажчиків цього не відбувається. У функції два параметри - one, two - передаються за значенням, three - передається за допомогою покажчика. Так як третім параметром є покажчик на тип int, то він, як і всі параметри подібного типу, передаватиметься за вказівником:
#include <stdio.h>
void test(int one, int two, int * three)
{
printf( "\nАдреса one дорівнює %р", &one );
printf( "\nАдреса two дорівнює %р", &two );
printf( "\nАдреса three дорівнює %р", &three );
*three+=1;
}
main()
{
int a1,b1;
int c1=42;
printf( "\nАдреса a1 дорівнює %р", &a1 );
printf( "\nАдреса b1 дорівнює %р", &b1 );
printf( "\nАдреса c1 дорівнює %р", &c1 );
test(a1,b1,&c1);
printf("\nЗначення c1 = %d\n",c1);
}

На виході ми отримуємо наступне:
Адреса а1 дорівнює FEC6
Адреса b1 дорівнює FEC8
Адреса c1 дорівнює FECA
Адреса one дорівнює FEC6
Адреса two дорівнює FEC8
Адреса three дорівнює FECA
Значення c1 = 43
Після того, як змінна *three в тілі функції test збільшується на одиницю, нове значення буде присвоєно змінній c1, пам'ять під яку відводиться у функції main().
У наступному прикладі напишемо програму, що відшукує та видаляє коментарі з програми на Сі. При цьому слід не забувати коректно опрацьовувати рядки у лапках та символьні константи. Вважається, що на вході - звичайна програма на Сі. Перш за все напишемо функцію, що відшукує початок коментарю (/*):
/*функція шукає початок коментарю */
void rcomment(int c)
{
int d;
if (c=='/')
if (( d=getchar())=='*')
in_comment();
else
if (d=='/')
{
putchar(c);
rcomment(d);
}
else
{
putchar(c);
putchar(d);
}
else
if (c=='\''|| c=='"') echo_quote(c);
else putchar(c);
}
Функція rcomment(int c) відшукує початок коментарю, а коли знаходить, викликає функцію in_comment(), що відшукує кінець коментарю. Таким чином, гарантується, що перша процедура дійсно ігноруватиме коментар:
/*функція відшукує кінець коментарю */
void in_comment(void)
{
int c,d;
c=getchar();
d=getchar();
while (c!='*'|| d!='/')
{
c=d;
d=getchar();
}
}
Крім того, функція rcomment(int c) шукає також одинарні та подвійні дужки, та якщо знаходить, викликає echo_quote(int c). Аргумент цієї функції показує, зустрілась одинарна або подвійна дужка. Функція гарантує, що інформація всередині дужок відображається точно та не приймається помилково за коментар:
/*функція відображає інформацію без коментарю */
void echo_quote(int c)
{
int d;
putchar(c);
while ((d=getchar()) !=c)
{
putchar(d);
if (d=='\\')
putchar(getchar());
}
putchar(d);
}
До речі, функція echo_quote(int c) не вважає лапки, що слідують за зворотною похилою рискою, заключними. Будь-який інший символ друкується так, як він є насправді. А на кінець текст функції main() даної програми, що відкривається переліком прототипів визначених нами функцій:
/* головна програма */
#include <stdio.h>
void rcomment(int c);
void in_comment(void);
void echo_quote(int c);
main()
{
int c,d;
while ((c=getchar())!=EOF)
rcomment(c) ;
return 0;
}
Програма завершується, коли getchar() повертає символ кінця файлу. Це був типовий випадок проектування програми із застосуванням функціонального підходу

1.16.4 Функції із змінним числом параметрів
Інколи у функції потрібно передати деяке число фіксованих параметрів та невизначене число додаткових. В цьому випадку опис функції буде мати вигляд :
тип ім'я_функції(список параметрів, ...)
Список аргументів включає в себе скінченне число обов'язкових параметрів (цей список не може бути порожнім), після якого на місці невизначеного числа параметрів ставиться три крапки. Для роботи з цими параметрами у файлі stdarg.h визначений тип списку va_list і три макроси: va_start, va_arg, va_end.
Макрос va_start має синтаксис :
void va_start(va_list ap, lastfix);
Цей макрос починає роботу зі списком, встановлюючи його покажчик ap на перший аргумент зі списку аргументів з невизначеним числом.
Макрос va_arg має синтаксис :
void va_arg(va_list ap, type);
Цей макрос повертає значення наступного (чергового) аргументу зі списку. Перед викликом va_arg значення ap повинне бути встановлене викликом va_start або va_arg. Кожний виклик va_arg переводить покажчик на наступний аргумент.
Макрос va_end має синтаксис :
void va_end(va_list ap);
Даний макрос завершує роботу зі списком, звільняючи пам'ять.
#include <stdio.h>
#include <stdarg.h>
void sum(char *msg, ...)
{
int total = 0;
va_list ap;
int arg;
va_start(ap, msg);
while ((arg = va_arg(ap,int)) != 0)
{
total += arg;
}
printf(msg, total);
va_end(ap);
}
int main(void)
{
sum("Сума 1+2+3+4 рівна %d\n", 1,2,3,4,0);
return 0;
}

1.16.5 Рекурсивні функції
Рекурсія - це спосіб організації обчислювального процесу, при якому функція в ході виконання операторів звертається сама до себе.
Функція називається рекурсивною, якщо під час її виконання можливий повторний її виклик безпосередньо (прямий виклик) або шляхом виклику іншої функції, в якій міститься звертання до неї (непрямий виклик).
Прямою (безпосередньою) рекурсією називається рекурсія, при якій всередині тіла деякої функції міститься виклик тієї ж функції.
void fn(int i)
{
/* ... */
fn(i);
/* ... */
}
Непрямою рекурсією називається рекурсія, що здійснює рекурсивний виклик функції шляхом ланцюга викликів інших функцій. При цьому всі функції ланцюга, що здійснюють рекурсію, вважаються також рекурсивними.

void fnA(int i);
void fnB(int i);
void fnC(int i);
void fnA(int i)
{
/* ... */
fnB(i);
/* ... */
}
void fnB(int i)
{
/* ... */
fnC(i);
/* ... */
}
void fnC(int i)
{
/* ... */
fnA(i);
/* ... */
}
Якщо функція викликає сама себе, то в стеку створюється копія значень її параметрів, як і при виклику звичайної функції, після чого управління передається першому оператору функції. При повторному виклику цей процес повторюється.
В якості прикладу розглянемо функцію, що рекурсивно обчислює факторіал. Як відомо, значення факторіала обчислюється за формулою: , причому і . Факторіал також можна обчислити за допомогою простого рекурентного співвідношення . Для ілюстрації рекурсії скористаємося саме цим співвідношенням.
#include<stdio.h>
#include<conio.h>
double fact(int n)
{
if (n<=1) return 1;
return (fact(n-1)*n);
}
void main()
{
int n;
double value;
clrscr();
printf("N=");
scanf("%d",&n);
value=fact(n);
printf("%d! = %.50g",n,value);
getch();
}

Роботу рекурсивної функції fact() розглянемо на прикладі n=6! За рекурентним співвідношенням : . Таким чином, щоб обчислити 6! ми спочатку повинні обчислити 5!. Використовуючи співвідношення, маємо, що , тобто необхідно визначити 4!. Продовжуючи процес, отримаємо :
1).
2).
3).
4).
5).
6).
В кроках 1-5 завершення обчислення кожний раз відкладається, а шостий крок є ключовим. Отримане значення, яке визначається безпосередньо, а не як факторіал іншого числа. Відповідно, ми можемо повернутися від 6-ого кроку до 1-ого, послідовно використовуючи значення :
6).1!=1
5).2!=2
4).3!=6
3). 4!=24
2). 5!=120
1). 6!=720
Важливим для розуміння ідеї рекурсії є те, що в рекурсивних функціях можна виділити дві серії кроків.
Перша серія - це кроки рекурсивного занурення функції в саму себе до тих пір, поки вибраний параметр не досягне граничного значення. Ця важлива вимога завжди повинна виконуватися, щоб функція не створила нескінченну послідовність викликів самої себе. Кількість таких кроків називається глибиною рекурсії.
Друга серія - це кроки рекурсивного виходу до тих пір, поки вибраний параметр не досягне початкового значення. Вона, як правило забезпечує отримання проміжних і кінцевих результатів.

1.16.6 Покажчики на функції
Як згадувалося раніше, на функцію, як і на інший об'єкт мови Сі можна створити покажчик.
float (*func)(float a, float b); /* покажчик на функцію, що приймає два параметри типу float і повертає значення типу float */
Покажчики на функції широко використовується для передачі функцій як параметрів іншим функціям.
За означенням покажчик на функцію містить адресу першого байта або слова виконуваного коду функції. Над покажчиками на функцію заборонені арифметичні операції.
Розглянемо приклад, що містить грубу помилку, спробу працювати з непроініціалізованим покажчиком.
#include<stdio.h>
#include<conio.h>
void main(void)
{
void (*efct)(char *s); /* змінній-покажчику виділена ОП, але efct не містить значення адреси ОП для функції */
efct("Error"); /* груба помилка - спроба працювати з неініціалізованим покажчиком*/
}
Для того, щоб можна було використовувати покажчик на функцію потрібно спочатку присвоїти йому значення адреси пам'яті, де розташована функція, як це зроблено в наступному прикладі.

#include<stdio.h>
#include<conio.h>
void print(char *s)
{
puts(s);
}
void main(void)
{
void (*efct)(char *s);
efct=&print; /* efct=print */
(*efct)("Function Print!"); /* efct("Function Print!"); */
}

Для отримання значення адреси функції необов'язково використовувати операцію &. Тому наступні присвоювання будуть мати однаковий результат :
1). efct=&print;
2). efct=print;

Операція розіменування покажчика на функцію * також є необов'язковою.
1). (*efct)("Function Print!");
2). efct("Function Print!");

Покажчикам на функції можна присвоювати адреси стандартних бібліотечних функцій.
#include<stdio.h>
#include<conio.h>
#include<math.h>
void main(void)
{
double (*fn)(double x);
float y,x=1;
fn=sin;
y=fn(x);
printf("sin(%g)==%g\n",x,y);
fn=cos;
y=fn(x);
printf("cos(%g)==%g\n",x,y);
}

Покажчики на функції можуть також виступати в якості аргументів функцій.

#include<stdio.h>
#include<conio.h>
#include<math.h>
double fn(double (*pfn)(double x),double x)
{
double y=pfn(x);
printf("y==%g\n",y);
return y;
}

double sin_cos(double x)
{
return sin(x)*cos(x);
}

void main(void)
{
fn(sin,1);
fn(&cos,1);
fn(&sin_cos,1);
}


1.16.7 Класи пам'яті
Будь-яка змінна та функція, описана y програмі на Сi, належить до конкретного класу пам'яті, що визначає час її існування та область видимості. Час існування змінної - це період, протягом якого змінна існує в пам'яті, а область видимості (область дії) - це частина програми, в якій змінна може використовуватися.
В мові Сі існує чотири специфікатори класу пам'яті: auto, register, extern і static.

Таблиця 1.17. Область дії та час існування змінних різних класів пам'яті

Клас пам'яті Ключове слово Час існування Область дії
Автоматичний auto тимчасово блок
Регістровий register тимчасово блок
Статичний локальний static постійно блок
Статичний глобальний static постійно файл
Зовнішній extern постійно програма

Клас пам'яті для функції завжди external, якщо перед її описом не стоїть специфікатор static. Клас пам'яті конкретної змінної залежить або від місця розташування її опису, або задається явно за допомогою спеціального специфікатору класу пам'яті, що розташовується перед описом функції. Усі змінні Сі можна віднести до одного з наступних класів пам'яті:

1) auto (автоматична, локальна)
Ключове слово auto використовується рідко. Кожна змінна, описана в тілі функції (в середині блоку), обмеженого фігурними дужками, відноситься до класу пам'яті автоматичних (локальних) змінних:
int anyfunc(void)
{
char item;
........
}
Область дії локальної змінної іtem поширюється лише на блок, в якому вона оголошена. Пам'ять відводиться під змінну динамічно, під час виконання програми при вході y блок, в якому описана відповідна змінна. Локальна змінна тимчасово зберігається в стеку, коли функція починає свою роботу. Після закінчення роботи функції, або при виході з блоку знищує виділену стекову пам'ять, відкидаючи за необхідністю всі збережені змінні, тобто при виході з блоку пам'ять, відведена під усі його автоматичні змінні, автоматично звільняється (звідси й термін - automatic). З цієї причини декілька функцій безконфліктно можуть оголошувати локальні змінні з ідентичними іменами (це найчастіше буває з іменами лічильників циклів, індексів масивів тощо).
Отже, область видимості такої змінної розпочинається з місця її опису і закінчується в кінці блоку, в якому змінна описана. Доступ до таких змінних із зовнішнього блоку неможливий.
Застосування автоматичних змінних в локальних блоках дозволяє наближати опис таких змінних до місця їх розташування. Наступний приклад демонструє опис автоматичних змінних в середині блоку:
#include <stdio.h>
void main()
{
printf("\n Знаходимося в main().");
{
int i;
for(i=10;i>0;i--)
printf("\n%d",i);
printf("\n");
}
}

2) register (регістрова)
Цей специфікатор може використовуватися лише для автоматичних змінних або для формальних параметрів функції. Він вказує компілятору на те, що користувач бажає розмістити змінну не в оперативній пам'яті, а на одному з швидкодіючих регістрів комп'ютеру, від чого програма виконуватиметься більш ефективніше. Звісно, це стосується перш за все саме тих змінних, звертання до яких у функції виконуватиметься найчастіше. На практиці на цей тип змінних накладаються деякі обмеження, що відображають реальні можливості конкретної машини. У випадку надлишкових та недопустимих описів подібний специфікатор просто ігнорується.

3) extern (зовнішня, глобальна)
Будь-яка змінна, описана не в тілі функції (без специфікатору класу пам'яті), по замовчуванню відноситься до extern - змінних (або глобальних змінних). Глобальні змінні продовжують існувати протягом усього життєвого циклу програми. Якщо користувач не вкаже ініційоване значення таким змінним, їм буде присвоєно початкове нульове значення. Найчастіше оголошення таких змінних розташовується безпосередньо перед main():
/*file1.c*/
#include <stdio.h>
int globalvar;
main()
{
/* operators */
}
Будь-які оператори у будь-якій функції файлу file1.c можуть виконувати читання та запис змінної globalvar. Але це ще не все! Виявляється, що глобальні змінні завжди залишаються під контролем завантажувача програми, що здійснює збірку програми із множини obj-файлів. Саме завдяки цьому до зовнішніх змінних можливий доступ з інших файлів. Для того, аби таку змінну можна було б використовувати в іншому файлі, слід задати специфікатор extern:
/*file2.c*/
#include<stdio.h>
void main()
{
extern globalvar;
printf("globalvar : %d", globalvar);
)
Опис extern globalvar; вказує компілятору на те, що ця змінна визначена як зовнішня та її опис знаходиться за межами даного файлу. У даному випадку опис extern розташований в середині функції, тому його дія впливає тільки на дану функцію. Якщо розмістити його ззовні будь-якої функції, то його дія пошириться на весь файл від точки опису.
Цікаво, якщо в середині блоку описана автоматична змінна, ім'я якої співпадає з іменем глобальної змінної, то в середині блоку глобальна змінна маскується локальною. Це означає, що в такому блоці видною буде саме автоматична, тобто локальна змінна.

4) static (статична)
Щоб обмежити доступ до змінних, дозволяючи зберегти їх значення між викриками функцій, слід оголошувати їх статичними. Статична змінна може бути внутрішньою або зовнішньою. Внутрішні статичні змінні локальні по відношенню до окремої функції, подібно автоматичним, проте на відміну від останніх продовжують існувати, а не виникають та знищуються при кожній активації функції. Це означає, що внутрішні статичні змінні є власною, постійною пам'яттю для функції:
int funct(void)
{
static int value=20;
...
}
Компілятор відведе постійну область пам'яті для змінної value та проініціалізує її значення. Ця ініціалізація не повторюватиметься щоразу при активації функції. В подальшому змінна матиме те значення, яке вона отримала по завершенні її останньої роботи. Слід відзначити, що така змінна буде назавжди прихованою для завантажувача даної програми. Тому область дії статичних змінних обмежена функцією, в якій вона була оголошена, а функції не мають доступу до статичних змінних, оголошених в інших функціях.
Зовнішні статичні об'єкти відомі в тому файлі, в якому описані, проте в інших файлах вони невідомі. Таким чином, забезпечується спосіб об'єднання даних та маніпулюючи ними підпрограм таким чином, що інші підпрограми та дані у будь-якому випадку не зможуть конфліктувати з ними.

1.16.8 Додаткові можливості функції main()
Потрібно зауважити, що функція main() може як повертати деяке значення в операційну систему, так і приймати параметри.
тип main(int argc, char* argv[], char *env[]) { /* … */ }
Імена параметрів можуть мати будь-які назви, але прийнято використовувати argc, argv та env. Перший параметр argc містить ціле число аргументів командного рядка, що посилається функції main(); argv - це масив покажчиків на рядки. Для версії ДОС argv[0] містить повний шлях програми, що в даний момент виконується, argv[1] та argv[2] відповідно вказує на перший та другий після імені програми параметри командного рядка, argv[argc-1] вказує на останній аргумент, argv[argc] містить NULL.
env - це масив покажчиків на рядки, причому кожний елемент env[] містить рядок типу ENVVAR=значення. ENVVAR - це ім'я змінної середовища.
Можливо для першого ознайомлення з Сi ця інформація не є обов'язковою, проте не може не зацікавити приклад програми, що демонструє найпростіший шлях використання аргументів, що передаються функції main():
/* Використання аргументів функції main() */
#include <stdio.h>
#include <stdlib.h>
void main(int argc, char *argv[], char *env[])
{
int i;
printf("Значення argc = %d \n\n",argc);
printf("В командному рядку міститься %d параметрів \n",argc);
for (i=0; i<argc; i++)
printf(" argv[%d]: %s\n", i, argv[i]);
printf("Середовище містить наступні рядки:\n");
for (i=0; env[i] != NULL; i++)
printf(" env[%d]: %s\n", i, env[i]);
}
Організуємо виконання програми з командним рядком таким чином:
C:> c:\tc\testargs.exe 1_st_arg "2_arg " 3 4 "dummy" stop!
В результаті роботи програми ви отримаєте приблизно наступне:
Значення argc = 7
В командному рядку міститься 7 параметрів
argv[0]: c:\tc\testargs.exe
argv[1]: 1_st_arg
argv[2]: 2_arg
argv[3]: 3
argv[4]: 4
argv[5]: dummy
argv[6]: stop!
Середовище містить наступні рядки:
env[0]: COMSPEC=C:\COMMAND.COM
env[1]: PROMPT=$p $g
env[2]: PATH=C:\SPRINT;C:\DOS;C:\TC
Максимальна загальна довжина командного рядка, включаючи пробіли та ім'я самої програми, не може перевищувати 128 символів, що є DOS-обмеженням.