36116

Конспект лекцій по Інформатиці

Конспект

Информатика, кибернетика и программирование

Для позначення програмних засобів під якими розуміється сукупність всіх програм використовуваних компютерами і область діяльності по їхньому створенню й застосуванню використається слово Software буквально мякі вироби що підкреслює рівнозначність самої машини й програмного забезпечення а також здатність програмного забезпечення модифікуватися пристосовуватися й розвиватися. Для позначення частини інформатики повязаної з розробкою алгоритмів і вивченням методів і прийомів їхньої побудови застосовують термін Braіnware англ. А...

Украинкский

2013-09-21

932.5 KB

13 чел.

Конспект лекцій по Інформатиці

Термін "інформатика" (франц. іnformatіque) походить від французьких слів іnformatіon (інформація) і automatіque (автоматика) і дослівно означає "інформаційна автоматика".

Широко розповсюджений також англомовний варіант цього терміна - "Сomputer scіence", що означає буквально "комп'ютерна наука".

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

В 1978 році міжнародний науковий конгрес офіційно закріпив за поняттям "інформатика" області, пов'язані з розробкою, створенням, використанням і матеріально-технічним обслуговуванням систем обробки інформації, включаючи комп'ютери і їхнє програмне забезпечення, а також організаційні, комерційні, адміністративні й соціально-політичні аспекти комп'ютеризації - масового впровадження комп'ютерної техніки в усі області життя людей.

Таким чином, інформатика базується на комп'ютерній техніці й немислима без її.

Инфоpматика - комплексна наукова дисципліна з найширшим діапазоном застосування. Її пріоритетні напрямки:

" pазpаботка обчислювальних систем і пpогpаммного забезпечення;

" теоpия инфоpмации, що вивчає процеси, пов'язані з передачею, прийомом, перетворенням і зберіганням інформації;

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

" методи штучного інтелекту, що моделюють методи логічного й аналітичного мислення в інтелектуальній діяльності людини (логічний висновок, навчання, розуміння мови, візуальне сприйняття, ігри й ін.);

" системний аналіз, що вивчає методологічні засоби, використовувані для підготовки й обґрунтування рішень по складних проблемах різного характеру;

" биоинформатика, що вивчає інформаційні процеси в біологічних системах;

" соціальна інформатика, що вивчає процеси інформатизації суспільства;

" методи машинної графіки, анімації, засобу мультимедиа;

" телекомунікаційні системи й мережі, у тому числі, глобальні комп'ютерні мережі, що поєднують все людство в єдине інформаційне співтовариство;

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

Російський академік А.А. Дородницин виділяє в інформатиці три нерозривно й істотно зв'язані частини - технічні засоби, програмні й алгоритмічні.

Технічні засоби, або апаратури комп'ютерів, в англійській мові позначаються словом Hardware, що буквально переводиться як "тверді вироби".

Для позначення програмних засобів, під якими розуміється сукупність всіх програм, використовуваних комп'ютерами, і область діяльності по їхньому створенню й застосуванню, використається слово Software (буквально - "м'які вироби"), що підкреслює рівнозначність самої машини й програмного забезпечення, а також здатність програмного забезпечення модифікуватися, пристосовуватися й розвиватися.

Програмуванню завдання завжди передує розробка способу її рішення у вигляді послідовності дій, що ведуть від вихідних даних до шуканого результату, іншими словами, розробка алгоритму рішення завдання. Для позначення частини інформатики, пов'язаної з розробкою алгоритмів і вивченням методів і прийомів їхньої побудови, застосовують термін Braіnware (англ. braіn - інтелект).

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

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

……………….

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

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

" у техніку під інформацією розуміють повідомлення, передані у формі знаків або сигналів;

" у кібернетику під інформацією розуміє ту частину знань, що використається для орієнтування, активної дії, керування, тобто з метою збереження, удосконалювання, розвитку системи (Н. Вінер).

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

Приведемо ще кілька визначень:

" Інформація - це відомості про об'єкти і явища навколишнього середовища, їхніх параметрах, властивостях і стані, які зменшують наявну про їх ступінь невизначеності, неповноти знань (Н.В. Макарова);

" Інформація - це заперечення ентропії (Леон Бриллюэн);

" Інформація - це міра складності структур (Моль);

" Інформація - це відбита розмаїтість (Урсул);

" Інформація - це зміст процесу відбиття (Тузів);

" Інформація - це ймовірність вибору (Яглом).

Сучасне наукове подання про інформації дуже точно сформулював   Норберт Вінер, "батько" кібернетики. А саме:

Інформація - це позначення змісту, отриманого із зовнішнього миру в процесі нашого пристосування до нього й пристосування до нього наших почуттів

Люди обмінюються інформацією у формі повідомлень. Повідомлення - це форма подання інформації у вигляді мови, текстів, жестів, поглядів, зображень, цифрових даних, графіків, таблиць і т.п.

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

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

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

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

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

1.3. У якому виді існує інформація?

Інформація може існувати у вигляді:

" текстів, малюнків, креслень, фотографій;

" світлових або звукових сигналів;

" радіохвиль;

" електричних і нервових імпульсів;

" магнітних записів;

" жестів і міміки;

" заходів і смакових відчуттів;

" хромосом, за допомогою яких передаються в спадщину ознаки й властивості організмів і т.д.

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

1.4. Як передається інформація?

Інформація передається у формі повідомлень від деякого джерела інформації до її приймача за допомогою каналу зв'язку між ними. Джерело посилає передане повідомлення, що кодується в переданий сигнал. Цей сигнал посилає по каналі зв'язку. У результаті в приймачі з'являється прийнятий сигнал, що декодируется й стає прийнятим повідомленням.

 Приклади:

1. Cообщение, що містить інформацію про прогноз погоди, передається приймачу (телеглядачеві) від джерела - фахівця-метеоролога за допомогою каналу зв'язку - телевізійних передавальних апаратур і телевізора.

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

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

1.5. Як виміряється кількість інформації?

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

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

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

 

 

 

       Підходи до визначення кількості інформації.   Формули Хартли й Шеннона.

Американський інженер Р. Хартли в 1928 р. процес одержання інформації розглядав як вибір одного повідомлення з кінцевого наперед заданої безлічі з N равновероятных з, а кількість інформації Й, що втримується в обраному повідомленні, визначав як двійковий логарифм N.

           Формула Хартли:   І = log2N

Допустимо, потрібно вгадати одне число з набору чисел від одиниці до ста. По формулі Хартли можна обчислити, яке кількість інформації для цього потрібно: І = log2100   6,644. Таким чином, із про вірно вгадане число містить кількість інформації, приблизно рівне 6,644 одиниці інформації.

Приведемо інші приклади равновероятных повідомлень:

1. при киданні монети: "випала решка", "випав орел";

2. на сторінці книги: "кількість букв парне", "кількість букв непарне".

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

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

                     Формула Шеннона: І = - ( p1log2 p1 + p2 log2 p2 + . . . + p log2 p),

де pі - імовірність того, що саме і-і повідомлення виділене в наборі з N з.

Легко помітити, що якщо ймовірності p1, ..., p рівні, те кожна з них дорівнює 1 / N, і формула Шеннона перетворюється у формулу Хартли.

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

Як  одиниця інформації Клод Шеннон запропонував прийняти  один  біт    (англ. bіt - bіnary dіgіt - двійкова цифра).

Біт у теорії інформації - кількість інформації, необхідне для розрізнення двох равновероятных повідомлень   (типу "орел"-"решка", "чіт"-"непара" і т.п.).

В обчислювальній техніці бітом називають найменшу "порцію" пам'яті комп'ютера, необхідну для зберігання одного із двох знаків "0" й "1", використовуваних для внутрімашинного подання даних і команд.

Біт - занадто дрібна одиниця виміру. На практиці частіше застосовується більша  одиниця -  байт,  рівним  восьми биткам. Саме вісім битов потрібно для того, щоб закодувати кожної з 256 символів алфавіту клавіатури комп'ютера (256=28).

Широко використаються також ще більш великі похідні одиниці інформації:

" 1 Кілобайт (Кбайт) = 1024 байт = 210 байт,

" 1 Мегабайт (Мбайт) = 1024 Кбайт = 220 байт,

" 1 Гігабайт (Гбайт) = 1024 Мбайт = 230 байт.

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

" 1 Терабайт (Тбайт) = 1024 Гбайт = 240 байт,

" 1 Петабайт (Пбайт) = 1024 Тбайт = 250 байт.

За одиницю інформації можна було б вибрати кількість інформації, необхідне для розрізнення, наприклад, десяти равновероятных повідомлень. Це буде не двійкова (біт), а десяткова (дит) одиниця інформації.

Що можна робити з інформацією?

Інформацію можна:

" створювати;

" передавати;

" сприймати;

" иcпользовать;

" запам'ятовувати;

" приймати;

" копіювати;

" формалізувати;

" поширювати;

" перетворювати;

" комбінувати;

" обробляти;

" ділити на частині;

" спрощувати;

" збирати;

" зберігати;

" шукати;

" вимірювати;

" руйнувати;

" й ін. "

Всі ці процеси, пов'язані з певними операціями над інформацією, називаються інформаційними процесами.

1.7. Якими властивостями володіє інформація?

Властивості інформації:

вірогідність;

повнота;

цінність;

своєчасність;

зрозумілість;

доступність;

стислість;

й ін.

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

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

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

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

Цінність інформації залежить від того, наскільки вона важлива для рішення завдання, а також від того, наскільки надалі  вона знайде застосування в яких-небудь видах діяльності людини.

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

Якщо коштовна й своєчасна інформація виражена незрозумілим образом, вона може стати марної.

Інформація стає зрозумілої, якщо вона виражена мовою, на якому говорять ті, кому призначена ця інформація.

Інформація повинна підносити в доступній (за рівнем сприйняття) формі. Тому ті самі   питання по різному викладаються в шкільних підручниках і наукових виданнях.

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

1.8. Що таке обробка інформації?

Обробка інформації - одержання одних інформаційних об'єктів з інших інформаційних об'єктів шляхом виконання деяких алгоритмів

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

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

Комп'ютери обробляють інформацію шляхом виконання деяких алгоритмів.

Живі організми й рослини обробляють інформацію за допомогою своїх органів і систем.

1.9. Що таке інформаційні ресурси й інформаційні технології?

Інформаційні ресурси - це ідеї людства й вказівки по їхній реалізації, накопичені у формі, що дозволяє їхнє відтворення.

Це книги, статті, патенти, дисертації, науково-дослідна й дослідно-конструкторська документація, технічні переклади, дані про передовий виробничий досвід й ін. [42].

Інформаційні ресурси (на відміну від всіх інших видів ресурсів - трудових, енергетичних, мінеральних і т.д.) тим швидше ростуть, чим більше їх витрачають.

Інформаційна технологія - це сукупність методів і пристроїв, використовуваних людьми для обробки інформації.

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

У цей час термін "інформаційна технологія" уживається у зв'язку з використанням комп'ютерів для обробки інформації. Інформаційні технології охоплюють всю обчислювальну техніку й техніку з й, почасти, - побутову електроніку, телебачення й радіомовлення.

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

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

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

1.10. Що розуміють під інформатизацією суспільства?

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

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

1.11. Питання для самоконтролю

1. Що означає термін "інформатика" й яке його походження?

2. Які області знань й адміністративно-господарської діяльності офіційно закріплені за поняттям "інформатика" з 1978 року?

3. Які сфери людської діяльності й у якому ступені зачіпає інформатика?

4. Назвіть основні складові частини інформатики й основні напрямки її застосування.

5. Що мається на увазі під поняттям "інформація" у побутовому, природничо-науковому й технічному змістах?

6. Приведіть приклади знання фактів і знання правил. Назвіть нові факти й нові правила, які Ви довідалися за сьогоднішній день.

7. Від кого (або чого) людина приймає інформацію? Кому передає інформацію?

8. Де і як людина зберігає інформацію?

9. Що необхідно додати в систему "джерело інформації - приймач інформації", щоб здійснювати передачу повідомлень?

10. Які типи дій виконує людина з інформацією?

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

а) створюється; д) копіюється; і) передається;

б) обробляється; е) сприймається; к) руйнується;

в) запам'ятовується; ж) виміряється; л) шукається;

г) ділиться на частині; з) приймається; м) спрощується.

12. Приведіть приклади обробки інформації людиною. Що є результатами цієї обробки?

13. Приведіть приклади інформації:

а) достовірної й недостовірної;

б) повної й неповної;

в) коштовної й малоцінної;

г) своєчасної й несвоєчасної;

д) зрозумілої й незрозумілої;

е) доступної й недоступної для засвоєння;

ж) короткої й великої.

14. Назвіть системи збору й обробки інформації в тілі людини.

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

16. Від чого залежить інформативність повідомлення, прийнятого людиною?

17. Чому кількість інформації в повідомленні зручніше оцінювати не по ступені збільшення знання про об'єкт, а по ступені зменшення невизначеності наших знань про нього?

18. Як визначається одиниця виміру кількості інформації?

19. У яких випадках можна обчислити кількість інформації, що втримується в повідомленні?

20. Що визначає термін "біт" у теорії інформації й в обчислювальній техніці?

21. Наведіть приклади повідомлень, інформативність яких можна однозначно визначити.

22. Наведіть приклади повідомлень, що містять один (два, три) біти інформації.

1.1 Історія виникнення 

       Трохи про історію виникнення мов програмування, та мови Сі зокрема. У 1949 році у Філадельфії (США) під керівництвом Джона Мочлі був створений "Стислий код" - перший примітивний інтерпретатор мови програмування. У 1951 році у фірмі Remington Rand американська програмістка Грейс Хоппер розробила першу транслюючи програму, що називалася компілятором (compiler - компоновщик). У 1957 році у штаб-квартирі фірми IBM на Медісон-авеню у Нью-Йорку з'явилася перша повна мова Фортран (FORmula TRANslation - трансляція формул). Групою розробників керував тоді відомий 30-річний математик Джон Бекус. Фортран - це перша із "дійсних" мов високого рівня.
       Далі, у 1972 році 31-літній фахівець із системного програмування фірми Bell Labs Денніс Рітчі розробив мову програмування Сі. У 1984 році французький математик та саксофоніст Филип Кан засновує фірму Borland International. Далі з'явився діалект мови Сі фірми Borland.
       На початку Сі була розроблена як мова для програмування в операційній системі Unix. Незабаром він став поширюватися для програмістів-практиків. Наприкінці 70-х були розроблені транслятори Сі для мікроЕОМ операційної системи СР/M. Після появи IBM PC стали з'являтися і компілятори мови Сі (для таких комп'ютерів їх зараз декілька десятків). У 1983 р. американський Інститут Стандартів (ANSI) сформував Технічний Комітет X3J11 для створення стандарту мови Сі. На сьогодні мова Сі++, що з'явилася як послідовник Сі, підпорядковується більшості вимог стандарту.
       За своїм змістом Сі, перш за все, є мовою функцій. Програмування на Сі здійснюється шляхом опису функцій і звертання до бібліотек (бібліотечних функцій). Більшість функцій повертають деякі значення, що можуть використовуватися в інших операторах.
Серед переваг мови Сі потрібно відзначити основні:
    • універсальність (використовується майже на всіх існуючих ЕОМ);
    • компактність та універсальність коду;
    • швидкість виконання програм;
    • гнучкість мови;
    • висока структурованість.

1.2 Елементи мови Сі

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

1.2.1 Алфавіт
      Алфавіт мови Сі включає :
   • великі та малі літери латинської абетки;
   • арабські цифри;
   • пробільні символи : пробіл, символи табуляції, символ переходу на наступний рядок тощо;
   • символи , . ; : ? ' ! | / \ ~ ( ) [ ] { } < > # % ^ & - + * =

1.2.2 Ідентифікатори
     
 Ідентифікатори використовуються для іменування різних об'єктів : змінних, констант, міток, функцій тощо. При записі ідентифікаторів можуть використовуватися великі та малі літери латинської абетки, арабські цифри та символ підкреслення. Ідентифікатор не може починатися з цифри і не може містити пробілів.
      Компілятор мови Сі розглядає літери верхнього та нижнього регістрів як різні символи. Тому можна створювати ідентифікатори, які співпадають орфографічно, але відрізняються регістром літер. Наприклад, кожний з наступних ідентифікаторів унікальний :

Sum sum sUm SUM sUM

      Слід також пам'ятати, що ідентифікатори не повинні співпадати з ключовими словами.

1.2.3 Константи
      Константами називають сталі величини, тобто такі, які в процесі виконання програми не змінюються. В мові Сі існує чотири типи констант : цілі, дійсні, рядкові та символьні.
     
 1. Цілі константи можуть бути десятковими, вісімковими або шістнадцятковими.
Десяткова константа - послідовність десяткових цифр (від 0 до 9), яка починається не з нуля якщо це число не нуль. Приклади десяткових констант : 10, 132, 1024.
Вісімкові константи починаються з символу 0, після якого розміщуються вісімкові цифри (від 0 до 7). Наприклад : 023. Запис константи вигляду 08 буде сприйматися компілятором як помилка, так як 8 не є вісімковою цифрою.
Шістнадцяткові константи починаються з символів 0х або 0Х, після яких розміщуються шістнадцяткові цифри (від 0 до F, можна записувати їх у верхньому чи нижньому регістрах). Наприклад : 0ХF123.
      
2. Дійсні константи складаються з цілої частини, десяткової крапки, дробової частини, символу експоненти (e чи E) та показника степеня. Дійсні константи мають наступний формат представлення :
      [ ціла_частина ][ . дробова_частина ][ Е [-] степінь ]
У записі константи можуть бути опущені ціла чи дробова частини (але не обидві разом), десяткова крапка з дробовою частиною чи символ E (e) з показником степеня (але не разом). Приклади дійсних констант : 2.2 , 220е-2, 22.Е-1, .22Е1.
Якщо потрібно сформувати від'ємну цілу або дійсну константу, то перед константою необхідно поставити знак унарного мінуса.
     
 3. Символьні константи. Символьна константа - це один або декілька символів, які заключені в апострофи. Якщо константа складається з одного символу, вона займає в пам'яті 1 байт (тип char). Двосимвольні константи займають в пам'яті відповідно 2 байти (тип int).
Послідовності символів, які починаються з символу \ (обернений слеш) називаються керуючими або escape-послідовностями (таблиця 1.1).

Таблиця 1.1. Escape-послідовності

Спеціальний символ

Шістнадцятковий код

Значення

\a

07

звуковий сигнал

\b

08

повернення на 1 символ

\f

0C

переведення сторінки

\n

0A

перехід на наступний рядок

\r

0D

повернення каретки

\t

09

горизонтальна табуляція

\v

0B

вертикальна табуляція

\\

5C

символ \

\'

27

символ '

\"

22

символ "

\?

3F

символ ?

\0

00

нульовий символ

\0ddd

-

вісімковий код символу

\0xddd

ddd

шістнадцятковий код символу


     
 4. Рядкові константи записуються як послідовності символів, заключених в подвійні лапки.
"Це рядковий літерал!\n"
      Для формування рядкових констант, які займають декілька рядків тексту програми використовується символ \ (обернений слеш):
"Довгі рядки можна розбивати на \
частини"
      Загальна форма визначення іменованої константи має вигляд :
const тип ім'я = значення ;
     
 Модифікатор const попереджує будь-які присвоювання даному об'єкту, а також інші дії, що можуть вплинути на зміну значення. Наприклад:
const float pi = 3.14l5926;
const maxint = 32767;
char *const str="Hello,P...!"; /* покажчик-константа */
char const *str2= "Hello!"; /* покажчик на константу */

     
 Використання одного лише модифікатору const еквівалентно const int.

1.2.4 Коментарі
      Текст на Сі, що міститься у дужках /* та */ ігноруватиметься компілятором, тобто вважатиметься коментарем до програми. Такі коментарі можуть розміщуватися в будь-якому місці програми.
     
 Коментарі здебільшого використовуються для "документування програм" та під час їх відлагодження. В програму бажано вміщувати текст, що хоч якось пояснює її роботу та призначення. Проте не слід надто зловживати коментарями, а використовувати більш розумні форми найменування змінних, констант, функцій тощо. Якщо, наприклад, функція матиме назву add_matrix, очевидно не зовсім раціональним буде включення у програму після її заголовної частини коментар про те, що:
/*функція обчислює cуму матриць */
     
 У цьому випадку ім'я функції пояснює її призначення. У більш сучасних версіях Сі широко застосовується так званий угорський запис імен, коли ім'я змінної містить в собі інформацію про її призначення і тип.

1.2.5 Ключові слова
      Ключові слова - це зарезервовані ідентифікатори, які мають спеціальне значення для компілятора. Їх використання суворо регламентоване. Імена змінних, констант, міток, типів тощо не можуть співпадати з ключовими словами.
     
 Наводимо перелік ключових слів мови Сі :

auto

continue

float

interrupt

short

unsigned

asm

default

for

long

signed

void

break

do

far

near

sizeof

volatile

case

double

goto

pascal

static

while

cdecl

else

huge

switch

struct

 

char

enum

if

register

typedef

 

const

extern

int

return

union

 

1.3 Структура програми. Базові типи даних

1.3.1 Функція main() : з цього все починається
       Усі програми, написані на мові Сі, повинні містити в собі хоча б одну функцію. Функція main() - вхідна точка будь-якої програмної системи, причому немає різниці, де її розміщувати. Але потрібно пам'ятати наступне: якщо вона буде відсутня, завантажувач не зможе зібрати програму, про що буде виведене відповідне попередження. Перший оператор програми повинен розміщуватися саме в цій функції.
Мінімальна програма на мові Сі має вигляд:
main()
{
    return 0;
}

      Функція починається з імені. В даному прикладі вона не має параметрів, тому за її ім'ям розташовуються порожні круглі дужки (). Далі обидві фігурні дужки {...} позначають блок або складений оператор, з яким ми працюватимемо, як з єдиним цілим. У Паскалі аналогічний зміст мають операторні дужки begin ... end.
      Мінімальна програма має лише один оператор - оператор повернення значення return. Він завершує виконання програми та повертає в нашому випадку деяке ціле значення (ненульове значення свідчить про помилки в програмі, нульове про успішне її завершення). Виконання навіть цієї найпростішої програми, як і решти багатьох, проходить у декілька етапів (рис 1.1.).

код запуску функція main() код завершення

Рис. 1.1. Етапи виконання програми на мові Сі

1.3.2 Базові типи даних
      Будь-яка програма передбачає виконання певних операцій з даними. Від їх типу залежить, яким чином будуть проводитися ці операції, зрештою, буде визначено, як реалізовуватиметься алгоритм.
      Що таке тип даних? Сформулювати це поняття можна так : множина значень плюс перелік дій або операцій, які можна виконати над кожною змінною даного типу. Вважається, що змінна або вираз належить до конкретного типу, якщо його значення лежить в області допустимих значень цього типу.
Арифметичні типи даних об'єднують цілі та дійсні, цілі у свою чергу - декілька різновидів цілих та символьних типів даних. Скалярні типи включають в себе арифметичні типи, покажчики та перелічувані типи. Агрегатні або структуровані типи містять в собі масиви, структури та файли. Нарешті функції представляють дещо особливий клас, який слід розглядати окремо.
      Базові типи даних Сі можна перерахувати у наступній послідовності:
1. char - символ
      Тип може використовуватися для зберігання літери, цифри або іншого символу з множини символів ASCII. Значенням об'єкта типу char є код символу. Тип char інтерпретується як однобайтове ціле з областю значень від -128 до 127.
2. int - ціле
      Цілі числа у діапазоні від -32768 до 32767. В операційних середовищах Windows та Windows NT використовуються 32-розрядні цілі, що дозволяє розширити діапазон їх значень від -2147483648 до 2147483647. Як різновиди цілих чисел, у деяких версіях компіляторів існують short - коротке ціле (слово) та long (4 байти) - довге ціле. Хоча синтаксис мови не залежить від ОС, розмірність цих типів може коливатися від конкретної реалізації. Гарантовано лише, що співвідношення розмірності є наступним: short <= int <=long.
