Iphone
shpora.me - незаменимый помощник для студентов и школьников, который позволяет быстро создавать и получать доступ к шпаргалкам или другим заметкам с любых устройств. В любое время. Абсолютно бесплатно. Зарегистрироватся | Войти

* данный блок не отображается зарегистрированым пользователям и на мобильных устройствах

програмування -Butox

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

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

Зовнішні пристрої(пристрої введення-виведення) це дисплей(монітор), клавіатура, маніпулятор"миша", дисководи та інші(сканер, модем тощо). Вони керують обробкою даних на зовнішніх носіях. Пристрої введення-виведення мають власні процесори, які можуть переносити дані із зовнішніх носіїв дооперативної пам'яті або навпаки.

Рис. 1.1. Загальна схема комп'ютера

 

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

Усі дані в комп'ютері є послідовностями 0 та 1. Значення 0 та 1 відповідають двом стійким станам елемента пам'яті, що називається біт (bit, або binary digit двійкова цифра). Вісім послідовних бітів утворюють байт.

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

Розмір оперативної пам'яті вимірюється зазвичай сотнями й тисячами МБ і середній розмір пам'яті комп'ютерів щороку зростає.

 

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

 

Двійковий запис чисел

Усі дані в комп'ютері зображуються(кодуються) у вигляді послідовностей 0 та 1. Зображення чисел за допомогою 0 та 1 називається двійковим, тобто зображенням у двійковій системі числення.

Звична десяткова система має 10 цифр. У записі числа молодша цифра позначає кількість одиниць, наступні кількість десятків, сотень і подальших степенів числа10. Кожна кількість може бути від0 до9. У двійковій системі роль десятки відіграє число2, а цифри 0 та 1 позначають можливу кількість0 або1.

Цифри позначають кількість відповідних степенів числа2. Отже, послідовні натуральні числа0, 1, 2, 3, 4, 5 мають двійкові записи 0, 1, 10, 11, 100, 101.

 

Види діяльності зі створення програми

Процес створення програми в найзагальніших рисах вимагає кількох видів діяльності:

аналіз задачі й уточнення її постановки;

проектування програми;

розробка програми(кодування);

перевірка програми(тестування);

передача замовнику(упровадження).

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

Приклад. Розглянемо задачу: написати програму ділення двох чисел. Вхідними даними є пара чисел: перше ділене, друге дільник. Проте дані з дільником0 є некоректними. Отже, уточнення постановки полягає в тому, що для коректних вхідних даних програма має виводити частку від ділення першого числа на друге, а для некоректних повідомлення про неможливість ділення.

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

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

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

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

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

Результатом проектування є модель(проект) програми, що далі поступово перетворюється на текст програми. Ця модель може бути записана, наприклад, у вигляді алгоритму з достатньо абстрактними кроками.

Приклад. За уточненою постановкою задачі ділення двох чисел спроектуємо програму у вигляді алгоритму.

1. Увести пару чисел.

2. Якщо ділення неможливе, то повідомити про помилку й завершити виконання.

3. Обчислити частку.

4. Надрукувати обчислене значення.

У багатьох програм є чотири основні частини: отримати вхідні дані, обробити некоректні вхідні дані, обробити коректні вхідні дані, вивести результати обробки. У наведеному прикладі цим частинам відповідають кроки14.

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

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

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

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

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

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

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

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

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

1)      дискретність алгоритм повинний бути представлений як послідовне виконання простих або раніше визначених кроків;

2)      детермінованість застосування алгоритму до тих же самих вхідних даних повинно приводити до однакових результатів;

3)      результативність алгоритм повинний приводити до розвязання задачі за скінченний час;

4)      масовість алгоритм повинний дозволяти отримувати результат при різних вхідних даних у досить широких межах.

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

Основні форми представлення алгоритмів:

-   словесний опис алгоритму;

-   графічне представлення алгоритму (блок-схема);

-   представлення алгоритму мовою програмування (програма).

Приклад. Скласти словесний опис алгоритму обчислення обсягу (V) прямокутного циліндра по радіусі (r) основи і висоті (h).

1.     Початок алгоритму.

2.     Ввести вхідні дані: r, h.

3.     Обчислити обсяг по формулі: V=π⋅r2 ⋅h.

4.     Вивести V.