3. float - число з плаваючою комою одинарної точності
      Тип призначений для зберігання дійсних чисел. Може представляти числа як у фіксованому форматі (наприклад число пі - 3.14159), так і в експоненціальній формі - 3.4Е+8.
4. double - число з плаваючою комою подвійної точності
      Має значно більший діапазон значень, порівняно з типом float: ±(1.7 10- 308 ... 1.7 10308).
      У мові Сі, на відміну від Паскаля, використовується префіксний запис оголошення. При цьому на початку вказується тип змінної, а потім її ім'я. Змінні повинні бути описаними до того моменту, як вони будуть використовуватися у програмі. Ніяких додаткових ключових слів при цьому не пишуть. Наприклад:
int name;
float var, var1;
double temp;
char ch;
long height;

      Змінні можна ініціалізувати (присвоювати їм початкові значення) безпосередньо у місці їх опису:
int height = 33 ;
float income = 2834.12 ;
char val = 12 ;

      Для виведення інформації на екран використаємо функцію printf() (детально про операції введення-виведення значень змінних йтиметься у розділі 1.3.4. "Функції введення та виведення"):
printf("Вік Олега-%d.Його прибуток %.2f",age,income);
      Крім того, цілі типи char, short, int, long можуть використовуватися з модифікаторами signed (із знаком) та unsigned (без знаку). Цілі без знаку (unsigned) не можуть набувати від'ємних значень, на відміну від знакових цілих (signed). За рахунок цього дещо розширюється діапазон можливих додатних значень типу (таблиця 1.2.).

Таблиця 1.2. Діапазони значень простих типів даних

Тип

Діапазон значень

Розмір (байт)

char

-128 … 127

1

short

 

2

int

-32768 ... 32767

2 або 4

long

-2,147,483,648 ... 2,147,483,647

4

unsigned char

0 ... 255

1

unsigned short

0 … 65535

2

unsigned

 

2 або 4

unsigned long

0 ... 4,294,967,295

4

float

±(3.4 10-38 ... 3.4 1038)

4

double

±(1.7 10-308 ... 1.7 10308)

8

long double

±(3.4 10-4932 ... 3.4 104932)

10

1.3.3 Перетворення типу
      Згадаємо, що компілятор Паскаля виконує автоматичне перетворення типів даних, особливо в математичних виразах, коли найчастіше цілочисельний тип перетворюється у тип з плаваючою комою. Цей стиль підтримує і Сі, причому значення типу char та int в арифметичних виразах змішуються: кожний з таких символів автоматично перетворюється в ціле. Взагалі, якщо операнди мають різні типи, перед тим, як виконати операцію, молодший тип "підтягується" до старшого. Результат - старшого типу. Отже,
    • char та short перетворюються в int;
    • float перетворюється в double;
    • якщо один з операндів long double, то і другий перетворюється в long double;
    • якщо один з операндів long, тоді другий перетворюється відповідно до того ж типу, і результат буде long;
    • якщо один з операндів unsigned, тоді другий перетворюється відповідно до того ж типу, і результат буде unsigned.
Приклад
:
double ft, sd;
unsigned char ch;
unsigned long in;
int i;
/* ...
*/
sd = ft*(i+ch/in);

      При виконанні оператора присвоювання в даному прикладі правила перетворення типів будуть використані наступним чином. Операнд ch перетворюється до unsigned int. Після цього він перетворюється до типу unsigned long. За цим же принципом і перетворюється до unsigned long і результат операції, що розміщена в круглих дужках буде мати тип unsigned long. Потім він перетворюється до типу double і результат всього виразу буде мати тип double.
      Взагалі, тип результату кожної арифметичної операції виразу є тип того операнду, який має у відповідності більш високий тип приведення.
Але, окрім цього в Сі, з'являється можливість і примусового перетворення типу, щоб дозволити явно конвертувати (перетворювати) значення одного типу даних в інший. Загальний синтаксис перетворення типу має два варіанти :
1). (новий_тип) вираз ;
2). новий_тип (вираз) ;
      Обидва варіанти перетворення виглядають так:
сhar letter = 'a';
int nasc = int (letter);
long iasc = (long) letter;

1.3.4 Функції введення та виведення
      Що б там не було, але реальні програми важко уявити без використання операцій введення та виведення.
В мові Сі на стандартні потоки введення-виведення (в більшості випадків - клавіатура та монітор) завжди вказують імена stdin та stdout. Обробку цих потоків здійснюють функції, визначені в заголовочному файлі stdio.h.
      Розглянемо основні функції введення-виведення.
      Функція getchar() зчитує і повертає черговий символ з послідовності символів вхідного потоку. Якщо цю послідовність вичерпано, то функція getchar() повертає значення -1 (цьому значенню відповідає константа EOF).
      Функція putchar(аргумент), де аргументом є вираз цілого типу, виводить у стандартний вихідний потік значення аргументу, перетворене до типу char.
Приклад :
#include<stdio.h>
void main()
{
  char ch;
  ch=getchar();
  putchar(ch);
}

      Для введення та виведення більш складної інформації використовуються функції scanf() та printf().
Функція printf() призначена для виведення інформації за заданим форматом. Синтаксис функції printf():
printf("Рядок формату"[, аргумент1[, аргумент2, [...]]]);
      Першим параметром даної функції є "рядок формату", який задає форму виведення інформації. Далі можуть розташовуватися вирази арифметичних типів або рядки (в списку аргументів вони відокремлюються комами). Функція printf() перетворює значення аргументів до вигляду, поданого у рядку формату, "збирає" перетворені значення в цей рядок і виводить одержану послідовність символів у стандартний потік виведення.
Рядок формату складається з об'єктів двох типів : звичайних символів, які з рядка копіюються в потік виведення, та специфікацій перетворення. Кількість специфікацій у рядку формату повинна дорівнювати кількості аргументів.
Приклад :
#include<stdio.h>
void main()
{
  int a=10,b=20,c=30;
  printf(" a==%d \n b==%d \n c==%d \n",a,b,c);
}

      Специфікації перетворення для функції printf():
%d - десяткове ціле;
%i - десяткове ціле;
%o - вісімкове ціле без знаку;
%u - десяткове ціле без знаку (unsigned)
%x - шістнадцяткове ціле без знаку;
%f - представлення величин float та double з фіксованою точкою;
%e або %Е - експоненціальний формат представлення дійсних величин;
%g - представлення дійсних величин як f або Е в залежності від значень;
%c - один символ (char);
%s - рядок символів;
%p - покажчик
%n - покажчик
%ld - long (в десятковому вигляді);
%lo - long (у вісімковому вигляді);
%p - виведення покажчика в шістнадцятковій формі;
%lu - unsigned long.
      Можна дещо розширити основне визначення специфікації перетворення, помістивши модифікатори між знаком % і символами, які визначають тип перетворення (таблиця 1.3.).

Таблиця 1.3. Значення основних модифікаторів рядка формату

Модифікатор

Значення

-

Аргумент буде друкуватися починаючи з лівої позиції поля заданої ширини. Звичайно друк аргументу закінчується в самій правій позиції поля. Приклад : %-10d

Рядок цифр

Задає мінімальну ширину поля. Поле буде автоматично збільшуватися, якщо число або рядок не буде вміщуватися у полі. Приклад : %4d

Цифри.цифри

Визначає точність : для типів даних з плаваючою комою - число символів, що друкуються зліва від десяткової коми; для символьних рядків - максимальну кількість символів, що можуть бути надруковані. Приклад : %4.2f

      Розглянемо декілька прикладів:
    
 Приклад 1 :
#include <stdio.h>
main()
{
    printf("/%d/\n",336);
    printf("/%2d/\n",336);
    printf("/%10d/\n",336);
    printf("/%-10d/\n",336);
};

     Результат виконання програми буде виглядати так :
/336/
/336/
/ 336/
/336 /

     Приклад 2 :
#include <stdio.h>
main()
{
    printf("/%f/\n",1234.56);
    printf("/%e/\n",1234.56);
    printf("/%4.2f/\n",1234.56);
    printf("/%3.1f/\n",1234.56);
    printf("/%10.3f/\n",1234.56);
    printf("/%10.3e/\n",1234.56);
}

     На цей раз результат виконання програми буде виглядати так :
/1234.560000/
/1.234560e+03/
/1234.56/
/1234.6/
/ 1234.560/
/ 1.235e+03/

     Для введення інформації зі стандартного потоку введення використовується функція scanf().
Синтаксис :
    
scanf("Рядок формату",&аргумент1[,&аргрумент2[, ...]]);
     Так, як і для функції printf(), для функції scanf() вказується рядок формату і список аргументів. Суттєва відмінність у синтаксисі цих двох функцій полягає в особливостях даного списку аргументів. Функція printf() використовує імена змінних, констант та вирази, в той час, як для функції scanf () вказується тільки покажчики на змінні.
     Поширеною помилкою використання scanf() у початківців є звертання: scanf("%d",n) замість scanf("%d",&n). Параметри цієї функції обов'язково повинні бути покажчиками!
Функція scanf() використовує практично той же набір символів специфікації, що і функція printf().
#include <stdio.h>
main()
{
    int a,b,c;
    printf("A=");
    scanf("%d",&a);
    printf("B=");
    scanf("%d",&b);
    c=a+b;
    printf("A+B=%d",c);
}

     Більшість реалізацій мови Сі дозволяють пов'язувати імена stdin та stdout не тільки з клавіатурою та екраном, а й із зовнішніми файлами. Для цього в рядку виклику Сі програми необхідно вказати імена цих файлів. Якщо перед ім'ям файла введення поставити знак <, то даний файл буде пов'язаний з потоком введення.
    
prog < file.in
     В даному прикладі інформація читається з файла file.in поточного каталогу, а не з клавіатури, тобто цей файл стає стандартним файлом введення, на який вказує stdin.
    
prog > file.out
     А при такому виклику програми інформація виводиться не на екран, а у файл file.out.
Якщо необхідно читати інформацію з одного файла, а результати записувати у інший одразу, виклик програми буде мати вигляд :
    
prog < file.in > file.out

1.3.5 Директиви включення
     У багатьох програмах ми зустрічаємо використання так званих директив включення файлів. Синтаксис використання їх у програмі наступний :
#include <file_1>
#include <file_2>
...
#include <file_n>

     По-перше, слід звернути увагу на те, що на відміну від більшості операторів, ця директива не завершується крапкою з комою. Використання таких директив призводить до того, що препроцесор підставляє на місце цих директив тексти файлів у відповідності з тими, що перелічені у дужках < ... > . Якщо ім'я файла міститься у таких дужках, то пошук файлу буде проводитися у спеціальному каталозі файлів для включення (як, правило, каталог INCLUDE, усі файли з розширенням *.h - header-файли). Якщо даний файл у цьому каталозі буде відсутнім, то препроцесор видасть відповідне повідомлення про помилку, яка є досить типовою для початківців при роботі в інтегрованому середовищі:
    
< Unable to open include file 'file.h'>
    <Неможливо відкрити файл включення ' file.h'>
     У цьому випадку достатньо перевірити не тільки наявність header-файлу у відповідній директорії, але й впевнитися у тому, що опція Options\Directories дійсно відповідає правильному диску та спеціальному каталогу, де розташовані файли включення.
Існує і другий спосіб - вказівка імені файла у подвійних лапках - "file_n.txt ", так найчастіше підключають програмісти власностворені файли включення. Тоді пошук файлу ведеться у поточній директорії активного диску, якщо ж пошук буде невдалим, система закінчує його у спеціальному каталозі для header-файлів, як і у загальному випадку. Найбільш частим у початківців є включення файлу "stdio.h":
#include <stdio.h>
main()
{
    printf("Hello ! ...\n");
    return 0;
}

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

1.4 Основні операції

      Операції подібні вбудованим функціям мови програмування. Вони застосовуються до виразів (операндів). Більшість операцій мають два операнди, один з яких розташовується перед знаком операції, а інший - після. Наприклад, два операнди має операція додавання А+В. Операції, які мають два операнди називаються бінарними. Існують і унарні операції, тобто такі, які мають лише один операнд. Наприклад, запис -А означає застосування до операнду А операції унарного мінуса. А три операнди має лише одна операція - ?:. Це єдина тернарна операція мови Сі.
У складних виразах послідовність виконання операцій визначається дужками, старшинством операцій, а при однаковому старшинстві - асоціативністю.
За призначенням операції можна поділити на :
   • арифметичні операції;
   • операції присвоювання;
   • операції відношення;
   • логічні операції;
   • порозрядні операції;
   • операція обчислення розміру sizeof();
   • умовна операція ?;
   • операція слідування (кома).

1.4.1 Арифметичні операції
      До арифметичних операцій належать відомі всім бінарні операції додавання, віднімання, множення, ділення та знаходження залишку від ділення (таблиця 1.4.).

 

Таблиця 1.5. Бінарні арифметичні операції 

Операція

Значення

Приклад

+

Додавання

a+b

-

Віднімання

a-b

*

Множення

a*b

/

Ділення

a/b

%

Залишок від ділення

a%6

      Для наведених арифметичних операцій діють наступні правила :
   • бінарні операції додавання (+) та віднімання (-) можуть застосовуватися до цілих та дійних чисел, а також до покажчиків;
   • в операціях множення (*) та ділення (/) операнди можуть бути будь-яких арифметичних типів;
   • операція "залишок від ділення" застосовується лише до цілих операндів.
   • операції виконуються зліва направо, тобто спочатку обчислюється вираз лівого операнда, потім вираз, що стоїть справа від знака операції. Якщо операнди мають однаковий тип, то результат арифметичної операції має той же тип. Тому, коли операції ділення / застосовується до цілих або символьних змінних, залишок відкидається. Так, вираз 11/3 буде рівний 3, а вираз 1/2 буде рівним нулю.
      В мові Сі визначені також і унарні арифметичні операції (таблиця 1.5.).

Таблиця 1.5. Унарні арифметичні операції

Операція

Значення

Приклад

+

Унарний плюс (підтвердження знака)

+5

-

Унарний мінус (зміна знака)

-x

++

Операція інкременту (збільшення на 1)

i++, ++i

--

Операція декременту (зменшення на 1)

j--, --j

      Операція інкременту (++) збільшує операнд на одиницю, а операція декременту (--) відповідно зменшує операнд на одиницю. Ці операції виконуються швидше, ніж звичайні операції додавання одиниці (a=a+1;) чи віднімання одиниці (a=a-1;).
      Існує дві форми запису операцій інкременту та декременту : префіксна та постфіксна.
      Якщо операція інкременту (декременту) розміщена перед змінною, то говорять про префіксну форму запису інкременту (декременту). Якщо операція інкременту (декременту) записана після змінної, то говорять про постфіксну форму запису. У префіксній формі змінна спочатку збільшується (зменшується) на одиницю, а потім її нове значення використовується у виразі. При постфіксній формі у виразі спочатку використовується поточне значення змінної, а потім відбувається збільшення (зменшення) цієї змінної на одиницю.
      Приклад, який демонструє роботу операції інкременту:
#include<stdio.h>
void main()
{
    int x=3,y=3;
    printf("Значення префіксного виразу : %d\n ",++x);
    printf("Значення постфіксного виразу: %d\n ",y++);
    printf("Значення х після інкременту : %d\n ",x);
    printf("Значення y після декременту : %d\n ",y);
}

1.4.2 Операції присвоювання
      В мові Сі знак = не означає "дорівнює". Він означає операцію присвоювання деякого значення змінній. Тобто зміст рядка вигляду "vr1=1024;" не виражається словами "vr1 дорівнює 1024". Замість цього потрібно казати так : "присвоїти змінній vr1 значення 1024".
Перелік операцій присвоювання мови Сі ілюструє таблиця 1.6.

Таблиця 1.6. Операції присвоювання

Операція

Значення

a = b

присвоювання значення b змінній а

a += b

додавання з присвоюванням. Означає a = a + b

a -= b

віднімання з присвоюванням. Означає a = a - b

a *= b

множення з присвоюванням. Означає a = a * b

a /= b

ділення з присвоюванням. Означає a = a / b

a %= b

залишок від ділення з присвоюванням. Означає a = a % b

a <<= b

зсув вліво з присвоюванням. Означає a = a << b

a >>= b

зсув вправо з присвоюванням. Означає a = a >> b

a &= b

порозрядне І з присвоюванням. Означає a = a & b

a |= b

порозрядне АБО з присвоюванням. Означає a = a | b

a ^= b

побітове додавання за МОД2 з присвоюванням, означає a = a ^ b

      Операція присвоювання повертає як результат присвоєне значення. Завдяки цьому в мові Сі допускаються присвоювання виду :
a=(b=c=1)+1;
      Розглянемо приклад, який демонструє використання таких присвоювань.
#include<stdio.h>
void main()
{
    int data1, data2, data3;
    data1=data2=data3=68;
    printf("\ndata1==%d\ndata2==%d\ndata3==%d",
    data1,data2,data3);
}

      Результат роботи програми виглядає так :
data1==68
data2==68
data3==68
data1=data2=data3=68;

      Присвоювання відбувається справа наліво : спочатку змінна data3 отримує значення 68, потім змінна datа2 і нарешті data1.

1.4.3 Операції порівняння

Таблиця 1.7. Операції порівняння

Операція

Значення

<

менше

<=

менше або рівно

==

перевірка на рівність

>=

більше або рівно

>

більше

!=

перевірка на нерівність

      Операції порівняння здебільшого використовуються в умовних виразах. Приклади умовних виразів :
b<0, 'b'=='B','f'!='F', 201>=205,
      Кожна умова перевіряється : істинна вона чи хибна. Точніше слід сказати, що кожна умова приймає значення "істинно" (true) або "хибно" (flase). В мові Сі немає логічного (булевого) типу. Тому результатом умовного виразу є цілочисельне арифметичне значення. "Істинно" - це ненульова величина, а "хибно" - це нуль. В більшості випадків в якості ненульового значення "істинно" використовується одиниця.
      Приклад :
#include<stdio.h>
main()
{
    int tr, fal;
    tr=(111<=115); /* вираз істинний */
    fal=(111>115); /* вираз хибний */
    printf("true - %d false - %d \n",tr,fal);
    return 0;
}

1.4.4 Логічні операції
      Логічні операції &&, ||, ! використовуються здебільшого для "об'єднання" виразів порівняння у відповідності з правилами логічного І, логічного АБО та логічного заперечення (таблиця 1.8.).

Таблиця 1.8. Логічні операції

Операція

Значення

&&

логічне І (and)

| |

логічне АБО (or)

!

логічне заперечення (not)

      Складні логічні вирази обчислюються "раціональним способом". Наприклад, якщо у виразі
(A<=B)&&(B<=C)
      виявилось, що А більше В, то всі вирази, як і його перша частина (А<=B), приймають значення "хибно", тому друга частина (В<=C) не обчислюється.
      Результат логічної операції 1, якщо істина і 0 у протилежному випадку.

Таблиця 1.9. Таблиця істинності логічних операцій

E1

E2

E1&&E2

E1||E2

!E1

0

0

0

0

1

0

1

0

1

1

1

0

0

1

0

1

1

1

1

0

1.4.5 Порозрядні операції (побітові операції)
      Порозрядні операції застосовуються тільки до цілочисельних операндів і "працюють" з їх двійковими представленнями. Ці операції неможливо використовувати із змінними типу double, float, long double.

Таблиця 1.10. Порозрядні операції

Операція

Значення

~

порозрядне заперечення

&

побітова кон'юнкція (побітове І)

|

побітова диз'юнкція (побітове АБО)

^

побітове додавання за МОД2

<<

зсув вліво

>>

зсув вправо

Таблиця 1.11. Таблиця істинності логічних порозрядних операцій

E1

E2

E1&E2

E1^E2

E1|E2

0

0

0

0

0

0

1

0

1

1

1

0

0

1

1

1

1

1

0

1

   • Порозрядне заперечення ! заміняє змінює кожну 1 на 0, а 0 на 1.
     Приклад :
~ (10011010) == (01100101)
   • Порозрядна кон'юнкція & (порозрядне І) порівнює послідовно розряд за розрядом два операнди. Для кожного розряду результат рівний 1, якщо тільки два відповідних розряди операндів рівні 1, в інших випадках результат 0.
   • Приклад :
(10010011) & (00111101) == (00010001)
   • Порозрядна диз'юнкція | (порозрядне АБО) порівнює послідовно розряд за розрядом два операнди. Для кожного розряду результат рівний 1, якщо хоча б один з відповідних розрядів рівний 1.
     Приклад :
(10010011) | (00111101) == (10111111)
   • Побітове додавання за МОД2 порівнює послідовно розряд за розрядом два операнди. Для кожного розряду результат рівний 1, якщо один з двох (але не обидва) відповідних розряди рівні 1.
     Приклад :
(10010011) ^ (00111101) == (10101110)
     На операції побітового додавання за МОД2 ґрунтується метод обміну значень двох цілочисельних змінних.
     
a^=b^=a^=b;
   • Операція зсуву вліво (вправо) переміщує розряди першого операнду вліво (вправо) на число позицій, яке задане другим операндом. Позиції, що звільняються, заповнюються нулями, а розряди, що зсуваються за ліву (праву) границю, втрачаються.
     Приклади :
     
(10001010) << 2 == (00101000)
     
(10001010) >> 2 == (00100010)

1.4.6 Операція слідування (кома)
     Операція "кома" (,) називається операцією слідування, яка "зв'язує" два довільних вирази. Список виразів, розділених між собою комами, обчислюються зліва направо. Наприклад, фрагмент тексту
a=4;
b=a+5;

     можна записати так :
a=4, b=b+5;
     Операція слідування використовується в основному в операторах циклу for() (про оператори циклів піде мова пізніше).
     Для порівняння наводимо приклад з використанням операції слідування (приклад 1) та без неї (приклад 2):
     Приклад 1.
int a[10],sum,i;
/* ...
*/
sum=a[0];
for (i=1;i<10;i++)
sum+=a[i];

     
Приклад 2.
int a[10],sum,i;
/* ... */
for (i=1,sum=a[0];i<10;sum+=a[i],i++) ;

1.4.7 Умовна операція ?:
     
Умовна операція ?: - єдина тернарна операція в мові Сі. Її синтаксис :
умова ? вираз_1 : вираз_2
     Принцип її роботи такий. Спочатку обчислюється вираз умови. Якщо цей вираз має ненульове значення, то обчислюється вираз_1. Результатом операції ?: в даному випадку буде значення виразу_1. Якщо вираз умови рівний нулю, то обчислюється вираз_2 і його значення буде результатом операції. В будь-якому випадку обчислюється тільки один із виразів (вираз_1 або вираз_2).
     Наприклад, дану операцію зручно використати для знаходження найбільшого з двох чисел x і y:
max=(x>y)?x:y;
     Приклад 1 :
#include<stdio.h>
void main()
{
   int points;
   printf("Введiть оцiнку [2..5]:");
   scanf("%d",&points);
   printf("%s",points>3?"Ви добре знаєте матерiал!":"Погано...");
}

     Приклад 2 :
j = (i<0) ? (-i) : (i); /* змінній j присвоюється модуль i*/

1.4.8 Операція sizeof()
     Дана операція обчислює розмір пам'яті, необхідний для розміщення в ній виразів або змінних вказаних типів.
     Операція має дві форми :
1).
ім'я_типу А;
     
sizeof А;
2).
sizeof (ім'я_типу);
     Операцію sizeof() можна застосовувати до констант, типів або змінних, у результаті чого буде отримано число байт, що відводяться під операнд. Приміром, sizеof(int) поверне число байт для розміщення змінної типу int.

1.5 Основи алгоритмізації

    
1.5.1 Алгоритми та їх властивості
        Алгоритм - це чітко визначена для конкретного виконавця послідовність дій, які спрямовані на досягнення поставленої мети або розв'язання задачі певного типу.
        У 820 році нашої ери в Бухарі був написаний підручник "Аль-Джабр Ва-аль-Мукабала" ("Наука виключення скорочення"), в якому були описані правила виконання чотирьох арифметичних дій над числами в десятковій системі числення. Автором підручника був арабський математик Мухаммед Бен Муса аль-Хорезмі. Від слова "альджебр" у назві підручника пішло слово "алгебра", а від імені аль-Хорезмі - слово "алгоризм", що пізніше перейшло в слово "алгоритм".
        Властивості алгоритмів :
1. Зрозумілість. В алгоритмі повинні бути лише операції, які знайомі виконавцеві. При цьому виконавцем алгоритму може бути: людина, комп'ютер, робот тощо.
2. Масовість. За допомогою складеного алгоритму повинен розв'язуватися цілий клас задач.
3. Однозначність. Будь-який алгоритм повинен бути описаний так, щоб при його виконанні у виконавця не виникало двозначних вказівок. Тобто різні виконавці згідно з алгоритмом повинні діяти однаково та прийти до одного й того ж результату.
4. Правильність. Виконання алгоритму повинно давати правильні результати.
5. Скінченність. Завершення роботи алгоритму повинно здійснюється в цілому за скінченну кількість кроків.
6. Дискретність. Алгоритм повинен складатися з окремих завершених операцій, які виконуються послідовно.
7. Ефективність. Алгоритм повинен забезпечувати розв'язання задачі за мінімальний час з мінімальними витратами оперативної пам'яті.
        Способи представлення алгоритмів. Алгоритми можуть бути представлені: у вигляді таблиці, описані як система словесних правил (лексикографічний або словеснокроковий спосіб запису алгоритму), представлені алгоритмічною мовою у вигляді послідовності операторів (операторний спосіб), або з допомогою графічного зображення у формі блок-схем ( графічний або геометричний спосіб запису алгоритму).
        Слід зауважити, що графічному способу подання алгоритмів надається перевага через його простоту, наочність і зручність. Блок-схема алгоритму зображає послідовність блоків, з'єднаних між собою стрілками, які вказують послідовність виконання і зв'язок між блоками. Всередині блоків записується їх короткий зміст.

1.5.2 Блок-схеми
        Блок-схема - це спосіб представлення алгоритму в графічній формі, у вигляді геометричних фігур, сполучених між собою лініями (стрілками). Форма блока визначає тип дії, а текст всередині блоку дає детальне пояснення конкретної дії. Стрілки на лініях, що сполучають блоки схеми, вказують послідовність виконання команд, передбачених алгоритмом. Блок-схеми, за рахунок наочності спрощують створення ефективних алгоритмів, розуміння роботи вже створених, а як наслідок і їх оптимізацію. Існуючі стандарти на типи блоків дозволяють легко адаптувати алгоритми, створені у вигляді блок-схем до будь-яких існуючих на сьогоднішній день мов програмування.
        Зображення блоків у алгоритмі, їх розміри, товщина ліній, кут нахилу ліній тощо, регламентуються Державним стандартом "Схеми алгоритмів, програм, даних і систем", а саме : 19.701-90 (ISO 5807-85).
        Блоки у блок-схемі з'єднуються лініями потоків. У кожен блок може входити не менше однієї лінії, з блоку ж (окрім логічного) може виходити лише одна лінія потоку . З логічного блоку завжди виходять дві лінії потоку: одна у випадку виконання умови, інша - при її невиконанні. Бажано, щоб лінії потоку не перетинались.
        Алгоритм може бути детальним, або спрощеним (деякі зрозумілі блоки можуть не записуватись, інакше алгоритм збільшується в розмірі).
        Основні види блок-схем :
   • прості (нерозгалужені);
   • розгалужені;
   • циклічні;
   • з підпрограмами;
   • змішані.




Рис. 1.2.
Основні елементи блок-схем

1.5.3 Базові алгоритмічні конструкції
        Базові алгоритмічні конструкції - це способи управління процесами обробки даних. Виділяють три базові алгоритмічні конструкції:
1. лінійні алгоритми ;
2. алгоритми розгалуженої структури;
3. алгоритми циклічної структури.


        Лінійні алгоритми (рис. 1.3). Алгоритм називається лінійним, якщо блоки алгоритму виконуються один за одним. Алгоритми лінійної структури не містять умовних і безумовних переходів, циклів.
        Алгоритми розгалуженої структури (рис.1.4). Якщо вибраний метод розв'язання задачі передбачає виконання різних дій в залежності від значень будь-яких змінних, але при цьому кожна гілка алгоритму в процесі розв'язання задачі виконується не більше одного разу, алгоритм називається розгалуженим.
        Алгоритми циклічної структури (рис.1.5).
Цикл - це команда виконавцеві (компілятору) багаторазово повторити послідовність певних команд.
При багатократному проходженні деяких ділянок алгоритму в процесі виконання алгоритм називається циклічним. Кількість проходжень циклу повинна бути повністю визначена алгоритмом розв'язання задачі, інакше виникає "зациклювання", при якому процес розв'язання задачі не може завершитися.
Алгоритми розв'язку задач циклічної структури можуть бути такими, що при однократному проході циклу деякі ділянки алгоритму виконуються неодноразово, тобто всередині циклу існують інші цикли. Алгоритми такої структури називаються алгоритмами з вкладеними циклами.

       Тепер перейдемо до запису алгоритмів програм безпосередньо мовою програмування Сі.
        Оператори - це основні елементи, з яких "будуються" програми на будь-якій мові програмування. Більшість операторів складаються з виразів. Виходячи з цього, спочатку розглянемо вирази.
Вираз представляє собою об'єднання операцій і операндів. Найпростіший вираз складається з одного операнду.
Приклади виразів :
5
-7
10+21
a*(b+d*1)-1
x=++a%3
a>3

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

-5+7

2

1<2

1

6+(a=1+2)

9

a=1+2

3

        Як вже було сказано, основу будь-якої програми складають оператори. Оператором-виразом називається вираз, вслід за яким стоїть крапка з комою.         Взагалі усі оператори можна згрупувати у наступні класи:
   • оператори присвоювання;
   • виклики функцій;
   • розгалуження;
   • цикли.
        Проте, оператори найчастіше відносяться до більш ніж одного з чотирьох класів. Наприклад, оператор if (a=fn(b+c)>d) складається з представників наступних класів : присвоювання, виклик функції та розгалуження. У тому і є гнучкість Сі, що є можливість змішування в одному операторі операторів різних класів. Проте навряд чи слід цим зловживати - програма може вийти правильною, проте надто заплутаною та нечитабельною.

1.6.1 Оператор розгалуження if
        Оператор розгалуження призначений для виконання тих або інших дій в залежності від істинності або хибності деякої умови. Основний оператор цього блоку в Сі - if ... else не має ключового слова then, як у Паскалі, проте обов'язково вимагає, щоб умова, що перевіряється, розміщувалася б у круглих дужках. Оператор, що слідує за логічним виразом, є then- частиною оператору if...else.
        Синтаксис оператора :
if (<умова>)
<оператор1>;
[else <оператор2;>]


Рис. 1.6. Синтаксис оператора if

        Умова хибна, якщо вона дорівнює нулю, в інших випадках вона істинна. Це означає, що навіть від'ємні значення розглядаються як істинні. До того ж, умова, що перевіряється, повинна бути скалярною, тобто зводитися до простого значення, яке можливо перевірити на рівність нулю. Взагалі не рекомендується використання змінних типу float або double в логічних виразах перевірки умов з причини недостатньої точності подібних виразів. Більш досвідчені програмісти скорочують оператори типу:
if (вираз!=0) оператор;
        до наступного:
if (вираз) оператор;
        Обидва логічні вирази функціонально еквівалентні, тому що будь-яке ненульове значення розцінюється як істина. Це можна довести наступними програмами:
Приклад 1.
/* програма виводить результат ділення двох дійсних чисел */
#include<stdio.h>
#include<conio.h>
void main()
{
      float a,b,c;
      printf("Введiть число a :\n");
      scanf("%f",&a);
      printf("Введiть число b :\n");
      scanf("%f",&b);
      if (b==0) printf("Дiлення да нуль !\n");
      else
      {
            c=a/b;
            printf("a : b == %g",c);
      };
}

Приклад 2.
/* застосування умовного розгалужування */
#include <stdio.h>
main()
{
      int number;
      int ok;
      printf("Введіть число з інтервалу 1..100 : ");
      scanf("%d",&number);
      ok=(1<=number) && (number<=100);
      if (!ok)
            printf("Не коректно !!\n");
      return ok;
}
      Змінній ok присвоюється значення результату виразу: ненульове значення, якщо істина, і в протилежному випадку - нуль. Умовний оператор if(!ok) перевіряє, якщо ok дорівнюватиме нулю, то !ok дасть позитивний результат й відтоді буде отримано повідомлення про некоректність, виходячи з контексту наведеного прикладу.

1.6.2 Оператор switch
Синтаксис :
switch(<вираз цілого типу>)
{
      case <значення_1>:
            <послідовність_операторів_1>;
      break;
      case <значення_2>:
            <послідовність_операторів_2>;
      break;
      ..............................................................
      case <значення_n>:
            <послідовність_операторів_n>;
      break;
      [default:
            <послідовність_операторів_n+1>;]
}

      Оператор-перемикач switch призначений для вибору одного з декількох альтернативних шляхів виконання програми. Виконання оператора switch починається з обчислення значення виразу (виразу, що слідує за ключовим словом switch у круглих дужках). Після цього управління передається одному з <операторів>. Оператор, що отримав управління - це той оператор, значення константи варіанту якого співпадає зі значенням виразу перемикача.
      Вітка default (може опускатися, про що свідчить наявність квадратних дужок) означає, що якщо жодна з вищенаведених умов не задовольнятиметься (тобто вираз цілого типу не дорівнює жодному із значень, що позначені у саse-фрагментах), керування передається по замовчуванню в це місце програми. Треба також зазначити обов'язкове застосування оператора break у кожному з case-фрагментів (цей оператор застосовують для негайного припинення виконання операторів while, do, for, switch), що негайно передасть керування у точку програми, що слідує відразу за останнім оператором у switch-блоці.
Приклад
1:
switch(i)
{
      case -1:
            n++;
      break;
      case 0:
            z++;
      break;
      case 1:
            p++;
      break;
}

Приклад 2 :
switch(c)
{
      case 'A':
            capa++;
      case 'a':
            lettera++;
      default:
            total++;
}

      
В останньому прикладі всі три оператори в тілі оператора switch будуть виконані, якщо значення с рівне 'A', далі оператори виконуються в порядку їх слідування в тілі, так як відсутні break.

1.6.3 Оператор циклу з передумовою while
      Оператор while використовується для організації циклічного виконання оператора або серії операторів, поки виконується певна умова.
      Синтаксис :
while (<логічний вираз>)
      оператор;


Рис. 1.7. Синтаксис оператора while

      Цикл закінчується у наступних випадках :
1. умовний вираз у заголовку приймає нульове значення;
2. у тілі циклу досягнуто місця, де розташований оператор break;
3. у тілі циклу виконаний оператор return;
      У перших двох випадках керування передається оператору, розташованому безпосередньо за циклом, у третьому випадку активна на той момент функція завершує свою роботу, повертаючи якесь значення.
Знову ж таки нерідкою помилкою програмістів, що працювали раніше на Паскалі, є використання замість оператора порівняння (==) оператора присвоювання (=). Наприклад, наступний фрагмент створить нескінчений цикл:
/* некоректне використання оператору циклу */
int main(void)
{
      int j=5;
      while(j=5) /* змінній j присвоїти значення 5 */
      {
            printf("%d\n",j);
            j++;
      }
}

      Компілятор Сі попередить про некоректне присвоювання в даному випадку, виправити яке особливих труднощів не викличе.
Втім, часто такий цикл використовується для перевірки відповіді користувача на питання з програми ("так чи ні ?"):

/* фрагмент використання while */
printf ("Підтверджуєте ? Так чи ні ?(y/n);");
scanf("%c",&ch);
while (ch!='y' && ch!='n')
{
      printf("\n Відповідайте так чи ні . . (y/n);");
      scanf("%c",&ch);
}

      Тіло циклу почне виконуватися, якщо користувач введе будь-який символ, відмінний від у або n. Цикл виконується доти, доки користувач не введе або 'у' , або 'n'.
      Цікаво розглянути й наступний приклад, що застосовує оператор while у функції підрахунку факторіалу:
long factorial(int number)
{
      long total;
      total=number;
      while (--number)
            total*=number;
      return total;
}

1.6.4 Оператор циклу з постумовою do … while
      Оператор do…while використовується для організації циклічного виконання оператора або серії операторів, які називаються тілом циклу, до тих пір, поки умова не стане хибною.
      Синтаксис :
do
      <оператор>;
while (<логічний_вираз>);


Рис. 1.8. Синтаксис оператора do … while

      Ситуації, що призводять до виходу з циклу, аналогічні наведеним для циклу while із передумовою. Характерним є те, що тіло циклу виконається хоча б один раз. На відміну від Паскаля, в якому цикл з постумовою repeat operator until умова виконується, поки умова невірна, цикл do ... while навпаки припиняє виконання, коли умовний вираз обертається в нуль (стає невірним).
       Приклад 1.
printf ("Підтверджуєте ? Так чи ні ?(y/n);");
do
      scanf("%c",&ch);
while (ch!='y' && ch!='n')

      Приклад 2.
#include<stdio.h>
#include<conio.h>
void main()
{
      int n,i;
      float fact;
      printf("Програма обчислення n!.\n");
      printf("Введiть число n :\n");
      scanf("%d",&n);
      i = 1;
      fact = 1;
      do {
            fact *= i;
            i++;
      } while (i <= n);
      printf("n!==%g",fact);
}

1.6.5 Оператор розриву break
      
Синтаксис :
break;
      
Оператор розриву break перериває виконання операторів do, for, while або switch.
В операторі switch він використовується для завершення блоку case.
В операторах циклу - для негайного завершення циклу, що не зв'язане з перевіркою звичайної умови завершення циклу. Коли оператор break зустрічається всередині оператора циклу, то здійснюється негайний вихід з циклу і перехід до виконання оператору, що слідує за оператором циклу.
      Приклад
:
main()
{
      int i;
      for (i=0;i<1000;i++)
      {
            printf("%d - %d\n",i,i*i*i);
            if (i*i*i>=10000) break;
      }
      return 0;
}

1.6.6 Оператор продовження continue
      
Синтаксис :
continue;
      
Оператор continue передає управління на наступну ітерацію в операторах циклу do, for, while. Він може розміщуватися тільки в тілі цих операторів. В операторах do і while наступна ітерація починається з обчислення виразу умови. Для оператора for наступна ітерація починається з обчислення виразу зміни значення лічильника.
      Приклад :
while (i-- > 0)
{
      x=f(i);
      if (x == 1) continue;
            else y=x*x;
}

      В даному прикладі тіло циклу while виконується якщо i більше нуля. Спочатку значення f(i) присвоюється змінній x;потім, якщо x не рівний 1, то y присвоюється значення квадрата числа х, і управління передається на "заголовок" циклу, тобто на обчислення виразу (i-- > 0). Якщо ж х рівний 1, то виконується оператор продовження continue, і виконання продовжується з "заголовку" оператора циклу while, без обчислення квадрата x.

1.6.7 Оператор циклу for
      Оператор for забезпечує циклічне повторення деякого оператора певне число разів. Оператор, який повторюється називається тілом циклу. Повторення циклу звичайно здійснюється з використанням деякої змінної (лічильника), яка змінюється при кожному виконанні тіла циклу. Повторення завершується, коли лічильник досягає заданого значення.
      Синтаксис оператора:
for([ініціалізація];[перевірка_умови];[нове_значення])
оператор ;


Рис. 1.9. Синтаксис оператора for

      Звернемо увагу на те, що кожен з трьох виразів може бути відсутнім. Перший вираз служить для ініціалізації лічильника, другий - для перевірки кінця циклу, а третій вираз - для зміни значення лічильника. Формально роботу циклу можна описати такими кроками:
1. якщо перший вираз (ініціалізація) присутній, то він обчислюється;
2. обчислюється вираз умови (якщо він присутній). Якщо умова виробляє значення 0, тобто вона невірна, цикл припиняється, у протилежному випадку він буде продовжений;
3. виконується тіло циклу;
4. якщо присутній вираз зміни лічильника, то він обчислюється;
5. надалі перехід до пункту під номером 2.
Поява у будь-якому місці циклу оператора continue призведе до негайного переходу до пункту 4.
      Приклад використання циклу for :
/* друк парних чисел у проміжку від 500 до 0 */
#include <stdio.h>
void main(void)
{
      long i;
      for(i=500;i>=0;i-=2)
      printf("\n%ld",i);
      printf("\n");
}

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

#include <stdio.h>
int main(void)
{
      long i;
      for(i=500;i>=0;printf("\n%ld",i),i-=2) ;
}

Другий варіант використовує оператор continue:
#include <stdio.h>
int main(void)
{
      long i;
      for(i=500;i>=0;i--)
            if (i%2 == 1)
                  continue;
            else
                  printf("\n %ld", i );
      printf("\n");
}

      Справа програміста, який з варіантів обрати - надати перевагу більш стислому викладанню або навіть взагалі скористатися іншим оператором. Цікаво, що різновид циклу for можна звести до циклу while наступним чином:
for(вираз1;вираз2;вираз3)
      оператор;

/* далі - аналогічний цикл while */
вираз1;
while (вираз2)
{
      оператор;
      вираз3;
}

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

1.6.8 Оператор переходу goto
      Синтаксис :
goto <мітка>;
/* ... */
<мітка> : <оператор>;

      Оператор безумовного переходу goto передає управління безпосередньо на <оператор>, перед яким розташована <мітка>. Область дії мітки обмежена функцією, в якій вона визначена. Тому, кожна мітка повинна бути відмінною від інших в одній і тій самій функції. Також, неможливо передати управління оператором goto в іншу функцію.
      Оператор, перед яким розташована <мітка> виконується зразу після виконання оператора goto.
      Якщо оператор з міткою відсутній, то компілятор видасть повідомлення про помилку.
      Приклад використання goto:
if (errorcode>0)
      goto exit;

exit :
return errorcode;

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

1.6.9 "Порожній" оператор
      Синтаксис :
;
      Порожній оператор - це оператор що складається лише з крапки з комою. Він може використовуватися в будь-якому місці програми, де за синтаксисом потрібний оператор.
for (i=0;i<10;printf("%d\n",i);) ;

1.6.10 "Складений" оператор
      "Складений" оператор представляє собою два або більше операторів. Його також називають "блоком".
      Синтаксис :
{
      [<оператори>]
}

      Дія складеного оператора полягає в обов'язковому послідовному виконанні операторів, які містяться між { та }, за виключенням тих випадків, коли який-небудь оператор явно не передасть управління в інше місце програми.
if (i>0)
{
      printf("i == %d\n",i);
      i--;
}

1.7 Тип перерахування enum

       При написанні програм часто виникає необхідність визначити декілька іменованих констант, для яких потрібно, щоб всі вони мали різні значення (при цьому конкретні значення можуть бути не важливими). Для цього зручно скористатися типом даних "перерахування" enum (enumeration), всі можливі значення якого задаються списком цілочисельних констант.
       Синтаксис :
enum [ ім'я_типу ] { список_констант };
       Ім'я типу задається тоді, коли в програмі є необхідність визначати змінні даного типу. Компілятор забезпечує, щоб ці змінні приймали значення тільки із вказаного списку констант.
enum {mRead, mEdit, mWrite, mCreate } Mode;
       Цей оператор вводить іменовані константи mRead, mEdit, mWrite і змінну Mode, яка може приймати значення цих констант. В момент оголошення змінна ініціалізується значенням першої константи, в наведеному прикладі - mRead. В подальшому їй можна присвоювати будь-які допустимі значення. Наприклад :
Mode = mCreate;
        Значення змінної типу перерахування можна перевіряти, порівнюючи її з можливими значеннями. Крім того, потрібно враховувати, що типи перерахування відносяться до цілих порядкових типів і до них можуть бути застосовані будь-які операції порівняння. Наприклад :
if (Mode>mRead) /* … */ ;
       Змінну Mode можна також використовувати в структурі switch:
switch(Mode)
{
       case mRead: /* … */
       break;
       case mEdit: /* … */
       break;
       case mWrite: /* … */
       break;
       case mCreate: /* … */
       break;
}

       По замовчуванню значення, які вказані в enum, інтерпретуються як цілі числа, причому перше значення рівне 0, друге - 1 і т.д. Значення по замовчанню можна змінити, якщо після імені константи вказати знак рівності і задати ціле значення константи. Наприклад :
enum {mRead = -1, mEdit, mWrite = 2, mCreate } Mode;
       Якщо після констант не задане їх ціле значення, воно вважається на 1 більшим, ніж попереднє. Тому для нашого прикладу значення констант такі:

mRead

-1

mEdit

0

mWrite

2

mCreate

3

1.8 Покажчики

1.8.1 Основні відомості про покажчики
       В результаті процесу компіляції програми всі імена змінних будуть перетворені в адреси комірок пам'яті, в яких містяться відповідні значення даних. У командах машинної програми при цьому знаходяться машинні адреси розміщення значень змінних. Саме це і є пряма адресація - виклик значення за адресою в команді. Наприклад, в операторі присвоювання:
k = j на машинному рівні відбувається копіювання значення з області ОП, що відведена змінній j, в область ОП, яка відведена змінній k. Таким чином, при виконанні машинної програми реалізуються операції над операндами - значеннями змінних, розташованими за визначеними адресами ОП. На машинному рівні імена змінних у командах не використовуються, а тільки адреси, сформовані транслятором з використанням імен змінних. Проте програміст не має доступу до цих адрес, якщо він не використовує покажчики.
       Покажчики в Сі використовується набагато інтенсивніше, аніж, скажімо, у Паскалі, тому що іноді деякі обчислення виразити можливо лише за їх допомогою, а частково й тому, що з ними утворюються більш компактні та ефективніші програми, аніж ми використовували б звичайні засоби. Навіть існує твердження - аби стати знавцем Сі, потрібно бути спеціалістом з використання покажчиків.
       Покажчик (вказівник) - це змінна або константа стандартного типу даних для збереження адреси змінної визначеного типу. Значення покажчика - це беззнакове ціле, воно повідомляє, де розміщена змінна, і нічого не говорить про саму змінну.
       Тип змінної, що адресується, може бути стандартний, нумерований, структурний, об'єднання або void. Покажчик на тип
void може адресувати значення будь-якого типу. Розмір пам'яті для самого покажчика і формат збереженої адреси (вмісту покажчика) залежить від типу комп'ютера та обраної моделі пам'яті. Константа NULL зі стандартного файлу stdio.h призначена для ініціалізації покажчиків нульовим (незайнятим) значенням адреси.
       Змінна типу покажчик оголошується подібно звичайним змінним із застосуванням унарного символу "*". Форма оголошення змінної типу покажчик наступна:
       
тип [модифікатор] * імені-покажчика ;
де тип - найменування типу змінної, адресу якої буде містити змінна-покажчик (на яку він буде вказувати).
       Модифікатор необов'язковий і може мати значення:
   •
near - ближній, 16-бітний покажчик (встановлюється за замовчуванням), призначений для адресації 64-кілобайтного сегмента ОП;
   •
far - дальній, 32-бітний покажчик, містить адресу сегмента і зсув у ньому: може адресувати ОП обсягом до 1 Мб;
   •
huge - величезний, аналогічний покажчику типу far, але зберігається у нормалізованому форматі, що гарантує коректне виконання над ним операцій; застосовується до функцій і до покажчиків для специфікації того, що адреса функції або змінної, що адресується, має тип huge;
   • імені-покажчика - ідентифікатор змінної типу покажчик;
   • визначає змінну типу покажчик.
       Значення змінної-покажчика - це адреса деякої величини, ціле без знака. Покажчик містить адресу першого байту змінної визначеного типу. Тип змінної, що адресується, і на яку посилається покажчик, визначає об'єм ОП, що виділяється змінній, та зв'язаному з нею покажчикові. Для того, щоб машинною програмою обробити (наприклад, прочитати або записати) значення змінної за допомогою покажчика, треба знати адресу її початкового (нульового) байта та кількість байтів, що займає ця змінна. Покажчик містить адресу нульового байту цієї змінної, а тип змінної, що адресується, визначає, скільки байтів, починаючи з адреси, визначеної покажчиком, займає це значення.
       Нижче наведено приклади деяких можливих оголошень покажчиків:
int *pi; /* - покажчик - змінна на дані типу int */
float *pf; /* - покажчик - змінна на дані типу float */
int ml [5]; /* - ім'я масиву на 5 значень типу int; ml - покажчик-константа, про це йтиметься згодом */
int *m2[10]; /* m2 - ім'я масиву на 10 значень типу покажчик на значення типу int, m2 - покажчик-константа */
int (*m3)[10]; /* - покажчик на масив з 10 елементів типу int; m3 - покажчик-константа */

       Зверніть увагу на те, що у трьох з наведених оголошень ім'я масиву є константою - покажчиком! (Про це йтиметься в наступному окремому розділі.)
       За допомогою покажчиків, наприклад, можна:
1. обробляти одновимірні та багатовимірні масиви, рядки, символи, структури і масиви структур;
2. динамічно створювати нові змінні в процесі виконання програми;
3. обробляти зв'язані структури: стеки, черги, списки, дерева, мережі;
4. передавати функціям адреси фактичних параметрів;
5. передавати функціям адреси функцій в якості параметрів.
       Протягом довгого часу програмісти були незадоволені покажчиками. Зокрема, застосування покажчиків критикується через те, що в силу їх природи неможливо визначити, на яку змінну вказує в даний момент покажчик, якщо не повертатися до того місця, де покажчику востаннє було присвоєно значення. Це ускладнює програму і робить доведення її правильності дещо ускладненим. Програміст, що добре володіє Сі, повинен насамперед знати, що таке покажчики, та вміти їх використовувати. Практично у програмі можна використовувати не імена змінних, а тільки покажчики, тобто адреси розміщення змінних програми.

1.8.2 Моделі пам'яті
       У мові Сі для операційної системи MS-DOS розмір ОП (оперативної пам'яті) для розміщення покажчика залежить від типу використаної моделі пам'яті. У програмах на мові Сі можна використовувати одну з шести моделей пам'яті: крихітну (
tiny), малу (small, по замовчуванню), середню (medium), компактну (compact), велику (large) і величезну (huge).
Взагалі оперативна пам'ять для виконання програми на мові Сі використовується для:
   • розміщення програми (коду програми);
   • розміщення зовнішніх (глобальних) і статичних даних (що мають специфікатори
extern і static, про них йтиметься нижче);
   • динамічного використання ОП для змінних, сформованих у процесі виконання програми (купа, динамічна ОП, про них йтиметься нижче);
   • для розміщення локальних (
auto - автоматичних) змінних, змінних функцій (стек) під час виконання програми.


Рис. 1.10. Структура оперативної пам'яті

       ОП програми та її статичних даних у процесі виконання програми залишається незмінною. ОП з купи виділяється та звільняється в процесі виконання програми. Об'єм ОП для купи залежить від того, скільки ОП запитує програма за допомогою функцій calloc() та malloc() для динамічного розміщення даних. Пам'ять стека виділяється для фактичних параметрів активізованих функцій і їх локальних (автоматичних) змінних. Розглянемо основні характеристики різних моделей ОП.
       
Крихітна (tiny model) ОП. Модель пам'яті використовується при дефіциті ОП. Для коду програми, статичних даних, динамічних даних (купи) та стеку виділяється 64 Кб. Змінна - покажчик типу near (ближній) займає 2 байти.
       
Мала (small model) ОП. Для програми призначається 64 Кб. Стек, купа і статичні дані займають по 64 Кб. Ця модель приймається по замовчуванню та використовується для вирішення маленьких і середніх задач. Покажчик типу near займає 2 байти і містить адресу - зсув усередині сегмента ОП з 64 Кб.
       
Середня (medium model) ОП. Розмір ОП для програми дорівнює 1 Мбайт. Стек, купа і статичні дані розміщаються в сегментах ОП розміром 64 Кб. Цю модель застосовують для дуже великих програм і невеликих обсягів даних. Покажчик у програмі типу far займає 4 байти. Для адресації даних покажчик типу near займає 2 байти.
       
Компактна (compact model) ОП. Для програми призначається 64 Кб. Для даних - 1 Мбайт. Об'єм статичних даних обмежується 64 Кб. Розмір стека повинен бути не більш 64 Кб. Ця модель використовується для малих і середніх програм, що вимагають великого об'єму даних. Покажчики в програмі складаються з 2 байтів, а для даних - з 4 байтів.
       
Велика (large model) ОП. ОП для програми обмежена 1 Мб. Для статичних даних призначається 64 Кб. Купа може займати до 1 Мб. Програма і дані адресуються покажчиками, що займають 4 байти. Модель використовується для великих задач. Окрема одиниця даних, наприклад масив, повинна займати не більш 64 Кб.
       
Величезна (huge model) ОП. Аналогічна великій моделі. Додатково в ній знімається обмеження на розмір окремої одиниці даних.

1.8.3 Основні операції над покажчиками
       Мова Сі надає можливість використання адрес змінних програми за допомогою основних операцій - & та *:
За допомогою основних операцій можна отримати значення адреси змінної а використовуючи непряму адресацію - одержати значення змінної за її адресою.
Призначення цих операцій:
       
& ім'я змінної - одержання адреси; визначає адресу розміщення значення змінної визначеного типу;
       
* ім'я-покажчика - отримання значення визначеного типу за вказаною адресою; визначає вміст змінної, розміщеної за адресою, що міститься у даному покажчику; це - непряма адресація (інші назви - "зняття значення за покажчиком" або "розіменування" ).
       Оператор присвоювання значення адреси покажчику має вигляд:
   
    Ім'я_змінної_покажчика = & ім'я змінної;
Наприклад:
int i, *pi; /* pi -змінна покажчик */
pi = &i; /* pi одержує значення адреси 'i' */

       Операція & - визначення адреси змінної повертає адресу ОП свого операнда. Операндом операції & повинне бути ім'я змінної того ж типу, для якого визначений покажчик лівої частини оператора присвоювання, що одержує значення цієї адреси. У вищенаведеному прикладі це тип int.
       Операції * і & можна писати впритул до імені операнду або через пробіл. Наприклад: &і, * pi.
Непряма адресація змінної за допомогою операції * здійснює доступ до змінної за покажчиком, тобто повернення значення змінної, розташованої за адресою, що міститься у покажчику. Операнд операції * обов'язково повинен бути типу покажчик. Результат операції * - це значення, на яке вказує (адресує, посилається) операнд. Тип результату - це тип, визначений при оголошенні покажчика.
       У загальному вигляді оператор присвоювання, що використовує ім'я покажчика та операцію непрямої адресації, можна представити у вигляді:
ім'я змінної * ім'я-покажчика;
де ім'я-покажчика - це змінна або константа, що містить адресу розміщення значення, необхідного для змінної лівої частини оператора присвоювання.
       Наприклад:
i= *pi; /* 'i' одержує значення, розташоване за адресою, що міститься в покажчику 'pi' */
       Як і будь-які змінні, змінна pi типу покажчик має адресу і значення. Операція & над змінною типу покажчик: &pi - дає адресу місця розташування самого покажчика, pi - ім'я покажчика визначає його значення, a *pi - значення змінної, що адресує покажчик.
       Звичайно, усі ці значення можна надрукувати. Наприклад, за допомогою наступної програми:
#include <stdio.h>
void main()
{
    char c = 'A';
    int i = 7776;
    int *pi = &i;
    char *pc = &c;
    printf ("pi=%u,*pi=%d, &pi=%u\n", pi, *pi, &pi);
    printf ("pc=%u, *pc=%c, &pc=%u\n", pc, *pc, &pc);
}

       У результаті виконання буде виведено:
pi = 65522, *pi = 7776, &pi = 65520
pc = 65525, *рс = А, &pc = 65518

       Одне з основних співвідношень при роботі з покажчиками - це симетричність операцій адресації та непрямої адресації. Вона полягає в тому, що:
&х == х, тобто вміст за адресою змінної х є значення х. Наприклад, оголошення покажчика pi і змінних i та j:
int *pi, i = 123, j;
pi = &i; /*-присвоювання покажчику значення адреси i */
j = *pi; /* - присвоювання j вмісту за адресою pi */

       Тут змінна j отримує вміст, розташований за адресою змінної i, тобто значення змінної, що адресує покажчик pi: j = * pi = * &i = i;. Два останніх вищенаведених оператора виконують те саме, що один оператор: j = i.
       Для повного остаточного розуміння процесів, що відбувається у пам'яті при маніпуляції з покажчиками, розглянемо ще такий фрагмент:
void func()
{
    int х;
    int *pх; /* pх - покажчик на змінну типу int*/
    pх= &х ; /* адреса змінної х заноситься в рх*/
    *pх=77; /* число зберігається за адресою, на яку вказує рх */
}

       Розглянемо цей приклад на конкретному малюнку: функція займає область пам'яті, починаючи з адреси 0х100, х знаходиться за адресою 0х102, а рх - 0х106. Тоді перша операція присвоювання, коли значення &х(0х102) зберігається в рх, матиме вигляд, зображений на рис. 1.11 зліва:
Наступну операцію, коли число 77 записується за адресою, яка знаходиться в рх та дорівнює 0х102 (адреса х), відображає рис. 1.11 справа. Запис *рх надає доступ до вмісту комірки, на яку вказує рх.

       Далі наведений приклад програми виводу значень покажчика і вмісту, розташованого за адресою, що він зберігає.
#include<stdio.h>
void main()
{
    int i = 123, *pi = &i; /* pi-покажчик на значення типу int */
    printf("розмір покажчика pi = %d\n", sizeof(pi));
    printf("адреса розміщення покажчика pi=%u\n", &pi) ;
    printf("адреса змінної i = %u\n", &i) ;
    printf("значення покажчика pi = %u\n", pi) ;
    printf("значення за адресою pi = %d\n", *pi) ;
    printf("значення змінної i = %d\n", i) ;
}

       Результати виконання програми:
розмір покажчика pi = 2
адреса розміщення покажчика pi = 65522
адреса змінної i= 65524
значення покажчика pi = 65524
значення за адресою pi = 123
значення змінної i = 123

       Покажчики можна використовувати:
1. у виразах, наприклад, для одержання значень, розташованих за адресою, що зберігається у покажчику;
2. у лівій частині операторів присвоювання, наприклад:
    a. для одержання значення адреси, за якою розташоване значення змінної;
    b. для одержання значення змінної.
       Наприклад, якщо pi - покажчик цілого значення (змінної i), то *pi можна використовувати в будь-якому місці програми, де можна використовувати значення цілого типу. Наприклад:
int i = 123, j, *pi;
pi = &i; /*pi у лівій частині оператора присвоювання */
j = *pi + 1; /*-це еквівалентно: j = i + 1; pi-у виразі правої частини оператора присвоювання*/

       Виклик значення за покажчиком можна використовувати також як фактичні параметри при звертанні до функцій. Наприклад:
d = sqrt ((double) *pi); /* *pi - фактичний параметр */
fscant (f, "%d", pi ); /* pi - фактичний параметр */
printf ("%d\n", *pi ); /* *pi - фактичний параметр */

       У виразах унарні операції & і *, пов'язані з покажчиками, мають більший пріоритет, ніж арифметичні. Наприклад:
*рх = &х;
у = 1 + *рх; /*-спочатку виконується '*', потім '+' */

       Останній оператор еквівалентний:
у = 1 + х;
       Для звертання до значення за допомогою покажчика-змінної його можна використовувати в операторі присвоювання скрізь, де може бути ім'я змінної. Наприклад, після виконання оператора: рх = &х; цілком еквівалентними є такі описи:
Оператор: Його еквівалент: Або:
*рх =0; х = 0;
*рх += 1; *рх = *рх + 1; х = х + 1;
(*рх)++ ; *рх = *рх + 1; х = х + 1;
(*рх)--; *рх = *рх - 1; х = х - 1;

       Наступна програма демонструє найпростіше практичне використання покажчиків, виводячи звичайну послідовність літер алфавіту:
#include <stdio.h>
char c; /* змінна символьного типу*/
main()
{
    char *pc; /* покажчик на змінну символьного типу*/
    pc=&c;
    for(c='A';c<='Z';c++)
    printf("%c",*pc);
    return 0;
}

       У операторі printf("%c",*pc) має місце розіменування покажчика (*рс) - передача у функцію значення, що зберігається за адресою, яка міститься у змінній рс. Щоб дійсно довести, що рс є псевдонімом с, спробуємо замінити *рс на с у виклику функції - і після заміни програма працюватиме абсолютно аналогічно. Оскільки покажчики обмежені заданим типом даних, типовою серйозною помилкою їх використання буває присвоєння адреси одного типу даних покажчика іншого типу, на що компілятор реагує таким чином:
"Suspicious pointer conversion in function main()"
       На ТС це лише попередження (підозріле перетворення покажчика у функції main()(?!)), і якщо на нього ніяк не відреагувати, то програма працюватиме й надалі (адже помилку зафіксовано не буде) і залишається лише здогадуватися, який результат буде надалі. Зазначимо, що компілятор BС++ з приводу такого "підозрілого перетворення" пішов все-таки далі: він просто відмовляється працювати, видаючи повідомлення про помилку. Відповідальність за ініціалізацію покажчиків повністю покладається на програміста, і більш детально про це йтиметься далі.

1.8.4 Багаторівнева непряма адресація
       У мові Сі можна використовувати багаторівневу непряму адресацію, тобто непряму адресацію на 1, 2 і т.д. рівні. При цьому для оголошення і звертання до значень за допомогою покажчиків можна використовувати відповідно кілька символів зірочка: *. Зірочки при оголошенні ніби уточнюють призначення імені змінної, визначаючи рівень непрямої адресації для звертання до значень за допомогою цих покажчиків. Приклад оголошення змінної і покажчиків для багаторівневої непрямої адресації можна привести наступний:
int i = 123 /* де: i - ім'я змінної */
int *pi = &i; /* pi - покажчик на змінну і */
int **ppi = &pi; /* ppi - покажчик на покажчик на змінну pi */
int ***pppi = &ppi; /* pppi - покажчик на 'покажчик на 'покажчик на змінну ppi' */

       Для звертання до значень за допомогою покажчиків можна прийняти наступне правило, що жорстко зв'язує форму звертання з оголошенням цих покажчиків:
   • повна кількість зірочок непрямої адресації, рівна кількості зірочок при оголошенні покажчика, визначає значення змінної;
   • зменшення кількості зірочок непрямої адресації додає до імені змінної слово "покажчик", причому цих слів може бути стільки, скільки може бути рівнів непрямої адресації для цих імен покажчиків, тобто стільки, скільки зірочок стоїть в оголошенні покажчика.
Наприклад, після оголошення:
int i, *pi=&i;
звертання у виді:
*pi - визначає значення змінної,
pi - покажчик на змінну i.
А при звертанні до змінних можна використовувати різну кількість зірочок для різних рівнів адресації:
pi, ppi, pppi - 0-й рівень адресації, пряма адресація;
*pi, *ppi, *pppi - 1-й рівень непрямої адресації
**ppi, **pppi - 2-й рівень непрямої адресації
***pppi - 3-й рівень непрямої адресації
       Таким чином, до покажчиків 1-го і вище рівнів непрямої адресації можливі звертання і з меншою кількістю зірочок непрямої адресації, аніж задано при оголошенні покажчика. Ці звертання визначають адреси, тобто значення покажчиків визначеного рівня адресації. Відповідність між кількістю зірочок при звертанні за допомогою покажчика і призначенням звертання за покажчиком для наведеного прикладу ілюструє таблиця 1.12 (де Р.н.а. - рівень непрямої адресації):
Таблиця 1.12. Відповідність між кількістю уточнень (*) і результатом звертання за допомогою покажчика

Звертання

Результат звертання

Р.н.а.

i

значення змінної i

1

*pi
pi

значення змінної, на яку вказує pi
покажчик на змінну типу int, значення pi

1
0

**ppi
*ppi
ppi

значення змінної типу int
покажчик на змінну типу int
покажчик на "покажчик на змінну типу int', значення покажчика ppi

2
1
0

***pppi
**pppi
*pppi
pppi

значення змінної типу int;
покажчик на змінну типу int
покажчик на 'покажчик на змінну типу int'
покажчик на 'покажчик на 'покажчик на змінну типу int', значення покажчика pppi

3
2
1
0

1.8.5 Операції над покажчиками
       Мова Сі надає можливості для виконання над покажчиками операцій присвоювання, цілочисельної арифметики та порівнянь. Мовою Сі можливо:
1. присвоїти покажчику значення адреси даних, або нуль;
2. збільшити (зменшити) значення покажчика;
3. додати або відняти від значення покажчика ціле число;
4. скласти або відняти значення одного покажчика від іншого;
5. порівняти два покажчики за допомогою операцій відношення.
       Змінній-покажчику можна надати певне значення за допомогою одного із способів:
1. присвоїти покажчику адресу змінної, що має місце в ОП, або нуль, наприклад:
       pi = &j;
       pi = NULL;

2. оголосити покажчик поза функцією (у тому числі поза
main()) або у будь-якій функції, додавши до нього його інструкцію static; при цьому початковим значенням покажчика є нульова адреса (NULL);
3. присвоїти покажчику значення іншого покажчика, що до цього моменту вже має визначене значення; наприклад:
       pi = pj; це - подвійна вказівка однієї і тієї ж змінної;
4. присвоїти змінній-покажчику значення за допомогою функцій
calloc() або malloc() - функцій динамічного виділення ОП.
       Усі названі дії над покажчиками будуть наведені у прикладах програм даного розділу. Розглянемо кілька простих прикладів дій над покажчиками.
       Зміну значень покажчика можна робити за допомогою операцій: +, ++, -, --. Бінарні операції (+ та -) можна виконувати над покажчиками, якщо обидва покажчики посилаються на змінні одного типу, тому що об'єм ОП для різних типів даних може вирізнятися.
       Наприклад, значення типу
int займає 2 байти, а типу float - 4 байти. Додавання одиниці до покажчика додасть "квант пам'яті", тобто кількість байтів, що займає одне значення типу, що адресується. Для покажчика на елементи масиву це означає, що здійснюється перехід до адреси наступного елемента масиву, а не до наступного байта. Тобто значення покажчика при переході від елемента до елемента масиву цілих значень буде збільшуватися на 2, а типу float - на 4 байти. Результат обчислення покажчиків визначений у мові Сі як значення типу int.
       Приклад програми зміни значення покажчика на 1 квант пам'яті за допомогою операції "++" і визначення результату обчислення покажчиків даний на такому прикладі:
#include<stdio.h>
void main ()
{
     int a[] = { 100, 200, 300 };
     int *ptr1, *ptr2;
     ptr1=a; /*- ptrl одержує значення адреси а[0] */
     ptr2 = &а[2]; /*- ptr2 одержує значення адреси а[2] */
     ptr1++; /* збільшення значення ptrl на квант ОП:
     ptr1 = &а[1]*/
     ptr2++; /* збільшення значення ptr2 на квант ОП:
     ptr2 = &а[3]*/
     printf (" ptr2 - ptr1 = %d\n", ptr2 - ptr1);
}

     Результат виконання програми:
ptr2 - ptr1 = 2
     Результат 2 виконання операції віднімання визначає 2 кванти ОП для значень типу
int:
ptr2 - ptr1 = &а[3] - &а[1] = (а + 3) - (а + 1) = 2;
     У наступному Сі-фрагменті продемонстрований приклад програми для виведення значень номерів (індексів) елементів масивів, адрес першого байта ОП для їх розміщення та значень елементів масивів. Справа в тому, що в Сі є дуже важлива властивість - ім'я масиву еквівалентно адресу його нульового елемента:
х == &х[0]. Покажчики pi і pf спочатку містять значення адрес нульових елементів масивів, а при виведенні складаються з i-номером елемента масиву, визначаючи адресу i-елемента масиву. Для одержання адрес елементів масивів у програмі використовується додавання покажчиків-констант х та у, та змінних-покажчиків pi і pf з цілим значенням змінної i. Зміна адрес у програмі дорівнює кванту ОП для даних відповідного типу: для цілих - 2 байти, для дійсних - 4 байти.
#include<stdio.h>
void main()
{
     int x[4], *pi = х, i;
     float y[4], *pf = y;
     printf("\nномер елемента адреси елементів масивів:\n i pi+i х + i &x[i] pf+i у+i &y[i]\n");
     for (i = 0; i < 4; i++ )
          printf(" %d : %6u %6u %6u %6u %6u %6u\n", i, pi + i, x + i, &x[i], pf + i, y + i, &y[i]);
}

     Результати виконання програми:

номер елемента адреси елементів масивів:

i

pi+i

х+i

&x[i]

pf+i

y+i

&y[i]

0:

65518

65518

65518

65498

65498

65498

1:

65520

65520

65520

65502

65502

65502

2:

65522

65522

65522

65506

65506

65506

3:

65524

65524

65524

65510

65510

65510


     Мовою Сі можна визначити адреси нульового елемента масиву х як х або &х[0]: х == &х[0]. Краще і стисло використовувати просто х - це базова адреса масиву. Ту саму адресу елемента масиву можна представити у вигляді: х + 2 == &х[2]; х + i == &x[i].

     Те саме значення можна представити у вигляді:
*(х + 0) == *х == х[0] - значення нульового елемента масиву х;
*(х + 2) == x[2] - значення другого елемента масиву х;
*(х + i) == x[i] - значення i-го елемента масиву х.
     А операції над елементами масиву х можна представити у вигляді:
*х + 2== х[0] +2; *(х + i) - 3 == x[i] - 3;

1.8.6 Проблеми, пов'язані з покажчиками
     Проблеми, пов'язані з покажчиками, виникають при некоректному використанні покажчиків. Усі застереження щодо некоректного використання покажчиків відносяться до мови Сі так само, як і до багатьох інших низькорівневих мов програмування. Некоректним використанням покажчиків може бути:
   • спроба працювати з неініціалізованим покажчиком, тобто з покажчиком, що не містить адреси ОП, що виділена змінній;
   • втрата вказівника, тобто значення покажчика через присвоювання йому нового значення до звільнення ОП, яку він адресує;
   • незвільнення ОП, що виділена за допомогою функції
malloc();
   • спроба повернути як результат роботи функції адресу локальної змінної класу auto (про функції та класи змінних йтиметься далі);
     Запит на виділення ОП з купи робиться за допомогою функцій
calloc() та malloc(). Повернення (звільнення) ОП робиться за допомогою функції free().      Розглянемо деякі проблеми, пов'язані з покажчиками.
     При оголошенні покажчика на скалярне значення будь-якого типу оперативна пам'ять для значення, що адресується, не резервується. Виділяється тільки ОП для змінної-покажчика, але покажчик при цьому не має значення. Якщо покажчик має специфікатор static, то ініціюється початкове значення покажчика, рівне нулю (особливості статичних змінних, про що йтиметься в окремому розділі). Приклад ініціалізації покажчиків нульовими значеннями при їх оголошенні:
static int *pi, *pj; /* pi = NULL; pj= NULL; */
     Розглянемо приклад, що містить грубу помилку: спробу працювати з непроініціалізованим покажчиком.
int *х; /* змінній-покажчику 'х' виділена ОП, але 'х' не містить значення адреси ОП для змінної */
*х = 123; /* - груба помилка! */

     Таке присвоювання помилкове, тому що змінна-покажчик х не має значення адреси, за яким має бути розташоване значення змінної.
     Компілятор видасть попередження:
Warning: Possible use of 'x' before definition
     При цьому випадкове (непроініціалізоване) значення покажчика (сміття) може бути неприпустимим адресним значенням! Наприклад, воно може збігатися з адресами розміщення програми або даних користувача, або даних операційної системи. Запис цілого числа 123 за такою адресою може порушити працездатність програми користувача або самої OC. Компілятор не виявляє цю помилку, це повинен робити програміст!
     Виправити ситуацію можна за допомогою функції
malloc(). Форма звертання до функції malloc() наступна:
ім'я-покажчика = (тип-покажчика) malloc ( об'єм -ОП ) ;
де ім'я-покажчика - ім'я змінної-покажчика, тип-покажчика - тип значення, що повертається функцією
malloc;
об'єм-ОП - кількість байтів ОП, що виділяються змінній, яка адресується.
     Наприклад:
х = (int *) malloc ( sizeof (int) );
     При цьому з купи виділяється 2 байти ОП для цілого значення, а отримана адреса його розміщення заноситься в змінну-покажчик х. Значення покажчика гарантовано не збігається з адресами, що використовуються іншими програмами, у тому числі програмами OС. Параметр функції malloc визначає об'єм ОП для цілого значення за допомогою функції
sizeof(int). Запис (int *) означає, що адреса, що повертається функцією malloc(), буде розглядатися як покажчик на змінну цілого типу. Це операція приведення типів.
     Таким чином, помилки не буде у випадку використання наступних операторів:
int *х; /* х - ім'я покажчика, він одержав ОП */
х = (int *) malloc ( sizeof(int)); /* Виділена ОП цілому значенню, на яке вказує 'x' */
*х = 123; /* змінна, на яку вказує 'х', одержала значення 123*/
     Повернення (звільнення) ОП у купі виконує функція free(). Її аргументом є ім'я покажчика, що посилається на пам'ять, що звільняється. Наприклад:
free (x);
     Щоб уникнути помилок при роботі з функціями не слід повертати як результат їхнього виконання адреси автоматичних (локальних) змінних функції. Оскільки при виході з функції пам'ять для всіх автоматичних змінних звільняється, повернута адреса може бути використаною системою й інформація за цією адресою може бути невірною. Можна повернути адресу ОП, що виділена з купи.
     Одна з можливих помилок - подвійна вказівка на дані, розташовані у купі, і зменшення об'єму доступної ОП через незвільнення отриманої ОП. Це може бути для будь-якого типу даних, у тому числі для скаляра або масиву. Розглянемо випадок для скаляра.
     Приклад фрагмента програми з подвійною вказівкою і зменшенням об'єму доступної ОП через незвільнення ОП наведений нижче:
#include<alloc.h>
void main ()
{
     /* Виділення ОП динамічним змінним х, у и z: */
     int *х = (int *) malloc ( sizeof(int)),
     *у = (int *) malloc ( sizeof(int)),
     *z = (int *) malloc ( sizeof(int));
     /* Ініціалізація значення покажчиків х, у, z;*/
     *х = 14; *у = 15; *z = 17;
     /*Динамічні змінні одержали конкретні цілі значення*/
     y=x; /* груба помилка - втрата покажчика на динамічну  змінну в без попереднього звільнення її ОП */
}

     У наведеному вище прикладі немає оголошення імен змінних, є тільки покажчики на ці змінні. Після виконання оператора y = х; х та у є двома покажчиками на ту саму ОП змінної *х. Тобто *х = 14; і *у = 14. Крім того, 2 байти, виділені змінній, яку адресував y для розміщення цілого значення (*у), стають недоступними (загублені), тому що значення y, його адреса, замінені значенням х. А в купі ці 2 байти для *у вважаються зайнятими, тобто розмір купи зменшений на 2 байти. Відбулося зменшення доступної ОП. Цього слід уникати.
     Щоб уникнути такої помилки треба попередньо звільнити ОП, виділену змінній *у, а потім виконати присвоювання значення змінній у. Наприклад:
free (у); /* звільнення ОП, виділеної змінної '*у' */
у = х; /* присвоювання нового значення змінній 'у' */
     Чи можна змінній-покажчику присвоїти значення адреси в операторі оголошення ? Наприклад:
int *x = 12345;
     Тут константа 12345 цілого типу, а значенням покажчика х може бути тільки адресою, покажчиком на байт в ОП. Тому компілятор при цьому видасть повідомлення про помилку:
Error PR.CPP 3: Cannot convert 'int to 'int *'
     Проте не викличе помилки наступне присвоювання:
int a[5], *х = а;
     Використання покажчиків часто пов'язано з використанням масивів різних типів. Кожний з типів даних масивів має свої особливості. Тому далі розглянемо властивості покажчиків для роботи з масивами.

1.9 Масиви


1.9.1 Основні поняття
       Між покажчиками і масивами існує тісний взаємозв'язок. Будь-яка дія над елементами масивів, що досягається індексуванням, може бути виконана за допомогою покажчиків (посилань) і операцій над ними. Варіант програми з покажчиками буде виконаний швидше, але для розуміння він складніший.
       Як показує практика роботи на Сі, покажчики рідко використовуються зі скалярними змінними, а частіше - з масивами. Покажчики дають можливість застосовувати адреси приблизно так, як це робить ЕОМ на машинному рівні. Це дозволяє ефективно організувати роботу з масивами. Будь-яку серйозну програму, що використовує масиви, можна написати за допомогою покажчиків.
       Для роботи з масивом необхідно:
1. визначити ім'я масиву, його розмірність (кількість вимірів) і розмір - кількість елементів масиву;
2. виділити ОП для його розміщення.
       У мові Сі можна використовувати масиви даних будь-якого типу:
   • статичні: з виділенням ОП до початку виконання функції; ОП виділяється в стеку або в ОП для статичних даних;
   • динамічні: ОП виділяється з купи в процесі виконання програми, за допомогою функцій malloc() і calloc().
       Динамічні змінні використовують, якщо розмір масиву невідомий до початку роботи програми і визначається в процесі її виконання, наприклад за допомогою обчислення або введення.
       Розмір масиву визначається:
1. для статичних масивів при його оголошенні; ОП виділяється до початку виконання програми; ім'я масиву - покажчик-константа; кількість елементів масиву визначається:
       a. явно; наприклад:
int а[5];
       b. неявно, при ініціалізації елементів масиву; наприклад:
       int а[] = { 1, 2, 3 };
2. для динамічних масивів у процесі виконання програми; ОП для них запитується і виділяється динамічно, з купи; ім'я покажчика на масив - це змінна; масиви ці можуть бути:
       a. одновимірні і багатовимірні; при цьому визначається кількість елементів усього масиву й ОП запитується для всього масиву;
       b. вільні (спеціальні двовимірні); при цьому визначається кількість рядків і кількість елементів кожного рядка, і ОП запитується і виділяється для елементів кожного рядка масиву в процесі виконання програми; при використанні вільних масивів використовують масиви покажчиків;
Розмір масиву можна не вказувати. В цьому разі необхідно вказати порожні квадратні дужки:
1. якщо при оголошенні ініціалізується значення його елементів; наприклад:
  static int а[] = {1, 2, 3};
  char b[] = "Відповідь:";
2. для масивів - формальних параметрів функцій; наприклад:
  int fun1(int a[], int n);
  int fun2(int b[k][m][n]);
3. при посиланні на раніше оголошений зовнішній масив; наприклад:
  int а[5]; /* оголошення зовнішнього масиву */
  main ()
  {
         extern int а[]; /*посилання на зовнішній масив */
  }
       В усіх оголошеннях масиву ім'я масиву - це покажчик-константа! Для формування динамічного масиву може використовуватися тільки ім'я покажчика на масив - це покажчик-змінна. Наприклад:
 int *m1 = (int * ) malloc ( 100 * sizeof (int)) ;
 float *m2 = (float * ) malloc ( 200 * sizeof (float)) ;
де
m1 - змінна-покажчик на масив 100 значень типу int;
    m2 - змінна-покажчик на масив 200 значень типу float.
       Звільнення виділеної ОП відбувається за допомогою функції:
free (покажчик-змінна) ;
       Наприклад:
free(ml);
free(m2);
       Звертання до елементів масивів m1 і m2 може виглядати так:
m1[i], m2[j].
       Пересилання масивів у Сі немає. Але можна переслати масиви поелементно або сумістити масиви в ОП, давши їм практично те саме ім'я.
       Наприклад:
int *m1 = (int *) malloc(100 * sizeof(int));
int *m2 = (int *) malloc(100 * sizeof(int));
       Для пересилання елементів одного масиву в іншій можна використати оператор циклу:
for (i = 0; i < 100; i++ ) m2[i] = ml [i] ;
       Замість m2[i] = m1 [i]; можна використовувати:
*m2++ = *ml++;
або:
*(m2 + i) = *(ml + i) ;
       За допомогою покажчиків можна сполучити обидва масиви й у такий спосіб:
free(m2);
m2 = ml ;
        Після цього обидва масиви займатимуть одну й ту саму область ОП, виділену для масиву
m1. Однак це не завжди припустимо. Наприклад, коли масиви розташовані в різних типах ОП: один - у стеку, інший - у купі. Наприклад, у функції main() оголошені:
int *m1 = (int *) malloc(100* sizeof(int));
int m2[100] ;
       У вищенаведеному прикладі
m1 - пакажчик-змінна, і масив m1 розташований у купі, m2 - покажчик-константа, і масив m2 розташований у стеку. У цьому випадку помилковий оператор: m2 = m1; тому що m2 - це покажчик-константа. Але після free(m1) припустимим є оператор:
m1 = m2; /* оскільки m1 - покажчик-змінна */
       Для доступу до частин масивів і до елементів масивів використовується індексування (індекс). Індекс - це вираз, що визначає адресу значення або групи значень масиву, наприклад адреса значень чергового рядка двовимірного масиву. Індексування можна застосовувати до покажчиків-змінних на одновимірний масив - так само, як і до покажчиків-констант.
       Індексний вираз обчислюється шляхом додавання адреси початку масиву з цілим значенням для одержання адреси необхідного елемента або частини масиву. Для одержання значення за індексним виразом до результату - адреси елемента масиву застосовується операція непрямої адресації (*), тобто одержання значення за заданою адресою. Відповідно до правил обчислення адреси цілочисельний вираз, що додається до адреси початку масиву, збільшується на розмір кванта ОП типу, що адресується покажчиком.
       Розглянемо способи оголошення і формування адрес частини масиву й елементів одновимірних і багатомірних масивів за допомогою покажчиків.

1.9.2 Оголошення та звертання в одновимірних масивах
       Форма оголошення одновимірного масиву з явною вказівкою кількості елементів масиву:
тип ім'я_масива [кількість-елементів-масива];
       Звертання до елементів одновимірного масиву в загальному випадку можна представити індексуванням, тобто у вигляді
ім'я-масива [вираз];
де
ім'я-масиву - покажчик-константа;
вираз - індекс, число цілого типу; він визначає зсув - збільшення адреси заданого елемента масиву щодо адреси нульового елемента масиву.
       Елементи одновимірного масиву розташовуються в ОП підряд: нульовий, перший і т д. Приклад оголошення масиву:
int а[10];
іnt *p = а; /* - р одержує значення а */
       При цьому компілятор виділяє масив в стеку ОП розміром (sizeof(Type) * розмір-масиву ) байтів.
       У вищенаведеному прикладі це 2 * 10 = 20 байтів. Причому а - покажчик-константа, адреса початку масиву, тобто його нульового елемента, р - змінна; змінній р можна присвоїти значення одним із способів:
р = а;
р = &а[0];
р = &a[i];
де
&а[i] == (а + i) - адреса і-елемента масиву.
       Відповідно до правил перетворення типів значення адреси i-елемента масиву на машинному рівні формується таким чином:
&а[i]= а + i * sizeof(int);
       Справедливі також наступні співвідношення:
&a == a+0 == &a[0] - адреса а[0] - нульового елемента масиву;
а+2 == &а[2] - адреса а[2] - другого елементи масиву;
а+i == &a[i] - адреса a[i] - i-гo елемента масиву;
*а==*(а+0)==*(&а[0])==a[0] - значення 0-ого елемента масиву;
*(а + 2) == а[2] - значення а[2] - другого елементи масиву;
*(а + i) == а[i] - значення a[i] - i-гo елемента масиву;
*а + 2 == а[0] + 2 - сума значень а[0] і 2.
       Якщо р - покажчик на елементи такого ж типу, які і елементи масиву a та p=а, то а та р взаємозамінні; при цьому:
p == &a[0] == a + 0;
p+2 == &a[2] == a + 2;
*(p + 2) == (&a[2]) == a[2] == p[2];
*(p + i) == (&a[i]) == a[i] == p[i];
       Для a та p еквівалентні всі звертання до елементів a у вигляді:
a[i], *(a+i), *(i+a), i[a], та
p[i], *(p+i), *(i+p), i[p]

1.9.3 Оголошення та звертання до багатовимірних масивів
       У даному розділі розглянемо оголошення і зв'язок покажчиків і елементів багатомірних масивів - що мають 2 та більше вимірів.
       Багатомірний масив у мові Сі розглядається як сукупність масивів меншої розмірності. Наприклад, двовимірний масив - це сукупність одновимірних масивів (його рядків), тривимірний масив - це сукупність матриць, матриці - сукупності рядків, а рядок - сукупність елементів одновимірного масиву.
       Елементи масивів розташовуються в ОП таким чином, що швидше змінюються самі праві індекси, тобто елементи одновимірного масиву розташовуються підряд, двовимірного - по рядках, тривимірного - по матрицях, а матриці - по рядках.
       Для звертання до елементів багатомірного масиву можна використовувати нуль і більш індексів (індексних виразів):
ім'я-масиву [вираз1][вираз2] ...
       Наприклад, для звертання:
   • до одновимірного масиву можна використовувати одно-індексний вираз (індекс);
   • до двовимірного - 1 або 2 індексний вираз;
   • до тривимірного - 1, 2 або 3 індексний вираз і т.д.
       При звертанні до багатомірних масивів одержання значення елемента масиву можливо тільки після визначення адреси елемента масиву, тобто при повній кількості індексів. При цьому обчислюються індексні вираз зліва на право, і доступу до значення виконується після обчислення останнього індексного виразу.
       Приклад оголошення двовимірного масиву значень типу int:
int а[m][n] ;
       Цей масив складається з m одновимірних масивів (рядків), у кожному з яких утримується n елементів (стовпців). При роботі з цим двовимірним масивом можна використовувати одно або 2 індексний вираз. Наприклад:
  а[i][j]- містить 2 індекси; використовується для звертання до елемента i-рядка, j-стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;
  a[i] - містить 1 індекс; визначає адресу одновимірного масиву: адреса початку i-рядка масиву;
  а - не містить індексу і визначає адресу масиву, його нульового елемента.
Таким чином, звертання до двовимірних масивів за допомогою імені і тільки одного індексу визначає покажчик на початок відповідного рядка масиву (адреса його нульового елемента). Наприклад:
а[0] == &a[0][0] == a+0*n*sizeof(int);
а[1] == &а[1][0] == a+1*n*sizeof(int);
a[i] == &a[i][0] == a+i*n*sizeof(int);
       Приклад оголошення тривимірного масиву:
int а[k][m][n] ;
де:
   •
k- кількість матриць з m рядками і n стовпцями;
   •
m - кількість рядків (одновимірних масивів) у матриці;
   •
n - кількість стовпців (елементів у рядку) матриці.
       Цей масив складається з k матриць, кожна з яких складається з m одновимірних масивів (рядків) по n елементів (стовпців). При звертанні до цього масиву можна використовувати імена:
  a[l][i][j] - містить 3 індекси; використовується для звертання до елемента l-матриці, i-рядка. j-стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;
  a[k][i] - визначає одновимірний масив - адреса початку i-рядка; k - матриці;
  a[k] - визначає двовимірний масив - адреса початку k - матриці, тобто нульового елемента його нульового рядка;
  а - адреса початку масиву, нульового елемента нульового рядка нульової матриці.
       Наприклад:
int b[3][4][5];
int i, *ip, *ipp;
i = b[0][0][1];
ip = b[2][0];
ipp = b[2];
де:
ip, ipp - покажчики на значення типу int.
       Після ip = b[2][0]; ip є покажчиком на елемент 0-рядка 0-го стовпця 2-й матриці масиву, тобто b[2][0][0].
       Після ipp = b[2]; ipp адресує 0-й рядок 2-ї матриці масиву, тобто містить адреса b[2][0][0].
       Звертання до елементів багатомірного масиву більш детально розглянемо на прикладі двовимірного масиву. Наприклад:
int а[3][4]; /* а - покажчик-константа */
int *р = а; /* р - покажчик-змінна */
       Після цього покажчик р можна використовувати замість покажчика а для звертання до рядків або елементів масиву а у вигляді: ім'я покажчика і зсув елемента щодо адреси початку масиву а.
В ОП елементи масиву а розташовуються таким чином, що швидше всіх змінюється самий правий індекс, тобто в послідовності:
а[0][0] а[0][1] а[0][2] а[0][3] а[1][0] ... а[2][2] а[2][3].
       При цьому для звертання до масиву а можна використовувати імена:
&a == а == &а[0][0] == *а - адреса а[0][0] - елемента 0-ого рядка 0-ого стовпця масиву а;
**а == *(&а[0][0]) == а[0][0] - значення елемента нульового рядка нульового стовпця масиву а;
a[i] == (а + i) == *(а + i) == &а[i][0] - адреса елемента i-рядка 0-стовпця;
*a[i] == **(а + i) == *(&а[i]) == a[i][0] - значення 0-го елемента i-рядка;
a[i][j] == *(*(а + i) + j) == *(a[i] + j) == a[i][j] - значення елемента i-рядка j-стовпця масиву а;
де:
(а + i) == *(а + i) == a[i] - адреса 0-го елемента i-рядка == &a[i][0];
(*(а + i) + j)- адреса j-елемента i-рядка = &a[i][j];
*(*(а + i) + j)- значення j-елемента i-рядка = a[i][j].
       Значення адреси початку i-рядка (адреси 0-елемента i-рядка) на машинному рівні формується у виді:
a[i] = а + i == (a+i*n*sizeof(int)), де n - кількість значень в одному рядку.
       Таким чином, адреса (i+1)-рядка відстоїть від i-рядка на (n*sizeof(int)) байтів, тобто на відстань одного рядка масиву.
       Вираз a[i][j] компілятор Сі переводить в еквівалентний вираз:
*(*а + i) + j). Зрозуміло, запис a[i][j] більш традиційний у математиці і більш наочний.
До елементів двовимірного масиву можна звернутися і за допомогою скалярного покажчика на масив. Наприклад, після оголошення:
int а[m][n], *р = а;
*(p+i*n+j) - значення j - елемента i-рядка ;
де:
n - кількість елементів у рядку;
i*n + j - змішання а[i][j]- елемента відносно початку масиву а.