5.     Кінець алгоритму.

БЛОК-СХЕМИ

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

 

 

Таблиця 1.- Умовні позначки в блок-схемах

 
 

ОСНОВНІ СТРУКТУРИ АЛГОРИТМІВ

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

 

 

Лінійний алгоритм

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

Приклад лінійного алгоритму алгоритм обчислення обсягу (V) прямокутного циліндра по радіусі (r) основи і висоті (h). Блок-схему показано на малюнку 4.1. Алгоритм лінійної структури реалізується в такий спосіб. Початок обробки даних блок 1. Для проведення обчислень здійснюється введення в блоці 2 вихідних даних (значень r і h). У блоці 3 обчислюється обсяг циліндра V. Після обчислень здійснюється виведення результату (блок 4) і останов (блок 5).

Малюнок 4.1. - Приклад лінійного алгоритму

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

Алгоритм, що розгалужується

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

 

Малюнок 4.2. Блок-схема Малюнок 4.3. Блок-схема

розгалуження обходу

Приклад. Обчислити значення f по одній із трьох формул у залежності від значення x:

Блок-схема алгоритму даної задачі приведена на малюнку 4.4.

Для обчислення значення f потрібно перевірити два з трьох взаємовиключних умов (для x<-1 і x>5). Після введення вхідних даних (блок 2) перевіряється перша умова x<-1 (блок 3). Якщо вона виконується, то значення f визначається по першій гілці формули (блок 4). У противному випадку перевіряється кожне з умов, що залишилися, (вони взаємовиключні). У даному випадку в блоці 5 перевіряється умова x>5. Якщо вона виконується, то значення f визначається по третій гілці формули (блок 6), у противному випадку - по другій гілці в блоці 7. У блоці 8 здійснюється виведення результату.

Циклічний алгоритм

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

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

 

Малюнок 4.4. Приклад розгалуженого алгоритму

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

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

Цикли з відомим числом повторень

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

На малюнках 4.54.7 представлені різні варіанти організації такого циклічного процесу. На малюнку 4.5 показана організація циклу з постумовою; на малюнку 4.6

 

 

Малюнок 4.5. Цикл із постумовою Малюнок 4.6. Цикл із передумовою

 
 

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

Малюнок 4.7. - Цикл із блоком модифікація

Якщо ∆ i=1, то в блоці модифікація значення ∆ i не вказують. Кількість m повторень (ітерацій) циклу обчислюється по формулі: m = [(iк-iн)/∆ i + 1], де [(iк-iн)/∆i + 1] - ціла частина величини (iк-iн)/∆ i.

 

Приклад. Обчислити значення функції y = 3ax+b /(c + xcos(x)) при 2 ≤ x ≤ 8, ∆ x = 0,4. Значення a, b, c задані.

Для розвязання цієї задачі потрібно в циклі перебрати всі значення x від xн=2 до xк=8 із кроком ∆x = 0,4 і для кожного з них отримати значення y. Різні варіанти реалізації циклічного процесу для даної задачі показані на малюнках 4.8 - 4.10.

Малюнок 4.8. - Обчислення y Малюнок 4.9. - Обчислення y

(цикл ПОКИ) (цикл ДО)

 

Малюнок 4.10. Обчислення y (цикл із блоком модифікація)

Цикли з невідомим числом повторень

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

Цикли з невідомим числом повторень можуть бути двох типів із передумовою (їх також називають циклами ПОКИ) і з постумовою (цикли ДО). Блок-схеми цих циклів показані на малюнках 4.11 і 4.12.

 

 

Малюнок 4.11. - Цикл ПОКИ Малюнок 4.12. - Цикл ДО

(с передумовою) (з постумовою)

Помітимо, що умови, які перевіряються в цих циклах, взаємо протилежні: у циклі ПОКИ перевіряється умова продовження циклу, а в циклі ДО - умова виходу з циклу.

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

Особливість циклу ДО: тіло циклу завжди виконується хоча б один раз.

В обчислювальному плані ці цикли еквівалентні, тобто в алгоритмі завжди можна замінити цикл ПОКИ циклом ДО і навпаки.

Приклад. Обчислити значення y по формулі y = 3a + xsin2(π/x) для x ≥1,

∆ x=0,8. Обчислення y виконувати доти, поки значення x3 стане більше a. Визначити кількість обчислених значень y. Вивести всі значення y і їхню кількість.

Особливістю цієї задачі є те, що для організації циклу неможливо використовувати блок модифікації, тому що невідомо кінцеве значення параметра x, при якому буде досягнута умова x3>a. Блок-схема розвязання задачі представлена на малюнку 4.13.

У блоці 2 відбувається введення значень a, xн, ∆ x. У блоці 3 присвоюються початкові значення змінній x, лічильникові кількості повторень циклу k. У блоці 4 перевіряється умова продовження циклу: поки умова виконується, у блоці 5 обчислюються значення y, у блоці 6 виводяться значення змінних x і y, підготовляються значення змінних x і k для наступного проходження циклу. Як тільки змінна x досягне такого значення, що x3>a, виводиться значення лічильника k (блок 8) і алгоритм завершується.

 

Малюнок 4.13. Приклад циклу з невідомим числом повторень

Трохи про історію виникнення мов програмування, та мови Сі зокрема. У 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 для створення стандарту мови Сі. На сьогодні мова Сі++, що з'явилася як послідовник Сі, підпорядковується більшості вимог стандарту.

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

Серед переваг мови Сі потрібно відзначити основні:

     універсальність (використовується майже на всіх існуючих ЕОМ);

     компактність та універсальність коду;

     швидкість виконання програм;

     гнучкість мови;

     висока структурованість.

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

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

      Лексеми в свою чергу утворюються із символів, вирази - із лексем і символів, оператори - із символів, лексем і виразів.

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

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

    Вираз задає правило обчислення деякого значення.

    Оператор задає кінцевий опис деякої дії.

Алфавіт

      Алфавіт мови Сі включає :

    великі та малі літери латинської абетки;

    арабські цифри;

    пробільні символи : пробіл, символи табуляції, символ переходу на наступний рядок тощо;

    символи , . ; : ? ' ! | / \ ~ ( ) [ ] { } < > # % ^ & - + * =

Ідентифікатори

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

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

Sum sum sUm SUM sUM

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

Константи

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

      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.

Коментарі

      Текст на Сі, що міститься у дужках /* та */ ігноруватиметься компілятором, тобто вважатиметься коментарем до програми. Такі коментарі можуть розміщуватися в будь-якому місці програми.

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

/*функція обчислює cуму матриць */

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

Ключові слова

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

      Наводимо перелік ключових слів мови Сі :

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

 

 

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

Функція main() : з цього все починається

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

Мінімальна програма на мові Сі має вигляд:

main()

{

    return 0;

}

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

      Мінімальна програма має лише один оператор - оператор повернення значення return. Він завершує виконання програми та повертає в нашому випадку деяке ціле значення (ненульове значення свідчить про помилки в програмі, нульове про успішне її завершення). Виконання навіть цієї найпростішої програми, як і решти багатьох, проходить у декілька етапів (рис 1.1.).

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

 

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

Базові типи даних

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

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

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

      Базові типи даних Сі можна перерахувати у наступній послідовності:

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():

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

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

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

За призначенням операції можна поділити на :

    арифметичні операції;

    операції присвоювання;

    операції відношення;

    логічні операції;

    порозрядні операції;

    операція обчислення розміру sizeof();

    умовна операція ?;

    операція слідування (кома).

Арифметичні операції

      До арифметичних операцій належать відомі всім бінарні операції додавання, віднімання, множення, ділення та знаходження залишку від ділення (таблиця 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);

}

Операції присвоювання

      В мові Сі знак = не означає "дорівнює". Він означає операцію присвоювання деякого значення змінній. Тобто зміст рядка вигляду "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.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.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

 

Порозрядні операції (побітові операції)

      Порозрядні операції застосовуються тільки до цілочисельних операндів і "працюють" з їх двійковими представленнями. Ці операції неможливо використовувати із змінними типу 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)

Операція слідування (кома)

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

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;

     Приклад 2.

int a[10],sum,i;

/* ... */

for (i=1,sum=a[0];i<10;sum+=a,i++) ;

Умовна операція ?:

     Умовна операція ?: - єдина тернарна операція в мові Сі. Її синтаксис :

умова ? вираз_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*/

Операція sizeof()

     Дана операція обчислює розмір пам'яті, необхідний для розміщення в ній виразів або змінних вказаних типів.

     Операція має дві форми :