1.10 Масиви покажчиків

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

1.10.1 Робота з великими масивами
       Розмір одного масиву даних повинний бути не більше 64 Кб. Але в реальних задачах можуть використовуватися масиви, що вимагають ОП, більшої ніж 64 Кб. Наприклад, масив даних типу
float з 300 рядків і 200 стовпців потребує для розміщення 300 * 200 * 4 = 240000 байтів.
Для вирішення поставленої задачі можна використовувати масив покажчиків і динамічне виділення ОП для кожного рядка матриці. Рядок матриці не повинен перевищувати 64 Кб. У вищенаведеному прикладі ОП для рядка складає всього 800 байтів. Для виділення ОП з купи кожен рядок повинний мати покажчик. Для всіх рядків масиву треба оголосити масив покажчиків, по одному для кожного рядка. Потім кожному рядку масиву виділити ОП, привласнивши кожному елементу масиву покажчиків адресу початку розміщення рядка в ОП, і заповнити цей масив.
       У запропонованому лістингу представлена програма для роботи з великим масивом цілих значень: з 300 рядків і 200 стовпців. Для розміщення він вимагає: 200 * 300 * 2 = 120000 байтів. При формуванні великого масиву використовується р - статичний масив покажчиків
       При виконанні програми перебираються i-номери рядків масиву. Для кожного рядка за допомогою функції