1). ім'я_типу А;

     sizeof А;

2). sizeof (ім'я_типу);

     Операцію sizeof() можна застосовувати до констант, типів або змінних, у результаті чого буде отримано число байт, що відводяться під операнд. Приміром, sizеof(int) поверне число байт для розміщення змінної типу int.



 

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

Вираз представляє собою об'єднання операцій і операндів. Найпростіший вираз складається з одного операнду.

Приклади виразів :

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) складається з представників наступних класів : присвоювання, виклик функції та розгалуження. У тому і є гнучкість Сі, що є можливість змішування в одному операторі операторів різних класів. Проте навряд чи слід цим зловживати - програма може вийти правильною, проте надто заплутаною та нечитабельною.

Оператор розгалуження if

        Оператор розгалуження призначений для виконання тих або інших дій в залежності від істинності або хибності деякої умови. Основний оператор цього блоку в Сі - if ... else не має ключового слова then, як у Паскалі, проте обов'язково вимагає, щоб умова, що перевіряється, розміщувалася б у круглих дужках. Оператор, що слідує за логічним виразом, є then- частиною оператору if...else.

        Синтаксис оператора :

if (<умова>)

<оператор1>;

[else <оператор2;>]

Опис : image004

Рис. 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 дасть позитивний результат й відтоді буде отримано повідомлення про некоректність, виходячи з контексту наведеного прикладу.

 

Оператор 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.

 

Оператор циклу з передумовою while

      Оператор while використовується для організації циклічного виконання оператора або серії операторів, поки виконується певна умова.

      Синтаксис :

while (<логічний вираз>)

      оператор;

Опис : image005

Рис. 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;

}

 

Оператор циклу з постумовою do while

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

      Синтаксис :

do

      <оператор>;

while (<логічний_вираз>);

Опис : image006

Рис. 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);

}

 

Оператор розриву 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;

}

 

Оператор продовження 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.

 

Оператор циклу for

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

      Синтаксис оператора:

for([ініціалізація];[перевірка_умови];[нове_значення])

оператор ;

Опис : image007

Рис. 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;

}

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

 

Оператор переходу goto

      Синтаксис :

goto <мітка>;

/* ... */

<мітка> : <оператор>;

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

      Оператор, перед яким розташована <мітка> виконується зразу після виконання оператора goto.

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

      Приклад використання goto:

if (errorcode>0)

      goto exit;

 

exit :

return errorcode;

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

 

"Порожній" оператор

      Синтаксис :

;

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

for (i=0;i<10;printf("%d\n",i);) ;

 

"Складений" оператор

      "Складений" оператор представляє собою два або більше операторів. Його також називають "блоком".

      Синтаксис :

{

      [<оператори>]

}

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

if (i>0)

{

      printf("i == %d\n",i);

      i--;

}


 

 Що б там не було, але реальні програми важко уявити без використання операцій введення та виведення.

В мові Сі на стандартні потоки введення-виведення (в більшості випадків - клавіатура та монітор) завжди вказують імена 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

<![if !supportEmptyParas]> <![endif]>

Директиви включення

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

#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;

}

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

<![if !supportEmptyParas]> <![endif]>

Перетворення типу

      Компілятор Сі виконує автоматичне перетворення типів даних, особливо в математичних виразах, коли найчастіше цілочисельний тип перетворюється у тип з плаваючою комою, причому значення типу 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.16.

       Організація програми на Сі досить логічна. Мова Сі надає надзвичайно високу гнучкість для фізичної організації програми. Нижче наведена типова організація невеликого програмного проекту на Сі:

Опис : image014

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

       Структура кожної функції співпадає зі структурою головної функції програми main(). Функції іноді ще називають підпрограмами.

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

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

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

Етапи

Опис

Постановка задачі та її змістовний аналіз

1. Визначити вхідні дані, які результати необхідно отримати і в якому вигляді подавати відповіді.

2. Визначити за яких умов можливо отримати розв'язок

задачі, а за яких - ні.

3. Визначити, які результати вважатимуться вірними.

Формалізація задачі, вибір методу її розв'язання.

(математичне моделювання задачі)

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

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

3. Вибрати метод розв'язку задачі.

Складання алгоритму розв'язання задачі

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

Складання програми

Написання програми на мові програмування

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

Перевірка правильності роботи програми за допомогою тестів і виправлення наявних помилок.

Тест - це спеціально підібрані вхідні дані та результати, отримані в результаті обробки програмою цих даних.

Остаточне виконання програми, аналіз результатів

Після остаточного виконання програми необхідно провести аналіз результатів. Можлива зміна самого підходу до розв'язання задачі та повернення до першого етапу для повторного виконання усіх етапів.

 

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

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

 

Функції

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

Синтаксис опису функції має наступний вигляд :

тип_поверт_значення ім'я_функції ([список_аргументів])

{

       оператори тіла функції

}

Опис : image015

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

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

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

       Оголошенню функції можуть передувати специфікатори класу пам'яті extern або static.

       extern - глобальна видимість у всіх модулях (по замовчуванню);

       static - видимість тільки в межах модуля, в якому визначена функція.

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

 

Функції, що не повертають значення

       Функції типу 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(), що є вхідною точкою будь-якої системи. Якщо вона буде відсутня, завантажувач не зможе зібрати програму, про що буде отримано відповідне повідомлення.

 

Передача параметрів

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

#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() повертає символ кінця файлу. Це був типовий випадок проектування програми із застосуванням функціонального підходу.

 

Рекурсивні функції

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

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

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

void fn(int i)

{

     /* ... */

     fn(i);

     /* ... */

}

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

Опис : image016

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);

     /* ... */

}

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

     В якості прикладу розглянемо функцію, що рекурсивно обчислює факторіал. Як відомо, значення факторіала обчислюється за формулою: Опис : image031, причому Опис : image033і Опис : image035. Факторіал також можна обчислити за допомогою простого рекурентного співвідношення Опис : image037. Для ілюстрації рекурсії скористаємося саме цим співвідношенням.

#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! За рекурентним співвідношенням : Опис : image039. Таким чином, щоб обчислити 6! ми спочатку повинні обчислити 5!. Використовуючи співвідношення, маємо, що Опис : image041, тобто необхідно визначити 4!. Продовжуючи процес, отримаємо :

1). Опис : image039

2). Опис : image041

3). Опис : image045

4). Опис : image047

5). Опис : image049

6). Опис : image033

     В кроках 1-5 завершення обчислення кожний раз відкладається, а шостий крок є ключовим. Отримане значення, яке визначається безпосередньо, а не як факторіал іншого числа. Відповідно, ми можемо повернутися від 6-ого кроку до 1-ого, послідовно використовуючи значення :

6).1!=1

5).2!=2

4).3!=6

3). 4!=24

2). 5!=120

1). 6!=720

     Важливим для розуміння ідеї рекурсії є те, що в рекурсивних функціях можна виділити дві серії кроків.

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

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


 

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

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

 

Моделі пам'яті

       У мові Сі для операційної системи 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) ОП. Аналогічна великій моделі. Додатково в ній знімається обмеження на розмір окремої одиниці даних.

 

Основні операції над покажчиками

       Мова Сі надає можливість використання адрес змінних програми за допомогою основних операцій - & та *:

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

Призначення цих операцій:

       & ім'я змінної - одержання адреси; визначає адресу розміщення значення змінної визначеного типу;

       * ім'я-покажчика - отримання значення визначеного типу за вказаною адресою; визначає вміст змінної, розміщеної за адресою, що міститься у даному покажчику; це - непряма адресація (інші назви - "зняття значення за покажчиком" або "розіменування" ).

       Оператор присвоювання значення адреси покажчику має вигляд:

       Ім'я_змінної_покажчика = & ім'я змінної;

Наприклад:

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, 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 pf+i у+i &y\n");

     for (i = 0; i < 4; i++ )

          printf(" %d : %6u %6u %6u %6u %6u %6u\n", i, pi + i, x + i, &x, pf + i, y + i, &y);

}

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

 

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

i

pi+i

х+i

&x

pf+i

y+i

&y

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.

     Те саме значення можна представити у вигляді:

*(х + 0) == *х == х[0] - значення нульового елемента масиву х;

*(х + 2) == x[2] - значення другого елемента масиву х;

*(х + i) == x - значення i-го елемента масиву х.

     А операції над елементами масиву х можна представити у вигляді:

*х + 2== х[0] +2; *(х + i) - 3 == x - 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. визначити ім'я масиву, його розмірність (кількість вимірів) і розмір - кількість елементів масиву;

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, m2[j].

       Пересилання масивів у Сі немає. Але можна переслати масиви поелементно або сумістити масиви в ОП, давши їм практично те саме ім'я.

       Наприклад:

int *m1 = (int *) malloc(100 * sizeof(int));

int *m2 = (int *) malloc(100 * sizeof(int));

       Для пересилання елементів одного масиву в іншій можна використати оператор циклу:

for (i = 0; i < 100; i++ ) m2 = ml ;

       Замість m2 = m1 ; можна використовувати:

*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 - покажчик-змінна */

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

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

       Розглянемо способи оголошення і формування адрес частини масиву й елементів одновимірних і багатомірних масивів за допомогою покажчиків.

 

Оголошення та звертання в одновимірних масивах

       Форма оголошення одновимірного масиву з явною вказівкою кількості елементів масиву:

тип ім'я_масива [кількість-елементів-масива];

       Звертання до елементів одновимірного масиву в загальному випадку можна представити індексуванням, тобто у вигляді

ім'я-масива [вираз];

де ім'я-масиву - покажчик-константа;

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

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

int а[10];

іnt *p = а; /* - р одержує значення а */

       При цьому компілятор виділяє масив в стеку ОП розміром (sizeof(Type) * розмір-масиву ) байтів.

       У вищенаведеному прикладі це 2 * 10 = 20 байтів. Причому а - покажчик-константа, адреса початку масиву, тобто його нульового елемента,р - змінна; змінній р можна присвоїти значення одним із способів:

р = а;

р = &а[0];

р = &a;

де &а == (а + i) - адреса і-елемента масиву.

       Відповідно до правил перетворення типів значення адреси i-елемента масиву на машинному рівні формується таким чином:

а + i * sizeof(int);

       Справедливі також наступні співвідношення:

&a == a+0 == &a[0] - адреса а[0] - нульового елемента масиву;

а+2 == &а[2] - адреса а[2] - другого елементи масиву;

а+i == &a - адреса a - i-гo елемента масиву;

*а==*(а+0)==*(&а[0])==a[0] - значення 0-ого елемента масиву;

*(а + 2) == а[2] - значення а[2] - другого елементи масиву;

*(а + i) == а - значення a - 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) == a == p;

       Для a та p еквівалентні всі звертання до елементів a у вигляді:

a, *(a+i), *(i+a), i[a], та

p, *(p+i), *(i+p), i[p]

 

Оголошення та звертання до багатовимірних масивів

       У даному розділі розглянемо оголошення і зв'язок покажчиків і елементів багатомірних масивів - що мають 2 та більше вимірів.

       Багатомірний масив у мові Сі розглядається як сукупність масивів меншої розмірності. Наприклад, двовимірний масив - це сукупність одновимірних масивів (його рядків), тривимірний масив - це сукупність матриць, матриці - сукупності рядків, а рядок - сукупність елементів одновимірного масиву.

       Елементи масивів розташовуються в ОП таким чином, що швидше змінюються самі праві індекси, тобто елементи одновимірного масиву розташовуються підряд, двовимірного - по рядках, тривимірного - по матрицях, а матриці - по рядках.

       Для звертання до елементів багатомірного масиву можна використовувати нуль і більш індексів (індексних виразів):

ім'я-масиву [вираз1][вираз2] ...

       Наприклад, для звертання:

    до одновимірного масиву можна використовувати одно-індексний вираз (індекс);

    до двовимірного - 1 або 2 індексний вираз;

    до тривимірного - 1, 2 або 3 індексний вираз і т.д.

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

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

int а[m][n] ;

       Цей масив складається з m одновимірних масивів (рядків), у кожному з яких утримується n елементів (стовпців). При роботі з цим двовимірним масивом можна використовувати одно або 2 індексний вираз. Наприклад:

  а[j]- містить 2 індекси; використовується для звертання до елемента i-рядка, j-стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;

  a - містить 1 індекс; визначає адресу одновимірного масиву: адреса початку i-рядка масиву;

  а - не містить індексу і визначає адресу масиву, його нульового елемента.