malloc() виконується запит ОП з купи і формується p[i] - значення покажчика на дані i-рядки. Потім перебираються i-номери рядків від 1 до 200. Для кожного рядка перебираються j-номери стовпчиків від 1 до 300. Для кожного i та j за допомогою генератора випадкових чисел формуються і виводяться *(р[i] + j) - значення елементів масиву. Після обробки масиву за допомогою функції free(p[i]) звільняється ОП виділена i-рядку масиву.
У наведеній нижче програмі використовуються звертання до
Ai,j - елементів масиву у вигляді: *(p[i]+j), де p[i] + j - адреса Ai,j-елемента масиву.
#include <conio.h>
#include <stdlib.h>
#include <stdio.h>
void main()
{
    int *p[200], i, j;
    clrscr();
    randomize();
    for (i=0;i<200;i++)
    /* Запит ОП для рядків великого масиву: */
    p[i] = (int*) malloc (300 * sizeof (int));
    for (i = 0; i < 200; i++)
        for (j = 0; j < 300; j++ )
        {
            *(p[i] + j ) = random(100);
            printf("%3d", *(p[i] + j ));
            if ( (j + 1) % 20 == 0 )
                printf ("\n" ) ;
        }
        /* Звільння ОП рядків великого масиву: */
        for ( i=0; i < 200; i++ )
            free( p[i] );
}

У програмі використовується
р - масив покажчиків.

1.10.2 Вільні масиви та покажчики
    Термін "вільний" масив відносять до двовимірних масивів. Вони можуть бути будь-якого типу, у тому числі int, float, char і типу структура. Вільний масив - це двовимірний масив, у якому довжини його рядків можуть бути різними. Для роботи з вільними масивами використовуються масиви покажчиків, що містять в собі кількість елементів, рівну кількості рядків вільного масиву. Кожен елемент масиву покажчиків містить адресу початку рядка значень вільного масиву. ОП виділяється для кожного рядка вільного масиву, наприклад за допомогою функції
malloc(), і звільняється функцією free(). Для того щоб виконати функцію malloc(), треба визначити кількість елементів у рядку, наприклад із вводу користувача або яким-небудь іншим способом. У нульовому елементі кожного рядка вільного масиву зберігається число, рівне кількості елементів даного рядка Дані в кожен рядок можуть вводитися з файлу або з клавіатури в режимі діалогу. Приклад вільного масиву цілих чисел приведений на рис 1.12:


    У масиві на рис. 1.12 три рядки; у нульовому стовпці кожного рядка стоїть кількість елементів даного рядка. Далі - значення елементів матриці.
Приклад оголошення вільного масиву цілих, тобто статичного масиву покажчиків на дані типу
int:
int *а[100];
    Для масиву а приділяється ОП для 100 покажчиків на значення цілого типу, по одному покажчику на кожний з 100 рядків вільного масиву. Після визначення кількості елементів рядка для значень рядка повинна бути виділена ОП і сформоване значення покажчика в змінній
a[i]. Цей покажчик посилається на область ОП, виділену для значень і-рядка матриці. Тільки після цього можна заносити в цю ОП значення елементів вільного масиву.
    Реально ОП - це лінійна послідовність перенумерованих байтів. Елементи рядків вільного масиву можуть бути розташовані підряд або несуміжними відрізками ОП, виділеними для рядків.

1.11 Символьні рядки

1.11.1 Основні відомості про представлення рядків
       Символьний рядок представляє собою набір з одного або більше символів.
       Приклад : "Це рядок"
       В мові Сі немає спеціального типу даних, який можна було б використовувати для опису рядків. Замість цього рядки представляються у вигляді масиву елементів типу char. Це означає, що символи рядка розташовуються в пам'яті в сусідніх комірках, по одному символу в комірці.

       Необхідно відмітити, що останнім елементом масиву є символ '\0'. Це нульовий символ (байт, кожний біт якого рівний нулю). У мові Сі він використовується для того, щоб визначати кінець рядка.
       Примітка. Нульовий символ - це не цифра 0; він не виводиться на друк і в таблиці символів ASCII (див. додаток) має номер 0. Наявність нульового символу передбачає, що кількість комірок масиву повинна бути принаймні на одну більше, ніж число символів, які необхідно розміщувати в пам'яті. Наприклад, оголошення
       char str[10];