Таким чином, звертання до двовимірних масивів за допомогою імені і тільки одного індексу визначає покажчик на початок відповідного рядка масиву (адреса його нульового елемента). Наприклад:

а[0] == &a[0][0] == a+0*n*sizeof(int);

а[1] == &а[1][0] == a+1*n*sizeof(int);

a == &a[0] == a+i*n*sizeof(int);

       Приклад оголошення тривимірного масиву:

int а[k][m][n] ;

де:

    k- кількість матриць з m рядками і n стовпцями;

    m - кількість рядків (одновимірних масивів) у матриці;

    n - кількість стовпців (елементів у рядку) матриці.

       Цей масив складається з матриць, кожна з яких складається з m одновимірних масивів (рядків) по n елементів (стовпців). При звертанні до цього масиву можна використовувати імена:

  a[l][j] - містить 3 індекси; використовується для звертання до елемента l-матриці, i-рядка. j-стовпця масиву; обчислюються індексні вирази, визначається адреса елемента масиву і вилучається його значення;

  a[k] - визначає одновимірний масив - адреса початку i-рядка; - матриці;

  a[k] - визначає двовимірний масив - адреса початку k - матриці, тобто нульового елемента його нульового рядка;

  а - адреса початку масиву, нульового елемента нульового рядка нульової матриці.

       Наприклад:

int b[3][4][5];

int i, *ip, *ipp;

i = b[0][0][1];

ip = b[2][0];

ipp = b[2];

де: ipipp - покажчики на значення типу 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) == &а[0] - адреса елемента i-рядка 0-стовпця;

*a == **(а + i) == *(&а) == a[0] - значення 0-го елемента i-рядка;

a[j] == *(*(а + i) + j) == *(a + j) == a[j] - значення елемента i-рядка j-стовпця масиву а;

де:

(а + i) == *(а + i) == a - адреса 0-го елемента i-рядка == &a[0];

(*(а + i) + j)- адреса j-елемента i-рядка = &a[j];

*(*(а + i) + j)- значення j-елемента i-рядка = a[j].

       Значення адреси початку i-рядка (адреси 0-елемента i-рядка) на машинному рівні формується у виді:

a = а + i == (a+i*n*sizeof(int)), де n - кількість значень в одному рядку.

       Таким чином, адреса (i+1)-рядка відстоїть від i-рядка на (n*sizeof(int)) байтів, тобто на відстань одного рядка масиву.

       Вираз a[j] компілятор Сі переводить в еквівалентний вираз:

*(*а + i) + j). Зрозуміло, запис a[j] більш традиційний у математиці і більш наочний.

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

int а[m][n], *р = а;

*(p+i*n+j) - значення j - елемента i-рядка ;

де: n - кількість елементів у рядку;

i*n + j - змішання а[j]- елемента відносно початку масиву а.

 

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

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

 

Робота з великими масивами

       Розмір одного масиву даних повинний бути не більше 64 Кб. Але в реальних задачах можуть використовуватися масиви, що вимагають ОП, більшої ніж 64 Кб. Наприклад, масив даних типу float з 300 рядків і 200 стовпців потребує для розміщення 300 * 200 * 4 = 240000 байтів.

Для вирішення поставленої задачі можна використовувати масив покажчиків і динамічне виділення ОП для кожного рядка матриці. Рядок матриці не повинен перевищувати 64 Кб. У вищенаведеному прикладі ОП для рядка складає всього 800 байтів. Для виділення ОП з купи кожен рядок повинний мати покажчик. Для всіх рядків масиву треба оголосити масив покажчиків, по одному для кожного рядка. Потім кожному рядку масиву виділити ОП, привласнивши кожному елементу масиву покажчиків адресу початку розміщення рядка в ОП, і заповнити цей масив.

       У запропонованому лістингу представлена програма для роботи з великим масивом цілих значень: з 300 рядків і 200 стовпців. Для розміщення він вимагає: 200 * 300 * 2 = 120000 байтів. При формуванні великого масиву використовується р - статичний масив покажчиків

       При виконанні програми перебираються i-номери рядків масиву. Для кожного рядка за допомогою функції malloc() виконується запит ОП з купи і формується p - значення покажчика на дані i-рядки. Потім перебираються i-номери рядків від 1 до 200. Для кожного рядка перебираються j-номери стовпчиків від 1 до 300. Для кожного i та j за допомогою генератора випадкових чисел формуються і виводяться *(р + j) - значення елементів масиву. Після обробки масиву за допомогою функції free(p) звільняється ОП виділена i-рядку масиву.