передбачає, що рядок містить може містити максимум 9 символів.
Основні методи ініціалізації символьних рядків.
      char str1[]= "ABCdef";
      char str2[]={'A', 'B', 'C', 'd', 'e', 'f',0};
      char str3[100];
       gets(str3);
      char str4[100];
       scanf("%s",str4);
       Усі константи-рядки в тексті програми, навіть ідентично записані, розміщуються за різними адресами в статичній пам'яті. З кожним рядком пов'язується сталий покажчик на його перший символ. Власне, рядок-константа є виразом типу "покажчик на char" зі сталим значенням - адресою першого символу.
       Так, присвоювання p="ABC" (p - покажчик на char) встановлює покажчик p на символ 'A'; значенням виразу *("ABC"+1) є символ 'B'.
       Елементи рядків доступні через покажчики на них, тому будь-який вираз типу "покажчик на char" можна вважати рядком.
       Необхідно мати також на увазі те, що рядок вигляду "х" - не те ж саме, що символ 'x'. Перша відмінність : 'x' - об'єкт одного з основних типів даних мови Сі (char), в той час, як "х" - об'єкт похідного типу (масиву елементів типу char). Друга різниця : "х" насправді складається з двох символів - символу 'x' і нуль-символу.

1.11.2 Функції роботи з рядками

       1. Функції введення рядків.
       Прочитати рядок із стандартного потоку введення можна за допомогою функції gets(). Вона отримує рядок із стандартного потоку введення. Функція читає символи до тих пір, поки їй не зустрінеться символ нового рядка '\n', який генерується натисканням клавіші ENTER. Функція зчитує всі символи до символу нового рядка, додаючи до них нульовий символ '\0'.
Синтаксис :
    char *gets(char *buffer);
       Як відомо, для читання рядків із стандартного потоку введення можна використовувати також функцію scanf() з форматом %s. Основна відмінність між scanf() і gets() полягає у способі визначенні досягнення кінця рядка; функція scanf() призначена скоріше для читання слова, а не рядка. Функція scanf() має два варіанти використання. Для кожного з них рядок починається з першого не порожнього символу. Якщо використовувати %s, то рядок продовжується до (але не включаючи) наступного порожнього символу (пробіл, табуляція або новий рядок). Якщо визначити розмір поля як %10s, то функція scanf() не прочитає більше 10 символів або ж прочитає послідовність символів до будь-якого першого порожнього символу.

       2. Функції виведення рядків.
       Тепер розглянемо функції виведення рядків. Для виведення рядків можна використовувати функції puts() і printf().
Синтаксис функції puts():
       int puts(char *string);
       Ця функція виводить всі символи рядка string у стандартний потік виведення. Виведення завершується переходом на наступний рядок.
       Різниця між функціями puts() і printf() полягає в тому, що функція printf() не виводить автоматично кожний рядок з нового рядка.

       Стандартна бібліотека мови програмування Сі містить клас функцій для роботи з рядками, і всі вони починаються з літер str. Для того, щоб використовувати одну або декілька функції необхідно підключити файл string.h.
       #include<string.h>
       
       3. Визначення довжини рядка. Для визначення довжини рядка використовується функція strlen(). Її синтаксис :
       size_t strlen(const char *s);
       Функція strlen() повертає довжину рядка s, при цьому завершуючий нульовий символ не враховується.
Приклад :
char *s= "Some string";
int len;
Наступний оператор встановить змінну len рівною довжині рядка, що адресується покажчиком s:
len = strlen(s); /* len == 11 */

       4. Копіювання рядків. Оператор присвоювання для рядків не визначений. Тому, якщо s1 і s2 - символьні масиви, то неможливо скопіювати один рядок в інший наступним чином.
char s1[100];
char s2[100];
s1 = s2; /* помилка */
       Останній оператор (s1=s2;) не скомпілюється.
       Щоб скопіювати один рядок в інший необхідно викликати функцію копіювання рядків strcpy(). Для двох покажчиків s1 і s2 типу char * оператор
strcpy(s1,s2);
       копіює символи, що адресуються покажчиком s2 в пам'ять, що адресується покажчиком s1, включаючи завершуючі нулі.
       Для копіювання рядків можна використовувати і функцію strncpy(), яка дозволяє обмежувати кількість символів, що копіюються.
strncpy(destantion, source, 10);
       Наведений оператор скопіює 10 символів із рядка source в рядок destantion. Якщо символів в рядку source менше, ніж вказане число символів, що копіюються, то байти, що не використовуються встановлюються рівними нулю.
       Примітка. Функції роботи з рядками, в імені яких міститься додаткова літера n мають додатковий числовий параметр, що певним чином обмежує кількість символів, з якими працюватиме функція.

       5. Конкатенація рядків.
       Конкатенація двох рядків означає їх об'єднання, при цьому створюється новий, більш довгий рядок. Наприклад, при оголошенні рядка
char first[]= "Один ";
       оператор
strcat(first, "два три чотири!");
       перетворить рядок first в рядок "Один два три чотири".
       При викликанні функції strcat(s1,s2) потрібно впевнитися, що перший аргумент типу char * ініціалізований і має достатньо місця щоб зберегти результат. Якщо s1 адресує рядок, який вже записаний, а s2 адресує нульовий рядок, то оператор
strcat(s1,s2);
       перезапише рядок s1, викликавши при цьому серйозну помилку.
       Функція strcat() повертає адресу рядка результату (що співпадає з її першим параметром), що дає можливість використати "каскад" декількох викликів функцій :
strcat(strcat(s1,s2),s3);
       Цей оператор додає рядок, що адресує s2, і рядок, що адресує s3, до кінця рядка, що адресує s1, що еквівалентно двом операторам:
strcat(s1,s2);
strcat(s1,s3);
       Повний список прототипів функцій роботи з рядками можна знайти в додатках на стор.

       6. Порівняння рядків.
       Функція strcmp() призначена для порівняння двох рядків. Синтаксис функції :
int strcmp(const char *s1, const char*s2);
       Функція strcmp() порівнює рядки s1 і s2 і повертає значення 0, якщо рядки рівні, тобто містять одне й те ж число однакових символів. При порівнянні рядків ми розуміємо їх порівняння в лексикографічному порядку, приблизно так, як наприклад, в словнику. У функції насправді здійснюється посимвольне порівняння рядків.
       Кожний символ рядка s1 порівнюється з відповідним символом рядка s2. Якщо s1 лексикографічно більше s2, то функція strcmp() повертає додатне значення, якщо менше, то - від'ємне.

1.12 Основні методи сортування масивів

      Починаючи з даного розділу, розглянемо декілька методів впорядкування елементів масиву, які широко використовуються у практичному програмуванні.

1.12.1 Метод бульбашкового сортування
      Метод "бульбашкового сортування" ґрунтується на перестановці сусідніх елементів. Для впорядкування елементів масиву здійснюються повторні проходи по масиву.
      Переміщення елементів масиву здійснюється таким чином : масив переглядається зліва направо, здійснюється порівняння пари сусідніх елементів; якщо елементи в парі розміщені в порядку зростання, вони лишаються без змін, а якщо ні - міняються місцями.
      В результаті першого проходу найбільше число буде поставлено в кінець масиву. У другому проході такі операції виконуються над елементами з першого до (N-1)-ого, у третьому - від першого до (N-2)-ого і т.д. Впорядкування масиву буде закінчено, якщо при проході масиву не виконається жодної перестановки елементів масиву. Факт перестановки фіксується за допомогою деякої змінної (у наступному прикладі -
is), яка на початку має значення 0 і набуває значення 1 тоді, коли виконається перестановка в якій-небудь парі.

Масив до впорядкування

22

20

-1

-40

88

-75

-22

Перший перегляд масиву

20

-1

-40

22

-75

-22

88

Другий перегляд масиву

-1

-40

20

-75

-22

22

88

Третій перегляд масиву

-40

-1

-75

-22

20

22

88

Четвертий перегляд масиву

-40

-75

-22

-1

20

22

88

П'ятий перегляд масиву

-75

-40

-22

-1

20

22

88

Рис. 1.15. Бульбашкове сортування

const n=10;
int a[n], i, c, is;
/* … */
do {
  is=0;
  for (i=1;i<n;i++)
    if (a[i-1]>a[i])
    {
       c=a[i];
       a[i]=a[i-1];
       a[i-1]=c;
       is=1;
    }
  } while (is);

1.12.2 Сортування методом вибору
  Даний метод сортування передбачає наступні дії : масив переглядається перший раз, знаходиться мінімальний елемент цього масиву, який міняється місцями з першим елементом. Другий раз масив переглядається, починаючи з другого елементу. Знову знаходиться мінімальний елемент, який міняється місцями з другим елементом масиву.
  Даний процес виконується до тих пір, поки не буде поставлено на місце N-1 елемент.

Масив до впорядкування

22

20

-1

-40

88

-75

-22

Перший перегляд масиву

-75

20

-1

-40

88

22

-22

Другий перегляд масиву

-75

-40

-1

20

88

22

-22

Третій перегляд масиву

-75

-40

-22

20

88

22

-1

Четвертий перегляд масиву

-75

-40

-22

-1

88

22

20

П'ятий перегляд масиву

-75

-40

-22

-1

20

22

88

Шостий перегляд масиву

-75

-40

-22

-1

20

22

88

Рис. 1.16. Сортування методом вибору

const int n=20;
int b[n];
int imin, i, j, a;
/* … */
for (i=0;i<n-1;i++)
{
  imin=i;
  for (j=i+1;j<n;j++)
    if (b[j]<b[imin]) imin=j;
    a=b[i];
    b[i]=b[imin];
    b[imin]=a;
}

1.12.3 Сортування вставками
  Даний метод сортування називається сортування вставками, так як на і-му етапі відбувається "вставка" і-ого елемента a[i] в потрібну позицію серед елементів a[1], a[2], …, a[i-1], які вже впорядковані. Після цієї вставки перші і елементів будуть впорядковані.
  Саме таким способом звичайно сортують карти, тримаючи в лівій руці вже впорядковані карти, і взявши правою рукою чергову карту вставляють її в потрібне місце, порівнюючи її з іншими проходячи справа наліво.

Масив до впорядкування

22

20

-1

-40

88

-75

-22

Перший перегляд масиву

20

22

-1

-40

88

-75

-22

Другий перегляд масиву

-1

20

22

-40

88

-75

-22

Третій перегляд масиву

-40

-1

20

22

88

-75

-22

Четвертий перегляд масиву

-40

-1

20

22

88

-75

-22

П'ятий перегляд масиву

-75

-40

-1

20

22

88

-22

Шостий перегляд масиву

-75

-40

-22

-1

20

22

88

Рис. 1.17. Сортування вставками

  Реалізувати сортування масиву вставками можна так :

const int n=20;
int b[n];
int i,j,c;
/* … */
for (i=1;i<n;i++)
{
  c=a[i];
  for (j=i-1;j>=0&&a[j]>c;j--)
    a[j+1]=a[j];
  a[j+1]=c;
}

1.12.4 Швидке сортування
  Швидке сортування полягає в тому, що множина елементів В { k1, k2, …, kn } перетворюється на множину B1, {k1}, B2, де В1 - підмножина В з елементами, не більшими за k1, а В2 - підмножина В з елементами більшими k1. Причому елемент k1 після розбиття множини В буде перебувати на потрібному місці. Далі до множин B1 і B2 знову застосовують впорядкування швидким сортуванням.
  Час роботи алгоритму швидкого сортування в гіршому випадку складає О(n2), але на практиці цей алгоритм виявляється одним із найшвидших.

double * quick(double *s,int low,int hi)
{
  double cnt,aux;
  int i,j;
  if (hi>low)
  {
    i=low;
    j=hi;
    cnt=s[i];
    while(i < j)
    {
      if (s[i+1]<=cnt)
      {
        s[i]=s[i+1];
        s[i+1]=cnt;
        i++;
      }
      else
      {
        if (s[j]<=cnt)
        {
          aux=s[j];
          s[j]=s[i+1];
          s[i+1]=aux;
        }
        j--;
      }
    }
  quick(s,low,i-1);
  quick(s,i+1,hi);
  }
  return(s);
}

1.13 Структури


1.13.1 Оголошення структури
       Структури дозволяють об'єднувати в єдиному об'єкті сукупність значень, які можуть мати різні типи. Оголошення структури здійснюється за допомогою ключового слова struct.