У наведеній нижче програмі використовуються звертання до Ai,j - елементів масиву у вигляді: *(p+j), де p + 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 = (int*) malloc (300 * sizeof (int));

    for (i = 0; i < 200; i++)

        for (j = 0; j < 300; j++ )

        {

            *(p + j ) = random(100);

            printf("%3d", *(p + j ));

            if ( (j + 1) % 20 == 0 )

                printf ("\n" ) ;

        }

        /* Звільння ОП рядків великого масиву: */

        for ( i=0; i < 200; i++ )

            free( p );

}

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

 

Вільні масиви та покажчики

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

 

    У масиві на рис. 1.12 три рядки; у нульовому стовпці кожного рядка стоїть кількість елементів даного рядка. Далі - значення елементів матриці.

Приклад оголошення вільного масиву цілих, тобто статичного масиву покажчиків на дані типу int:

int *а[100];

    Для масиву а приділяється ОП для 100 покажчиків на значення цілого типу, по одному покажчику на кожний з 100 рядків вільного масиву. Після визначення кількості елементів рядка для значень рядка повинна бути виділена ОП і сформоване значення покажчика в змінній a. Цей покажчик посилається на область ОП, виділену для значень і-рядка матриці. Тільки після цього можна заносити в цю ОП значення елементів вільного масиву.

    Реально ОП - це лінійна послідовність перенумерованих байтів. Елементи рядків вільного масиву можуть бути розташовані підряд або несуміжними відрізками ОП, виділеними для рядків.



 

Символьний рядок представляє собою набір з одного або більше символів.

       Приклад : "Це рядок"

       В мові Сі немає спеціального типу даних, який можна було б використовувати для опису рядків. Замість цього рядки представляються у вигляді масиву елементів типу 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. Функції введення рядків.

       Прочитати рядок із стандартного потоку введення можна за допомогою функції 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() повертає додатне значення, якщо менше, то - від'ємне.


Математические формулы. Шпаргалка для ЕГЭ с математики

Формулы сокращенного умножения

(а+b)2 = a2 + 2ab + b2

(а-b)2 = a2 – 2ab + b2

a2 – b2 = (a-b)(a+b)

a3 – b3 = (a-b)( a2 + ab + b2)

a3 + b3 = (a+b)( a2 – ab + b2)

(a + b)3 = a3 + 3a2b+ 3ab2+ b3

(a – b)3 = a3 – 3a2b+ 3ab2- b3

Свойства степеней

a0 = 1 (a≠0)

am/n = (a≥0, n ε N, m ε N)

a- r = 1/ a r (a>0, r ε Q)

m...

Русский язык и культура речи

перейти к оглавлению

1. ЭЛЕМЕНТЫ И УРОВНИ ЯЗЫКА

Характеризуя язык как систему, необходимо определить, из каких элементов он состоит. В большинстве языков мира выделяются следующие единицы: фонема (звук), морфема, слово, словосочетание и предложение. Единицы языка неоднородны по своему строению: простые (фонемы) и сложные (словосочетания, предложения). При этом более сложные единицы всегда состоят из более простых.

Самая простая единица языка – это фонема, неделимая и сама по себе...

законы диалектики

Основные законы диалектики.

1)Закон единства и борьбы противоположностей.

Этот закон является «ядром» диалектики, т.к. определяет источник развития, отвечает на вопрос, почему оно происходит.

Содержание закона: источник движения и развития мира находится в нем самом, в порождаемых им противоречиях.

Противоречие – это взаимодействие противоположных сторон, свойств и тенденций в составе той или иной системы или между системами. Диалектическое противоречие есть только там, где...

Идеология

1.Идеология как социальный феномен, её сущность. Содержание идеологииСоциально-исторической системой представлений о мире стала идеология как система рационально- логического обоснования поведения людей, их ценностей, норм взаимоотношений, целей и т.д. Идеология как явление во многом сходна с религией и с наукой. От науки она восприняла доказательность и логичность своих постулатов, но, в отличие от науки, идеология призвана давать оценку явлениям действительности (что хорошо, что...