Синтаксис опису структури виглядає так :
struct [ім'я_структури]
{
       тип1 елемент1;
       тип2 елемент2;
       ........................
       типN елементN;
} [список описів];
       З метою ознайомлення з цим типом даних розглянемо найпростіший приклад представлення поняття "дата", що складається з декількох частин: число (день, місяць, рік), назва тижня та місяця:
struct date {
     int day ;
     int month ;
     int year;
     char day_name[15];
     char mon_name[14];
} arr[100],*pd,data,new_data;
       В даному прикладі оголошуються:
data, new_data - змінні типу структури date;
pd - покажчик на тип data
arr - масив із 100 елементів, кожний елемент якого має тип date.
       Можливий і наступний опис структури з використанням typedef:
typedef struct mystruct {
       int year;
       char size;
       float field;
} MYSTRUCT;
MYSTRUCT s; /* те саме, що й struct mystruct s; */
       Пам'ять розподіляється у структурі покомпонентно, зліва-направо, від молодших до старших адрес пам'яті (рис. 1.18).
typedef struct dataTypes {
       float aFloat;
       int anInt;
       char aString[8];
       char aChar;
       char aLong;
} DataTypes;
DataTypes data;

       Потрібно відзначити, що на відміну від описів інших типів даних, опис структури не виділяє місця у пам'яті під елементи структури. Її опис визначає лише так званий шаблон, що описує характеристики змінних, що будуть розміщуватися у конкретній структурі. Щоб ввести змінні та зарезервувати для них пам'ять необхідно або після фігурної дужки, що завершує опис структури, вказати список ідентифікаторів, як це зроблено у вищенаведеному прикладі, або окремо оголосити змінні типу, як ми це робимо у звичайних випадках.
       Доступ до окремого елемента структури забезпечується операторами вибору: . (прямий селектор) та -> (непрямий селектор), наприклад,
struct mystruct {
       int i;
       char str[21];
       double d;
} s,*sptr=&s;
s.i =3;
sptr->d = 1.23;
       Ініціалізація структури подібна до тієї, що у масивах, але з урахуванням розміщення даних різного типу.
struct person {
       char frnm[20];
       char nm[30];
       int year;
       char s;
};
struct person poet={"Taras", "Shevtchenko",1814, 'M'},classics[]={{"Alfred", "Aho", 1939, 'M'},{"Seimour", "Ginzburg",}, /* … */ {"Jeffrey", "Ulman", 1938, 'M'}};
       У вищенаведеному прикладі ініціалізується змінна poet і масив структур classics. Значення classics[1].year і classics[1].s мають значення відповідно 0 і '\0'.
       Для змінних одного і того ж самого структурного типу визначена операція присвоювання, при цьому здійснюється поелементне копіювання значень полів.
struct date {
       int day ;
       int month ;
       int year;
       char day_name[15];
       char mon_name[14];
} data,new_data;
/* ... */
data=new_data;
       Але, для порівняння структур необхідно перевіряти рівність відповідних полів цих структур.
struct point
{
       float x,y;
       char c;
} point1,point2;
if (point1.x==point2.x&&point1.y==point2.y&&point1.c==point2.c)
{
       /* … */
};
       Звертання до окремих елементів структури теж не викликає труднощів:
data.year=2005;
printf("%d-%d-%d",data.day,data.month,data.year);
scanf("%d",data.day);
gets(arr[0].day_name);
       Доцільним та корисним є зв'язок структур та покажчиків, який дозволяє обійти деякі складні моменти. Так опис date *pdate утворить покажчик на структуру типу date. Використовуючи цей покажчик, можна звернутися до будь-якого елемента структури шляхом застосування операції -> , тобто date ->year , або що еквівалентно операції (*pdate).year. Однак слід зауважити, що спільне використання цих типів потребує від програміста достатньо високої кваліфікації, аби використовувати можливості найбільш ефективно та безпомилково.
Приклад 1.
#include<stdio.h>
#include<conio.h>
#define MAXTIT 41
#define MAXAUT 31
struct book
{
       char title[MAXTIT];
       char author[MAXAUT];
       float value;
};
void main()
{
       struct book libry;
       printf("Введiть назву книги.\n");
       gets(libry.title);
       printf("Тепер введiть прiзвище автора.\n");
       gets(libry.author);
       printf("Тепер введiть цiну.\n");
       scanf("%f",&libry.value);
       printf("\n%s '%s',%g grn.\n",libry.author,
       libry.title,libry.value);
};
Кожний опис структури вводить унікальний тип структури, тому в наступному фрагменті програми:
struct A {
       int i,j;
       double d;
} a, a1;
struct B {
       int i,j;
       double d;
} b;
об'єкти a і a1 мають однаковий тип struct A, але об'єкти a і b мають різні типи структури. Структурам можна виконувати присвоювання тільки в тому випадку якщо і вихідна структура, і структура, які присвоюється мають один і той же тип.
a = a1; /*можна виконати, так як a і a1 мають однаковий тип */
a = b; /* помилка */

1.13.2 Масиви структур
       Як і звичайними масивами простих типів, так само можна оперувати масивами структур, елементи якого мають структурований тип. Розглянемо наочний зразок, який ілюструє оголошення масиву структур:
typedef struct Date
{
       int d; /* день */
       int m; /* мiсяць */
       int y; /* рiк */
} Date;
Date arr[100];
       Вище було оголошено масив arr, що складається із 100 елементів, кожний з яких має тип Data. Кожний елемент масиву - це окрема змінна типу Data, що складається із трьох цілих елементів - d, m, y.
        Доступ до полів структури аналогічний доступу до звичайних змінних, плюс використання індексу номеру елементу у квадратних дужках:
arr[25].d=24;
arr[12].m=12;
       Запропонуємо програму, в якій реалізується концепція структурованого типу Data. Окремими функціями реалізуємо ініціалізацію елементів структури, додавання нового значення, виведення дати на екран, визначення високосного року.
#include<stdio.h>
#include<conio.h>
typedef struct Date
{
       int d; /* день */
       int m; /* мiсяць */
       int y; /* рiк */
} Date;
void set_date_arr(Date *arr,Date value,int n)
{
       int i;
       for (i=0;i<n;i++)
       {
              arr[i].d=value.d;
              arr[i].m=value.m;
              arr[i].y=value.y;
       }
}

void print_date_arr(Date *arr,int n)
{
       int i;
       for (i=0;i<n;i++)
       {
              printf("%d.%d.%d\n",arr[i].d,arr[i].m,arr[i].y);
       }
}

void print_date(Date &d)
/* виведення на екран дати */
{
       printf("%d.%d.%d\n",d.d,d.m,d.y);
}

void init_date(Date &d,int dd,int mm,int yy)
/* iнiцiалiзацiя структури типу Date */
{
       d.d=dd;
       d.m=mm;
       d.y=yy;
}

int leapyear(int yy)
/* визначення, чи високосний рiк */
{
       if ((yy%4==0&&yy%100!=0)||(yy%400==0)) return 1;
              else return 0;
}

void add_year(Date &d,int yy)
/* додати yy рокiв до дати */
{
       d.y+=yy;
}

void add_month(Date &d,int mm)
/* додати mm мiсяцiв до дати */
{
       d.m+=mm;
       if (d.m>12)
       {
              d.y+=d.m/12;
              d.m=d.m%12;
       }
}

void add_day(Date &d,int dd)
/* додати dd днiв до дати */
{
       int days[]={31,28,31,30,31,30,31,31,30,31,30,31};
       d.d+=dd;
       if (leapyear(d.y)) days[1]=29;
       while ((d.d>days[d.m-1]))
       {
              if (leapyear(d.y)) days[1]=29;
                     else days[1]=28;
              d.d-=days[d.m-1];
              d.m++;
              if (d.m>12)
              {
                     d.y+=d.m%12;
                     d.m=d.m/12;
              }
       }
}

void main(void)
{
       Date date1,date2;
       Date array[10]={{12,11,1980},{15,1,1982},{8,6,1985},{8,8,1993},{20,12,2002},{10,1,2003}};
       clrscr();
       init_date(date1,15,12,2002);
       add_day(date1,16);
       print_date(date1);
       puts("");
       init_date(date2,1,1,2003);
       add_month(date2,10);
       print_date(date2);
       puts("");
       print_date_arr(array,6);
}

1.13.3 Бітові поля
       Бітові поля (bit fields) - особливий вид полів структури. Вони дають можливість задавати кількість бітів, в яких зберігаються елементи цілих типів. Бітові поля дозволяють раціонально використовувати пам'ять за допомогою зберігання даних в мінімально потрібній кількості бітів.
       При оголошенні бітового поля вслід за типом елемента ставиться двокрапка (:) і вказується цілочисельна константа, яка задає розмір поля (кількість бітів). Розмір поля повинен бути константою в діапазоні між 0 і заданим загальним числом бітів, яке використовується для зберігання даного типу даних.
struct bit_field {
       int bit_1 : 1;
       int bits_2_to_5 : 4;
       int bit_6 : 1;
       int bits_7_to_16 : 10;
} bit_var;

1.14 Об'єднання (union)

        Об'єднання дозволяють в різні моменти часу зберігати в одному об'єкті значення різного типу. В процесі оголошення об'єднання з ним асоціюється набір типів, які можуть зберігатися в даному об'єднанні. В кожний момент часу об'єднання може зберігати значення тільки одного типу з набору. Контроль за тим, значення якого типу зберігається в даний момент в об'єднанні покладається на програміста.
Синтаксис :
union [ім'я_об'єднання]
{
        тип1 елемент1;
        тип2 елемент2;
        ........................
        типN елементN;
} [список описів];

        Пам'ять, яка виділяється під змінну типу об'єднання, визначається розміром найбільш довгого з елементів об'єднання. Всі елементи об'єднання розміщуються в одній і тій же області пам'яті з однієї й тієї ж адреси. Значення поточного елемента об'єднання втрачається, коли іншому елементу об'єднання присвоюється значення.
Приклад 1:
union sign
{
    int svar;
    unsigned uvar;
} number;

Приклад 2 :
union
{
    char *a,b;
    float f[20];
} var;

        В першому прикладі оголошується змінна типу об'єднання з ім'ям
number. Список оголошень елементів об'єднання містить дві змінні : svar типу int і uvar типу unsigned. Це об'єднання дозволяє запам'ятати ціле значення в знаковому або в без знаковому вигляді. Тип об'єднання має ім'я sign.
        В другому прикладі оголошується змінна типу об'єднання з ім'ям
var. Список оголошень елементів містить три оголошення : покажчика a на значення типу char, змінної b типу char і масиву f з 20 елементів типу float. Тип об'єднання не має імені. Пам'ять, що виділяється під змінну var, рівна пам'яті, необхідної для зберігання масиву f, так як це найдовший елемент об'єднання.

1.15 Файлові потоки


      В мові Сі та Сі++ файл розглядається як потік (stream), що представляє собою послідовність байтів, що записуються чи зчитуються. При цьому потік "не знає", що і в якій послідовності в нього записано. Розшифровка змісту написаних у ньому байтів лежить на програмі.

Таблиця 1.13. Значення аргументу mode функції fopen()

"r"

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

"w"

створення нового файлу тільки для запису, якщо файл із вказаним ім'ям вже існує, то він перезапишеться.

"a"

відкриття файлу тільки для додавання інформації в кінець файлу, якщо файл не існує, він створюється.

"r+"

відкриття існуючого файлу для читання та запису.

"w+"

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

"a+"

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

      Класичний підхід, прийнятий в Сі, полягає в тому, що інформація про потік заноситься в структуру FILE, яка визначена у файлі stdio.h. Файл відкривається за допомогою функції fopen, яка повертає покажчик на структуру типу FILE.
typedef struct
{
      short level; /*рівень буферу*/
      unsigned flags; /*статус файлу */
      char fd; /*дескриптор файла*/
      char hold; /*попередній символ,якщо немає буферу*/
      short bsize; /*розмір буферу*/
      unsigned char *buffer; /*буфер передавання даних*/
      unsigned char *curp; /*поточний активний покажчик*/
      short token; /*перевірка коректності*/
} FILE;

      Синтаксис функції
fopen() :
FILE *fopen(const char *filename, const char *mode);
      Дана функція відкриває файл із заданим ім'ям і зв'язує з ним потік. Аргумент mode вказує режим відкриття файла (таблиця 1.13).
      До вказаних специфікаторів в кінці або перед символом
"+" може додаватися символ "t" (текстовий файл), або "b" (бінарний, двійковий файл).

1.15.1 Текстові файли
Розглянемо спочатку роботу з текстовими файлами. Відкриття текстового файлу test.txt може мати вигляд :
#include<stdio.h>
void main()
{
      …
      FILE *f;
      if ((f=fopen("test.txt", "rt"))==NULL)
      {
            printf("Файл не вдалося відкрити.\n");
            return;
      }
      …
      fclose(f);
      …
}

      В даному прикладі змінна
f зв'язується з файлом "test.txt", який відкривається як текстовий тільки для читання.
      З відкритого таким чином файлу можна читати інформацію. Після закінчення роботи з файлом, його необхідно закрити за допомогою функції
fclose().
      Якщо файл відкривався би за допомогою
fopen("test.txt", "rt+"); , то можна було б не тільки читати, але й записувати в нього інформацію.
      З текстового файла можна читати інформацію по рядках, по символах або за форматом.
      Записування символу в файловий потік здійснюється функцією
putc().
int putc(int ch, FILE *f);
      Читання рядка здійснюється за допомогою функції
fgets().
char *fgets(char *s,int n,FILE *stream);
      У виклику функції
fgets() : s - покажчик на буфер, в який читається рядок, n - кількість символів. Читання символу в рядок проходить або до появи символу кінця рядка "\n", або читається n-1 символ. В кінці прочитаного рядка записується нульовий символ.
#include<stdio.h>
#include<string.h>
void main()
{
      char s[80];
      FILE *f;
      if ((f=fopen("1.cpp", "rt"))==NULL)
      {
          printf("There are an error\n");
          return;
      }
      do
      {
          fgets(s,80,f);
          printf("%s",s);
      } while (!feof(f));
      fclose(f);
}

      Функція
feof() перевіряє, чи не прочитаний символ завершення файла. Якщо такий символ прочитаний, то feof() повертає ненульове значення і цикл завершується.
      Читання з текстового файлу форматованих даних може здійснюватися функцією
fscanf(). Синтаксис :
int fscanf(FILE *stream, const char *format[, address, …]);
      Параметр
format визначає рядок форматування аргументів, які задаються своїми адресами.
      При форматованому читанні можуть виникати помилки у зв'язку з досягненням завершення файлу або невірним форматом записаних у файлі даних. Перевірити, чи успішно пройшло читання даних можна за значенням, яке повертає функція
fscanf(). При успішному читанні вона повертає кількість прочитаних полів. Тому читання даних можна організовувати наступним чином :
if (fscanf(f,"%d%d%d",&a,&b,&c)!=3)
{
      printf("Помилка читання!\n");
};
      Існує також і ряд функцій для запису даних у текстовий файл. Найчастіше використовуються функції fgetc(), fputs() та fprintf().
      Функція
fgetc() використовується для читання чергового символу з потоку, відкритого функцією fopen().
int fgetc(FILE *f);
      Синтаксис функції
fprintf() :
int fprintf(FILE *stream, const char *format[,argument,…]);
      Вона працює майже мак само, як і функція
printf(), але їй потрібний додатковий аргумент для посилання на файл. Він є першим у списку аргументів. Наводимо приклад, який ілюструє звертання до наведених вище функцій:
#include<stdio.h>
void main()
{
      FILE *fi;
      int age;
      fi=fopen("age.txt","r"); /* відкриття файла для читання */
      fscanf(fi,"%d",&age); /*читання з файла числового значення */
      fclose(fi); /* закриття файла */
      fi=fopen("data.txt", "a"); /* відкриття файла для додавання інформації в кінець */
      fprintf(fi, "Age==%d.\n",age); /* запис рядка в файл */
      fclose(fi); /* закриття файла */
}

1.15.2 Двійкові файли
      Тепер розглянемо роботу з двійковими файлами. Двійковий файл представляє собою просто послідовність символів. Що саме і в якій послідовності зберігається в двійковому файлі - повинна знати програма.
      Двійкові файли мають переваги, порівняно з текстовими при зберіганні числових даних. Операції читання і запису з такими файлами виконуються набагато швидше, ніж з текстовими, так як відсутня необхідність форматування (переведення в текстове представлення та навпаки). Двійкові файли зазвичай мають менший розмір, ніж аналогічні текстові файли. В двійкових файлах можна переміщуватися в будь-яку позицію і читати або записувати дані в довільній послідовності, в той час, як в текстових файлах практично завжди виконується послідовна обробка інформації.
      Про те, як відкривати двійкові файли було згадано раніше. Запис і читання в двійкових файлах виконується відповідно функціями
fwrite і fscanf.
size_t fwrite(const void *ptr, size_t size, size_t n, FILE*stream);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
      В обидві функції повинен передаватися покажчик ptr на дані, які вводяться або виводяться. Параметр size задає розмір в байтах даних, які читаються або записуються.
#include<stdio.h>
#include<conio.h>
struct mystruct
{
      int i;
      char ch;
};
int main(void)
{
      FILE *stream;
      struct mystruct s;
      if ((stream = fopen("test.txt", "wb")) == NULL)
      {
            fprintf(stderr, "Неможливо відкрити файл\n");
            return 1;
      }
      s.i = 0;
      s.ch = 'A';
      fwrite(&s, sizeof(s), 1, stream);
      fclose(stream);
      return 0;
}

      Тепер розглянемо особливості записування і читання рядків.
char s[10];
strcpy(s, "Example");

fwrite(s,strlen(s)+1,sizeof(char),stream);
      Записування рядків відбувається посимвольно. В даному прикладі число символів, які записуються - strlen(s)+1 (одиниця додається на нульовий символ в кінці). Читається рядок аналогічно:
fread(s,strlen(s)+1,sizeof(char),stream);
      При цьому читання проходить теж посимвольно.
Дуже часто доводиться працювати з рядками різних довжин. В таких випадках можна перед рядком записати у файл ціле число, яке рівне числу символів у рядку.

int i=strlen(s)+1;
fwrite(&i,1,sizeof(int),stream);
fwrite(s,i,1,stream);

fread(&i,1,sizeof(int),stream);
fread(s,i,1,stream)
      В усіх наведених вище прикладах читання даних проходило послідовно. Але, працюючи з двійковими файлами, можна організувати читання даних в довільному порядку. Для цього використовується "покажчик файла" (курсор), який визначає поточну позицію у файлі. При читанні даних курсор автоматично зміщується на число прочитаних байтів. Отримати поточну позицію курсору файла можна за допомогою функції ftell().
long ftell(FILE *stream);
      А встановлюється поточна позиція курсору у файлі за допомогою функції
fseek():
int fseek(FILE *stream, long offset, int whence);
      Ця функція задає зміщення на число байтів
offset від точки відліку, яка визначається параметром whence. Цей параметр може приймати значення 0, 1, 2 (таблиця 1.14).

Таблиця 1.14. Можливі значення параметра whence функції fseek

Константа

whence

Точка відліку

SEEK_SET

0

Початок файлу

SEEK_CUR

1

Поточна позиція

SEEK_END

2

Кінець файлу

      Якщо задане значення whence=1, то offset може приймати як додатне, так і від'ємне значення, тобто зсув вперед або назад.
      Функція
rewind() переміщує курсор на початок файлу.
void rewind(FILE *stream);
      Те ж саме можна зробити за допомогою функції
fseek() :
fseek(stream, 0L, SEEK_SET);
Приклад програми, в якій використовуються описані вище функції :
#include <stdio.h>
long filesize(FILE *stream);
int main(void)
{
     FILE *stream;
     stream = fopen("test.txt", "w+");
     fprintf(stream, "This is a test");
     printf("Розмір файла test.txt рівний %ld байт\n",
     filesize(stream));
     fclose(stream);
     return 0;
}
long filesize(FILE *stream)
{
     long curpos, length;
     curpos = ftell(stream);
     fseek(stream, 0L, SEEK_END);
     length = ftell(stream);
     fseek(stream, curpos, SEEK_SET);
     return length;
}

1.15.3 Використання дескрипторів файлів
      В мові Сі передбачений ще один механізм роботи з файлами - використання дескрипторів. Файли, які відкриваються таким чином не розраховані на роботу з буферами та форматованими даними.
      На початку роботи будь-якої програми відкриваються п'ять стандартних потоків зі своїми дескрипторами.

Таблиця 1.15. Дескриптори стандартних потоків введення-виведення

потік

дескриптор

 

stdin

0

стандартний вхідний потік

stdout

1

стандартний вихідний потік

stderr

2

стандартний потік повідомлень про помилки

stdaux

3

стандартний потік зовнішнього пристрою

stdprn

4

стандартний потік виведення на принтер

      Але будь-яка програма може і явним чином відкривати будь-які файли з дескрипторами.
      Функції, які працюють з дескрипторами файлів, описані в модулі
io.h.
      Файли відкривається функцією
open(), яка повертає дескриптор файлу:
int open(const char *path, int access [ , unsigned mode ] );
      Параметр
path задає ім'я файлу відкриття. Параметр access визначає режим доступу до файлу.
      
mode є не обов'язковим та задає режим відкриття файла.
Параметр
access формується за допомогою операції АБО (|) з переліку прапорців.

O_RDONLY

тільки для читання

O_WRONLY

тільки для запису

O_RDWR

для читання і запису

O_CREAT

створення нового файлу

O_TRUNC

якщо файл існує, то він стає порожнім

O_BINARY

двійковий файл

O_TEXT

текстовий файл

      Параметр mode може приймати наступні значення

S_IWRITE

дозволити запис

S_IREAD

дозволити читання

Використання функції fopen() демонструє наступний приклад :
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
int main(void)
{
      int handle;
      char msg[] = "Hello world";
      if ((handle = open("TEST.TXT", O_CREAT | O_TEXT)) == -1)
      {
          perror("Error:");
          return 1;
      }
      write(handle, msg, strlen(msg));
      close(handle);
      return 0;
}

      Як видно з прикладу, файл, відкритий функцією
open() повинен бути закритий за допомогою функції close().
int close(int handle);
      Читання і запис даних при роботі з файлами, що визначаються дескрипторами
handle, здійснюється функціями write() і read().
int read(int handle, void *buf, unsigned len);
int write(int handle, void *buf, unsigned len);
      В наведених функціях buf - покажчик на буфер, з якого записується в файл інформація, або в який читається len байтів з файла.

      
Буферизація потоків. В мові Сі існує ряд функцій, які дозволяють керувати буферизацією потоків.
      Функція
setbuf() дозволяє користувачу встановлювати буферизацію вказаного потоку stream. Синтаксис функції setbuf():
void setbuf(FILE *stream, char *buf);
      Значення аргументу stream повинне відповідати стандартному або вже відкритому потоку.
      Якщо значення аргументу
buffer рівне NULL, то буферизацію буде відмінено. Інакше, значення аргументу buffer буде визначати адресу масиву символів довжини BUFSIZ, де BUFSIZ - розмір буфера (константа, визначена в stdio.h).
      Визначений користувачем буфер використовується для буферизованого введення/виведення для вказаного потоку stream замість буферу, що виділяється системою по замовчуванню.
      Потоки
stderr і stdout по замовчуванню небуферизовані, але для них можна встановлювати буферизацію засобами setbuf.
      
Примітка. Наслідки буферизації будуть непередбаченими, якщо тільки функція setbuf() не викликана зразу вслід за функцією fopen() або fseek() для заданого потоку.
      В мові Сі для керування буферизацією потоків існує ще одна функція:
setvbuf(). Вона дозволяє користувачу керувати буферизацією та розміром буфера потоку stream. Синтаксис :
int setvbuf(FILE *stream, char *buf, int type, size_t size);
      Потік
stream повинен відноситися до відкритого потоку.
      Якщо значення параметру
buf не NULL, то масив, адреса якого задається значенням параметра buf буде використовуватися в якості буфера.
       Якщо потік буферизується, значення параметра type визначає тип буферизації. Тип буферизації може бути або _IONBF, або _IOFBF, або _IOLBF.
      Якщо тип рівний _IOFBF або _IOLBF, то значення параметра size використовується як розмір буфера.
      Якщо тип рівний _IONBF, то потік небуферизований, і значення параметрів
size і buf ігноруються.
      Допустиме значення параметра size: більше 0 і менше, ніж максимальний розмір цілого (
int).
      Значення констант _IONBF, _IOFBF та _IOLBF визначені у файлі stdio.h.
_IOFBF 0 /* буферизація на повний об'єм буфера */
_IOLBF 1 /* порядкова буферизація */
_IONBF 2 /* потік не буферизується */

      Для примусового виштовхування буферу можна використовувати функцію
fflush(). Її синтаксис :
int fflush(FILE *stream);
      Дана функція виштовхує вміст буфера, зв'язаного з потоком stream. Потік залишається відкритим. Якщо потік небуферизований, то виклик функції
fflush() не викличе ніяких ефектів.
      Буфер потоку автоматично виштовхується, коли він заповнюється, коли закривається потік або коли програма завершує своє виконання.
     
 Приклад 1.
#include <stdio.h>
#include<conio.h>
char outbuf[BUFSIZ];
int main(void)
{
    clrscr();
    setbuf(stdout, outbuf);
    puts("This is a test of buffered output.\n\n");
    puts("This output will go into outbuf\n");
    puts("and won't appear until the buffer\n");
    puts("fills up or we flush the stream.\n");
    getch();
    fflush(stdout);
    getch();
    return 0;
}

  
    Приклад 2.
#include <stdio.h>
int main(void)
{
      FILE *input, *output;
      char bufr[512];
      input = fopen("file.in", "r+b");
      output = fopen("file.out", "w");
      if (setvbuf(input, bufr, _IOFBF, 512) != 0)
          printf("
Помилка встановлення буферизацiї для вхiдного файла\n");
      else
          printf("
Для вхiдного файла встановлено буферизацiю\n");
      if (setvbuf(output, NULL, _IOLBF, 132) != 0)
          printf("
Помилка встановлення буферизацiї для вихiдного файла\n");
      else printf("
Буфер для вихiдного файла встановлено\n");
      fclose(input);
      fclose(output);
      return 0;
}

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

       Як визначити термін "програма"? Взагалі це послідовність операцій над структурами даних, що реалізують алгоритм розв'язання конкретної задачі. На початку проектування задачі ми розмірковуємо відносно того, що повинна робити наша програма, які конкретні задачі вона повинна розв'язувати, та які алгоритми при цьому повинні бути реалізовані. Буває, і це характерно для більшості задач, вихідна задача досить довга та складна, у зв'язку з чим програму складно проектувати та реалізовувати, а тим більше супроводжувати, якщо не використовувати методів керування її розмірами та складністю. Для цього потрібно використати відомі прийоми функціонально-модульного програмування для структурування програм, що полегшує їх створення, розуміння суті та супровід.
       Розв'язання практичної задачі проходить у кілька етапів, зміст яких подає таблиця 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-обмеженням. 

1.17 Складені оголошення

      Прості оголошення в мові Сі дозволяють визначати прості змінні, масиви, покажчики, функції, структури та об'єднання.
В найпростішому випадку, якщо оголошується проста змінна базового типу, типу структури або об'єднання, ідентифікатор описується типу, що заданий специфікацією типу.
      Для оголошення масиву значень деякого типу, функції, що повертає значення деякого типу, або покажчика на значення деякого типу, ідентифікатор доповнюється відповідно квадратними дужками […] справа, круглими дужками (… ) справа або ознакою покажчика - зірочкою (*) зліва.
      Наступні приклади ілюструють найпростіші форми оголошень :
int list[20]; /* масив list із 20 цілих значень */
char *cp; /* покажчик cp на значення типу char */
double func(); /* функція func(), що повертає значення типу double*/
      Синтаксис оголошення :
[ клас_пам'яті ] тип ідентифікатор [ = ініціалізатор ] ;
      Оголошення складаються з чотирьох частин :
1. необов'язкового специфікатора класу пам'яті (auto, register, static, extern);
2. базового типу або типу користувача;
3. оголошуючої частини;
4. необов'язкового ініціалізатора.
      Оголошуюча частина в свою чергу складається з ідентифікатора і, можливо, операторів оголошення. Найчастіше використовуються наступні оператори оголошення (таблиця 1.18).

Таблиця 1.18. "Оператори оголошення"

*

покажчик

префікс

*const

константний покажчик

префікс

&

посилання (адреса)

префікс

[ ]

масив

суфікс

( )

функція

суфікс

      Суфіксні оператори оголошення "міцніше зв'язані" з ім'ям, ніж префіксні. Тому typename *str[]; означає масив покажчиків на деякі об'єкти, а для визначення типів таких як "покажчик на функцію" необхідно використовувати дужки.
      Складене оголошення - це ідентифікатор, що доповнений більше ніж однією ознакою масиву, покажчика, або функції.
З одним ідентифікатором можна створити множину різних комбінацій ознак типу масив, покажчик або функція. Причому, деякі комбінації неприпустимі. Наприклад, масив не може містити в якості елементів функцію, а функція не може повертати масив або функцію.
      При інтерпретації складених оголошень спочатку розглядають квадратні і круглі дужки, що розташовані справа від ідентифікатора. Квадратні і круглі дужки мають однаковий пріоритет. Вони інтерпретуються зліва направо. Після них розглядаються зірочки, що розташовані зліва від ідентифікатора. Специфікація типу розглядається на останньому етапі, після того, як все складене оголошення проінтерпретоване.
Круглі дужки можуть також використовуватися для зміни існуючого по замовчуванню порядку інтерпретації оголошення.       Наприклад :
int *func(); /*функція, що повертає покажчик на int */
int (*func)();/*покажчик на функцію, що повертає int */

      Алгоритм інтерпретації складених оголошень :
     1. Знайти ідентифікатор (якщо їх декілька, то необхідно почати з того, який знаходить ближче до "середини" складеного оголошення).
     2. Подивитися вправо :
          • Якщо справа розташована відкриваюча кругла дужка - тоді це функція, а вираз, що розташований між цією відкриваючою дужкою '(' і відповідною їй закриваючою дужкою ')' необхідно інтерпретувати як параметри функції.
          • Якщо справа стоїть відкриваюча квадратна дужка '[' - тоді це масив і вираз між відповідними квадратними дужками […] необхідно інтерпретувати як розмір масиву. Примітка : якщо масив багатовимірний, то за дужками […] розташовується ще одна або декілька серій квадратних дужок […].
          • Якщо на будь-якому етапі інтерпретації справа зустрічається закриваюча кругла дужка ')', то необхідно спочатку повністю провести інтерпретацію всередині даної пари круглих дужок, а потім подовжити інтерпретацію справа від закриваючої круглої дужки ')'.
     3. Якщо зліва від проінтерпретованого виразу розташована зірочка і :
          • проінтерпретований вираз є функцією, то вона повертає покажчик;
          • проінтерпретований вираз є масивом, то кожний елемент цього масиву є покажчиком;
          • проінтерпретований вираз не є ні функцію, ні масивом, то вираз є покажчиком.
     4. Застосувати описані вище правила (2-3 пункт алгоритму) ще раз.
     5. Проінтерпретувати специфікацію типу даних.

     Приклад інтерпретації складених оголошень :

1. Ідентифікатор var оголошений як
2. покажчик на
3. функцію, що приймає в якості аргументу масив із ста значень типу
char і повертає
4. покажчик на
5. масив із ста елементів, кожний з яких є
6. покажчиком на
7. значення типу
char.

1.17.1 Описи з модифікаторами
     Використання в оголошеннях спеціальних ключових слів (модифікаторів) дозволяють надавати оголошенням спеціального змісту. Інформація, яку несуть в собі модифікатори, використовуються компілятором мови Сі в процесі генерування коду.
     Розглянемо правила інтерпретації оголошень, що містять модифікатори
const, volatile, cdecl, pascal, near, far, huge, interrupt.
     Модифікатори
cdecl, pascal, interrupt повинні розташовуватися безпосередньо перед ідентифікатором.
Модифікатори
const, volatile, near, far, huge впливають або на ідентифікатор, або на ознаку покажчика (зірочку), що розташована безпосередньо справа від модифікатора. Якщо справа розташований ідентифікатор, то модифікується тип об'єкта, що іменується даним ідентифікатором. Якщо ж справа розташована зірочка, то ця зірочка представляє собою покажчик на модифікований тип. Таким чином, конструкція
     
модифікатор *
     читається як "покажчик на модифікований тип".
Наприклад,
int const *p; /* покажчик на цілу константу */
int *const p; /* константний покажчик на величину типу int */
     Модифікатори типу const і volatile можуть також розташовуватися і перед специфікацією типу.
     В ТС використання модифікаторів
near, far, huge обмежене: вони можуть бути записані тільки перед ідентифікатором функції або перед ознакою покажчика (зірочкою).
     Допускається більше одного модифікатора для одного об'єкта (або елемента оголошення). В наступному прикладі тип функції func модифікується одночасно спеціальними ключовими словами
far і pascal. Порядок ключових слів неважливий, тобто комбінації far pascal і pascal far мають однаковий зміст.
     
int far * pascal far func();
     Тип значення, що повертається функцією
func, представляє собою покажчик на значення типу int. Тип цих значень модифікований спеціальним ключовим словом far.
     Як і звичайно, в оголошенні можуть бути використані круглі дужки для зміни порядку його інтерпретації.

     В даному прикладі наведене оголошення з різними варіантами розташування модифікатора far. Враховуючи правило, відповідно до якого модифікатор впливає на елемент оголошення, розташований справа від нього, можна інтерпретувати це оголошення наступним чином.
1. Ідентифікатор getint оголошений як
2. покажчик на
far
3. функцію, що приймає
4. один аргумент, який є покажчиком на
far
5. значення типу
int
6. і повертає покажчик на
far
7. значення типу
char

1.17.2 Модифікатори const і volatile
     Про модифікатор
const йшла мова в розділі 1.2.3."Константи". Модифікатор const не допускає явного присвоювання змінній або інших дій, що можуть вплинути на зміну її значення, таких як виконання операції інкременту і декременту. Значення покажчика, що оголошений з модифікатором const, не може бути зміненим, на відміну від значення об'єкта, на який він вказує.
     Модифікатори
volatile і const протилежні за змістом.
     Модифікатор
volatile вказує на те, що значення змінної може бути зміненим; але не тільки безпосередньо програмою, а також і зовнішнім впливом (наприклад, програмою обробки переривань, або, якщо змінна відповідає порту введення/виведення, обміном із зовнішнім пристроєм). Оголошення об'єкта з модифікатором volatile попереджує компілятор мови Сі, чого не слід робити
     Можливим також є одночасне використання в оголошенні модифікаторів
const і volatile. Це означає, що значення змінної не може модифікуватися програмою, але піддається зовнішньому впливу.
     Якщо з модифікатором
const або volatile оголошується змінна складеного типу, то дія модифікатора розповсюджується на всі його складові елементи.
     
Примітка. При відсутності в оголошенні специфікації типу і присутності модифікатора const або volatile мається на увазі тип int.
     
Приклади:
float const pi=3.14159265;
const maxint=32767;
char *const str= "Деякий рядок."; /* покажчик-константа */
char const *str2= "Рядок";/* покажчик на константний рядок */

     Із врахуванням наведених вище оголошень наступні оператори неприпустимими.
pi=3.0; /* присвоювання значення константі */
i=maxint--; /* зменшення константи */
str="Other string"; /* присвоювання значення константі-покажчику */
     Однак виклик функції strcpy(str,"String"); припустимий, так як в даному випадку здійснюється посимвольне копіювання рядка в область пам'яті, на яку вказує покажчик.
     Аналогічно, якщо покажчик на тип const присвоїти покажчику на тип, відмінний від
const, то через отриманий покажчик можна здійснювати присвоювання.

1.17.3 Модифікатори cdecl і pascal
     Результатом роботи компілятора мови Сі є файл, що містить об'єктний код програми. Файли з об'єктним кодом, що отримуються в результаті компіляції всіх файлів програми, компоновщик об'єднує в один файл виконання.
     При компіляції всі глобальні ідентифікатори програми, тобто імена функцій і глобальних змінних, зберігаються в об'єктному коді і використовуються компоновщиком в процесі роботи. По замовчуванню ці ідентифікатори зберігаються в своєму початковому вигляді. Крім того, в якості першого символу кожного ідентифікатора компілятор мови Сі додає символ підкреслення.
     Компоновщик по замовчуванню розрізняє великі та малі літери, тому ідентифікатори, що використовуються в різних файлах програми для іменування одного і того самого об'єкта, повинні повністю співпадати з точки зору як орфографії, так і регістрів літер. Для здійснення співпадіння ідентифікаторів, що використовуються в різномовних файлах, використовуються модифікатори
pascal і cdecl.
     Використання модифікатора
pascal до ідентифікатора призводить до того, що ідентифікатор перетворюється до верхнього регістру і до нього не додається символ підкреслення. Цей ідентифікатор не може використовуватися для іменування в програмі на мові Сі глобального об'єкта, який використовується також в програмі на мові Паскаль. В об'єктному коді, що згенерований компілятором мови Сі, і в об'єктному коді, що згенерований компілятором мови Паскаль, ідентифікатор буде представлений ідентично.
     Якщо модифікатор
pascal застосовується до ідентифікатора функції, то він здійснює вплив також і на передачу аргументів функції. Засилання аргументів у стек здійснюється в цьому випадку не в оберненому порядку, як прийнято в компіляторах мови Сі, а в прямому - першим засилається в стек перший аргумент.
     Функція типу
pascal не може мати змінне число параметрів, як, наприклад, функція printf().
     Існує ще один модифікатор, яка присвоює всім функціям і покажчикам на функції тип
pascal. Це означає, що вони будуть використовувати послідовність виклику, що прийнята в мові Паскаль, а їх ідентифікатори будуть можливими для виклику з програми на Паскалі. При цьому можна сказати, що деякі функції і покажчики на функції використовують викликаючу послідовність, прийняту в мові Сі, а їх ідентифікатори мають традиційний вигляд для ідентифікаторів мови Сі. Для цього їх оголошення повинні містити модифікатор cdecl.

1.17.4 Модифікатори near, far, huge
     Ці модифікатори здійснюють вплив на роботу з адресами об'єктів.
     Компілятор мови Сі дозволяє використовувати при компіляції одну з декількох моделей пам'яті.
     Використання моделі пам'яті визначає розміщення програми і даних в ОП, а також внутрішній формат покажчиків. Однак при використанні будь-якої моделі пам'яті можна оголосити покажчик з форматом, що відрізняється від прийнятого по замовчуванню. Це здійснюється за допомогою модифікаторів
near, far і huge.
     
Покажчик типу near - 16-бітовий; для визначення адреси об'єкта він використовує зсув відносно поточного вмісту сегментного регістру. Для покажчика типу near доступна пам'ять обмежена розміром поточного 64-кілобайтного сегмента даних.
     
Покажчик типу far - 32-бітовий; він містить як адресу сегменту, так і зсув. При використанні покажчиків типу far припустимі звернення до пам'яті в межах 1-мегабайтного адресного простору, однак значення покажчика типу far циклічно змінюється в межах 64-кілобайтного сегменту.
     
Покажчик типу huge - 32-бітовий; він також містить адресу сегменту і зсув. Значення покажчика типу huge може бути змінене в межах 1-мегабайтного адресного простору. В ТС покажчик huge завжди зберігається в нормалізованому форматі.

1.18.1 Директива #include
      Синтаксис :
#include "ім'я_файла"
#include <ім'я_файла>
      Директива #include використовується для включення копії вказаного файла в те місце програми, де знаходиться ця директива.
      Різниця між двома формами директиви полягає в методі пошуку пре процесором файла, що включається. Якщо ім'я файла розміщене в "кутових" дужках < >, то послідовність пошуку препроцесором заданого файла в каталогах визначається встановленими каталогами включення (include directories). Якщо ж ім'я файла заключне в лапки, то препроцесор шукає в першу чергу файл у поточній директорії, а потім вже у каталогах включення.
      Робота директиви #include зводиться практично до того, що директива #include прибирається, а на її місце заноситься копія вказаного файла.
      Текст файла, що включається може містити директиви препроцессора, і директиву #include зокрема. Це означає, що директива #include може бути вкладеною. Допустимий рівень вкладеності директиви #include залежить від конкретної реалізації компілятора.
#include <stdio.h> /* приклад 1*/
#include "defs.h" /* приклад 2*/
      В першому прикладі у головний файл включається файл з ім'ям stdio.h. Кутові дужки повідомляють компілятору, що пошук файла необхідно здійснювати в директоріях, вказаних в командному рядку компіляції, а потім в стандартних директоріях.
      В другому прикладі в головний файл включається файл з ім'ям defs.h. Подвійні лапки означають, що при пошуку файла спочатку повинна бути переглянута директорія, що містить поточний файл.
      В ТС є також можливість задавати ім'я шляху в директиві #include за допомогою іменованої константи. Якщо за словом include слідує ідентифікатор, то препроцесор перевіряє, чи не іменує він константу або макровизначення. Якщо ж за словом include слідує рядок, що заключений в лапки або в кутові дужки, то ТС не буде шукати в ній ім'я константи.
#define myincl "c:\test\my.h"
#include myincl

1.18.2 Директива #define
      Синтаксис :
#define ідентифікатор текст
#define ідентифікатор (список_параметрів) текст
      Директива #define заміняє всі входження ідентифікатора у програмі на текст, що слідує в директиві за ідентифікатором. Цей процес називається макропідстановкою. Ідентифікатор замінюється лише в тому випадку, якщо він представляє собою окрему лексему. Наприклад, якщо ідентифікатор є частиною рядка або більш довгого ідентифікатора, він не замінюється. Якщо за ідентифікатором слідує список параметрів, то директива визначає макровизначення з параметрами.
      Текст представляє собою набір лексем, таких як ключові слова, константи, ідентифікатори або вирази. Один або більше пробільних символів повинні відділяти текст від ідентифікатора (або заключених в дужки параметрів). Якщо текст не вміщується в рядку, то він може бути продовжений на наступному рядку; для цього слід набрати в кінці рядка символ обернений слеш \ і зразу за ним натиснути клавішу Enter.
      Текст може бути опущений. В такому разі всі екземпляри ідентифікатора будуть вилучені з тексту програми. Але сам ідентифікатор розглядається як визначений і при перевірці директива #if дає значення 1.
      Список параметрів, якщо він заданий, містить один або більше ідентифікаторів, розділених комами. Ідентифікатори в рядку параметрів повинні відрізнятися один від одного. Їх область дії обмежена макровизначенням. Список параметрів повинен бути заключений в круглі дужки. Імена формальних параметрів у тексті відмічають позиції, в які повинні бути підставлені фактичні аргументи макровиклику. Кожне ім'я формального параметра може з'явитися в тексті довільне число разів.
      В макровиклику вслід за ідентифікатором записується в круглих дужках список фактичних аргументів, що відповідають формальних параметрам із списку параметрів. Текст модифікується шляхом заміни кожного формального параметра на відповідний фактичний параметр. Списки фактичних параметрів і формальних параметрів повинні мастити одне і те ж число елементів.
      Примітка. Не слід плутати підстановку аргументів в макровизначеннях з передачею параметрів у функціях. Підстановка в препроцесорі носить чисто текстовий характер. Ніяких обчислень при перетворенні типу при цьому не виконується.
      Вище вже говорилося, що макровизначення може містити більше одного входження даного формального параметра. Якщо формальний параметр представлений виразом з "побічним ефектом" і цей вираз буде обчислюватися більше одного разу, разом з ним кожний раз буде виникати і "побічний ефект". Результат виконання в цьому випадку може бути помилковим.
      Всередині тексту в директиві #define можуть знаходитися вкладені імена інших макровизначень або констант.
Після того, як виконана макропідстановка, отриманий рядок знову переглядається для пошуку інших імен констант і макровизначень. При повторному перегляді не розглядається ім'я раніше проведеної макропідстановки. Тому директива
#define a a
      не призведе до за циклювання препроцесора.
Приклад 1 :
#define WIDTH 80
#define LENGTH (WIDTH+10)
      В даному прикладі ідентифікатор WIDTH визначається як ціла константа із значенням 80, а ідентифікатор LENGTH - як текст (WIDTH+10). Кожне входження ідентифікатора LENGTH у програму буде замінено на текст (WIDTH+10), який після розширення ідентифікатора WIDTH перетвориться на вираз (80+10). Дужки дозволяють уникнути помилок в операторах, подібних наступному :
      val=LENGTH*20;
Після обробки програми препроцесором текст набуде вигляду :
      val=(80+10)*20;
Значення, яке буде присвоєно змінній val рівне 1800. При відсутності дужок значення val буде рівне 280.
      val=80+10*20;
      Приклад 2 :
#define MAX(x,y) ((x)>(y))?(x):(y)
      В даному прикладі визначається макровизначення MAX. Кожне входження ідентифікатора MAX в тексті програми буде замінено на вираз ((x)>(y))?(x):(y), в якому замість формальних параметрів x та y підставляються фактичні. Наприклад, макровиклик :
MAX(1,2)
заміниться на вираз ((1)>(2))?(1):(2).

1.18.3 Директива #undef
      Синтаксис :
#undef ідентифікатор
      Визначення символічних констант і макросів можуть бути анульовані за допомогою директиви препроцесора #undef. Таким чином, область дії символічної константи або макросу починається з місця їх визначення і закінчується явним їх анулюванням директивою #undef або кінцем файла.
      Після анулювання ідентифікатор може бути знову використаний директивою #define.
Приклад :
#define WIDTH 80
/* … */
#undef WIDTH
/* … */
#define WIDTH 20

1.19 Динамічні структури даних


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

1.19.1 Лінійні списки
      Лінійний список - це скінченна послідовність однотипних елементів (вузлів). Кількість елементів у цій послідовності називається довжиною списку. Наприклад :
F=(1,2,3,4,5,6) - лінійний список, його довжина 6.
     При роботі зі списками дуже часто доводиться виконувати такі операції :
      • додавання елемента в початок списку;
      • вилучення елемента з початку списку;
      • додавання елемента в будь-яке місце списку;
      • вилучення елемента з будь-якого місця списку;
      • перевірку, чи порожній список;
      • очистку списку;
      • друк списку.
     Основні методи зберігання лінійних списків поділяються на методи послідовного та зв'язаного зберігання.
     Послідовне зберігання списків. Метод послідовного зберігання списків ґрунтується на використанні масиву елементів деякого типу та змінної, в якій зберігається поточна кількість елементів списку.
#define MAX 100 /* максимально можлива довжина списку */
typedef struct
{
     int x; /* тут потрібно описати структуру елементів списку*/
} elementtype;
typedef struct
{
     elementtype elements[MAX];
     int count;
} listtype;
     У вищенаведеному фрагменті програми описуються типи даних elementtype (визначає структуру елемента списку) та listtype (містить масив для зберігання елементів та змінну для зберігання поточного розміру списку).
Наводимо приклади реалізації функцій для виконання основних операцій над списками.

1. Ініціалізація списку (робить список порожнім).
void list_reset(listtype *list)
{
     list->count=0;
};

2. Додавання нового елементу у кінець списку.
void list_add(listtype *list,elementtype element)
{
     if (list->count==MAX) return;
     list->elements[list->count++]=element;
};

3. Додавання нового елементу в позицію pos.

void list_insert(listtype *list,int pos,elementtype element)
{
     int j;
     if (pos<0||pos>list->count||pos>=MAX) return;
     for (j=list->count;j>pos;j--)
     {
          list->elements[j]=list->elements[j-1];
     };
     list->elements[j]=element;
     list->count++;
};

4. Вилучення елемента з номером pos.

void list_delete(listtype *list,int pos)
{
     int j;
     if (pos<0||pos>list->count) return;
     for (j=pos+1;j<list->count;j++)
     {
          list->elements[j-1]=list->elements[j];
     };
     list->count--;
};

5. Отримання елемента з номером pos.
int list_get(listtype *list,int pos,elementtype *element)
{
     if (pos<0||pos>list->count)
     {
          return 0;
     };
     *element=list->elements[pos];
     return 1;
};


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

     Зв'язане зберігання лінійних списків. Найпростіший спосіб зв'язати множину елементів - зробити так, щоб кожний елемент містив посилання на наступний. Такий список називається односпрямованим (однозв'язаним). Якщо додати в такий список ще й посилання на попередній елемент, то отримаємо двозв'язаний список. А список, перший та останній елементи якого зв'язані, називається кільцевим.
     Структуру даних для зберігання односпрямованого лінійного списку можна описати таким чином :
typedef long elemtype;
typedef struct node
{
     elemtype val;
     struct node *next;
} list;

     В даному фрагменті програми описуються декілька типів даних :
elemtype - визначає тип даних лінійного списку. Можна використовувати будь-який стандартний тип даних, включаючи структури.
list - визначає структуру елемента лінійного списку (val - значення, яке зберігається у вузлі, next - покажчик на наступний вузол).
     Схематично лінійний односпрямований список виглядає так :

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

list *addbeg(list *first, elemtype x)
{
     list *vsp;
     vsp = (list *) malloc(sizeof(list));
     vsp->val=x;
     vsp->next=first;
     first=vsp;
     return first;
}

2. Видалення елемента з початку списку.

list *delbeg(list *first)
{
      list *vsp;
     vsp=first->next;
     free(first);
     return vsp;
}


3. Включення нового елемента у список.

list *add(list *pred, elemtype x)
{
      list *vsp;
     vsp = (list *) malloc(sizeof(list));
     vsp->val=x;
     vsp->next=pred->next;
     pred->next=vsp;
     return vsp;
}


4. Видалення елемента зі списку.

elemtype del(list *pred)
{
      elemtype x;
     list *vsp;
     vsp=pred->next;
     pred->next=pred->next->next;
     x=vsp->val;
     free(vsp);
     return x;
}

5. Друк значень списку.
void print(list *first)
{
     list *vsp;
     vsp=first;
     while (vsp)
     {
          printf("%i\n",vsp->val);
          vsp=vsp->next;
     }
}

6. Перевірка, чи порожній список
int pust(list *first)
{
     return !first;
}

7. Знищення списку
list *kill(list *first)
{
     while (!pust(first)) first=delbeg(first);
     return first;
}

1.19.2 Стеки
     Стек - динамічна структура даних, яка представляє собою впорядкований набір елементів, в якому додавання нових елементів і видалення існуючих проходить з одного кінця, який називається вершиною стека.
     Стек реалізує принцип LIFO (last in - first out, останнім прийшов - першим пішов). Найбільш наглядним прикладом організації стеку може бути дитяча пірамідка, де додавання і знімання кілець здійснюється як раз відповідно до цього принципу.
     Основні операції, які можна виконувати над стеками :
      • додавання елемента в стек;
      • вилучення елемента із стека;
      • перевірка, чи порожній стек;
      • перегляд елемента у вершині стека без видалення;
      • очистка стека.
     Стек створюється так само, як і лінійний список, так як стек є частковим випадком односпрямованого списку.
typedef long elemtype;
typedef struct node
{
     elemtype val;
     struct node *next;
} stack;

     Реалізація основних операцій над стеками :
1. Початкове формування стеку
stack *first(elemtype d)
{
     stack *pv=(stack*) calloc(1,sizeof(stack));
     pv->val=d;
     pv->next=NULL;
     return pv;
};

2. Занесення значення в стек
void push(stack **top,elemtype d)
{
     stack *pv=(stack*) calloc(1,sizeof(stack));
     pv->val=d;
     pv->next=*top;
     *top=pv;
};

3. Вилучення елемента зі стека
elemtype pop(stack **top)
{
     elemtype temp=(*top)->val;
     stack *pv=*top;
     *top=(*top)->next;
     free(pv);
     return temp;
};

1.19.3 Черги
     Черга - це лінійний список, де елементи вилучаються з початку списку, а додаються в кінець (як звичайна черга в магазині).
     Двостороння черга - це лінійний список, у якого операції додавання, вилучення і доступу до елементів можливі як спочатку так і в кінці списку. Таку чергу можна уявити як послідовність книг, що стоять на полиці так, що доступ до них можливий з обох кінців.
     Черга є частковим випадком односпрямованого списку. Вона реалізує принцип FIFO (first in - first out, першим прийшов - першим пішов).
     Черги створюються аналогічно до лінійних списків та стеків.
typedef long elemtype;
typedef struct node
{
     elemtype val;
     struct node *next;
} queue;

1. Початкове формування черги
queue *first (elemtype d)
{
     queue *pv=(queue*) calloc(1,sizeof(queue));
     pv->val=d;
     pv->next=NULL;
     return pv;
}

2. Додавання елемента в кінець
void add (queue **pend, elemtype d)
{
     queue *pv=(queue*) calloc(1,sizeof(queue));
     pv->val=d;
     pv->next=NULL;
     (*pend)->next=pv;
     *pend=pv;
}

3. Вилучення елемента з кінця
elemtype del(queue **pbeg)
{
     elemtype temp=(*pbeg)->val
     queue *pv=*pbeg;
     *pbeg=(*pbeg)->next;
     free(pv)
     return temp;
}

1.19.4 Двійкові дерева
     Бінарне дерево - це динамічна структура даних, що складається з вузлів (елементів), кожен з яких містить, окрім даних, не більше двох посилань на інші бінарні дерева. На кожен вузол припадає рівно одне посилання. Початковий вузол називається коренем дерева (рис 1.23.).
     Якщо дерево організоване таким чином, що для кожного вузла всі ключі його лівого піддерева менші за ключ цього вузла, а всі ключі його правого піддерева - більші, воно називається деревом пошуку. Однакові ключі в деревах пошуку не допускаються.
     В дереві пошуку можна знайти елемент за ключем, рухаючись від кореня і переходячи на ліве або праве піддерево в залежності від значення ключа в кожному вузлі. Такий спосіб набагато ефективніший пошуку по списку, так як час виконання операції пошуку визначається висотою дерева.
     Дерево є рекурсивною структурою даних, так як кожне піддерево є також деревом. Дії з такими структурами даних простіше всього описувати за допомогою рекурсивних алгоритмів.

     Наприклад, функцію обходу всіх вузлів дерева в загальному вигляді можна описати так :
function way(дерево)
{
     way(ліве піддерево);
     обробка кореня;
     way(праве піддерево);
};
     Можна обходити дерево і в іншому порядку, наприклад, спочатку корінь, а потім піддерева. Але наведена модель функції дозволяє отримати на виході відсортовану послідовність ключів, так як спочатку відвідуються вершини з меншими ключами, що розташовані в лівому піддереві.
     Таким чином, дерева пошуку можна використовувати для сортування значень. При обході дерева вузли не видаляються.
     Для бінарних дерев визначені наступні операції :
      • включення вузла у дерево;
      • пошук по дереву;
      • обхід дерева;
      • видалення вузла.
     Для кожного рекурсивного алгоритму можна створити його нерекурсивний еквівалент.
     Вузол бінарного дерева можна визначити як :
typedef struct sbtree
{
     int val;
     struct sbtree *left,*right;
} btree;

     Реалізація деяких операцій з бінарними деревами.
1). Рекурсивний пошук в бінарному дереві. Функція повертає покажчик на знайдений вузол.:
btree *Search(btree *p, int v)
{
     if (p==NULL) return(NULL); /* вітка порожня */
     if (p->val == v) return(p); /* вершина знайдена */
     if (p->val > v) /* порівняння з поточним вузлом */
          return(Search(p->left,v)); /* ліве піддерево */
     else
          return(Search(p->right,v)); /* праве піддерево */
}

2). Включення значення в двійкове дерево (рис 1.33.):
btree *Insert(btree *pp, int v)
{
     if (pp == NULL) /* знайдена порожня вітка */
     {
          btree *q = (btree*) calloc(1,sizeof(btree));
          /* створити вершину дерева */
          q->val = v; /* і повернути покажчик */
          q->left = q->right = NULL;
          return q;
     }
     if (pp->val == v) return pp;
     if (pp->val > v) /* перейти в ліве піддерево */
          pp->left=Insert(pp->left,v);
     else
          pp->right=Insert(pp->right,v);
     /* перейти в праве піддерево*/
     return pp;
}

3). Рекурсивний обхід двійкового дерева :
void Scan(btree *p)
{
     if (p==NULL) return;
     Scan(p->left);
     printf("%i\n",p->val);
     Scan(p->right);
}

 


 

А также другие работы, которые могут Вас заинтересовать

12046. Банковское кредитование 148 KB
  Банковское кредитование ВВЕДЕНИЕ Актуальность темы данной работы обусловлена несколькими аспектами. Вопервых увеличением числа кредитнофинансовых организаций осуществляющих кредитные операции. Это требует выявление специфики именн
12047. Оптимізація біржової торгівлі конвертованими валютами на базі прогнозування їх крос-курсів на прикладі діяльності приватного підприємця 1.62 MB
  ПОЯСНЮВАЛЬНА ЗАПИСКА до дипломної роботи магістра спеціальності 8.050102 Економічна кібернетика на тему: Оптимізація біржової торгівлі конвертованими валютами на базі прогнозування їх кроскурсів на прикладі діяльності приватного підприємця ЗАВДАННЯ для диплом
12048. Сравнительный анализ условий предоставления потребительский кредитов на материалах банков г.Барнаула 1009.5 KB
  Содержание Введение Теоретические аспекты организации кредитования физических лиц Основы кредитования физических лиц Процедура выдачи кредита Способы опр...
12049. Фундаментальный анализ эмитентов в инвестиционной деятельности на рынке ценных бумаг 649 KB
  Фундаментальный анализ эмитентов в инвестиционной деятельности на рынке ценных бумаг Содержание [1] Содержание [2] ВВЕДЕНИЕ [3] Глава 1. Фундаментальный подход к оценке акций. [3.1] 1.1 Сущность фундаментального подхода [...
12050. Организация и совершенствование управления банковскими рисками на примере АО «Казкоммерцбанк» 533.5 KB
  Введение Умение разумно рисковать один из элементов культуры предпринимательства в целом а банковской деятельности в особенности. Современный рынок банковских услуг находящийся сегодня кризисном положении наглядно иллюстрирует актуальность рассматриваемого
12051. АНАЛИЗ ФИНАНСОВОГО СОСТОЯНИЯ И ОЦЕНКА КРЕДИТОСПОСОБНОСТИ ЗАО «АРГО» 432.5 KB
  PAGE 27 ВВЕДЕНИЕ Кредитнофинансовая система – одна из важнейших и неотъемлемых структур рыночной экономики. Развитие банковской системы и товарного производства исторически шло параллельно и тесно переплеталось. Находясь в центре экономической жиз
12052. ШЛЯХИ ВДОСКОНАЛЕННЯ ДІЯЛЬНОСТІ БАНКІВСЬКОЇ СИСТЕМИ УКРАЇНИ 274 KB
  ВСТУП У наш час Україна переживає широкомасштабну економічну кризу. Це не дивує тому що з початку 90х років у нашій країні були проведені реформи спрямовані на зміну економічного і політичного устрою. Серед тих сфер де відбулися особливо великі зміни у зв’язку з
12053. Депозитарная деятельность в Республике Беларусь и пути ее совершенствования (на примере депозитария ООО «БММ-Траст») 1.18 MB
  ДИПЛОМНАЯ РАБОТА на тему: Депозитарная деятельность в Республике Беларусь и пути ее совершенствования на примере депозитария ООО БММТраст реферат Депозитарная деятельность в Республике Беларусь и пути ее совершенствования на примере депозитария ОО...
12054. Концепція управління персоналом в комерційному банку АКБ Приватбанк 1.13 MB
  ДИПЛОМНА РОБОТА спеціаліста Концепція управління персоналом в комерційному банку АКБ Приватбанк АНОТАЦІЯ Предметом дипломного проекту є вивчення стосунків працівників у процесі роботи комерційного банку з точки зору найбільш повного та ефект...