Professional Documents
Culture Documents
опорний конспект ООП укр
опорний конспект ООП укр
З НАВЧАЛЬНОЇ ДИСЦИПЛІНИ
”Об′єктно-орієнтоване програмування”
Дніпро
2
Структура программи
#include <stdio.h> //1 #include <iostream.h> //1
#include <conio.h> //1
int sum(int,int); //2 int sum(int,int); //2
int a,b; //2
int main () //3 void main () //3
{ //4 {
scanf ("%d", &a); //4б int a,b; //4а
scanf ("%d", &b); //4б cin >> a>>b; //4б
printf ("a+b=%5d\n”, sum(a,b)); //4в cout<<"a+b=”<<sum(a+b)<< “\n"; //4в
return 0; //4г }
}
int sum(int a,int b){return (a+b);} //5 int sum(int a, int b){return (a+b);} //5
При аналізі наведеної програми можна виділити наступні великі частини типового вихідного
модуля:
1) підключення заголовних файлів бібліотек (з функціями, константами і ін.);
2) оголошення користувальницьких символічних констант, прототипів допоміжних функцій,
класів і типів даних, глобальних змінних;
3) заголовок головної функції
4) визначення (тіло) головної функції - блок, що містить:
• оголошення локальних змінних і констант і їх ініціалізацію (присвоєння початкових значень);
// 4а
• введення вихідних даних (діалог з користувачем); // 4б
• обробка - звернення до функцій, обчислення виразів, виконання операторів;
• виведення результату; //4 в
• повернення коду завершення головної функції // 4г
5) визначення користувальницьких допоміжних функцій і методів класів;
Подібне структурування тексту вихідного модуля, який розміщується в одному файлі з
розширенням срр, не єдино можлива. Але наслідування даного зразку полегшує розуміння і
налагодження програм.
5) Після визначення головної функції йде визначення допоміжної функції sum(a, b). У тілі
функції є один оператор повернення (return). В результаті виконання цього оператора буде
обчислено значення суми двох змінних і отриманий результат буде повернений в місце виклику
функції. Для функції main типу int можна опускати оператор return 0.
Для зручності фігурна дужка, що закриває блок визначення (реалізації) функції, коментується
ім'ям функції.
Використання процесора.
Препроцесором називається перша фаза компілятора. Препроцесор входить до складу системи
програмування С++ та забезпечує обробку вихідного тексту програми перед її компіляцією. В ході
препроцесорної обробки програми реалізуються наступні дії:
1. виконується підстановка макророзширеннь замість імен макросів і їх аргументів.
2. в вихідний текст програми включаються файли, що містять допоміжну інформацію (описи
об'єктів, класів, прототипи функцій і т. п.)
3. Виконується умовна компіляція програми - визначення остаточного вигляду вихідного
тексту програми шляхом залишення або виключення окремих її фрагментів.
Завдання препроцесору описується за допомогою директив препроцесора, які починаються зі
знака #, що розміщується з першої позиції в рядку, перед яким у рядку можуть перебувати тільки
пробільні символи.
Препроцесорна обробка дозволяє скоротити час розробки програм, підвищити їх наочність і
мобільність. Зокрема, при зміні характеристик апаратно-програмних засобів досить змінити вміст
файлів, які підключаються.
Директива #include
Директива #include <ім'я файлу> вставляє вміст зазначеного файлу в ту точку вихідного
фала, де вона записана. Файл, який включається також може містити директиви #include. Пошук
файлу, якщо не вказано повний шлях, ведеться в стандартних каталогах включення файл. Замість
кутових дужок можуть використовуватися лапки («») - в цьому випадку пошук файлу ведеться в
каталозі, що містить вихідний файл, а потім вже в стандартних каталогах.
Директива #include є найпростішим засобом забезпечення узгодженості оголошень в різних
файлах, вона включає до них інформацію про інтерфейс із заголовних файлів.
Заголовны файли зазвичай мають розширення .h і можуть містити:
Визначення типів, констант, вбудованих функцій, шаблонів, перерахувань (злічень);
Оголошення функцій, даних, імен, шаблонів;
Простори імен;
Директиви процесора;
Коментар.
В заголовному файлі не повинно бути визначень функцій і даних. Ці правила не є вимогами
мови, але також відображають розумний спосіб використання директиви.
При вказівці заголовних файлів стандартної бібліотеки розширення .h можна опускати. Більш
ранні версії компіляторів можуть не підтримувати (це свіжа вимога стандарту).
Директива #define
Директива #define визначає підстановку в тексті програми. Вона використовується для визначення:
Символічних констант:
#define ім'я текст_підстановки
(Всі входження імені замінюються на текст_підстановки);
5
Макросів, які виглядають як функції, але реалізуються підстановкою їх тексту в текст
програми:
#define ім'я (параметри) текст_підстановки
Символів, керуючих умовною компіляцією. Вони використовуються разом з директивами
#ifdef і #ifndef. формат:
#define ім'я
Приклади:
#define VERSION 1
#define VASIA "Василь Іванович"
#define MAX (x, y) ((x)> (y)? (x) :( y))
#define MUX
Імена рекомендується записувати прописними буквами, щоб візуально відрізняти їх від імен змінних і
функцій. Параметри макросу використовуються при макропідстановці, наприклад, якщо в тексті програми
використовується виклик макросу y = MAX(sum1, sum2) :, він буде замінений на
y =((sum1,)>(sum2)?(sum1) :( sum2));
Відсутність круглих дужок може привести до неправильного порядку обчислення, оскільки препроцесор не
оцінює вставляється текст, щододається, з точки зору синтаксису. Наприклад, якщо до макросу #define
sqr(x)(x*x) звернутися як sqr(y+1), в результаті підстановки отриаємо вираз (y+1*y+1).
Макроси і символічні константи встановлені з мови С, при написанні програм на С++ їх слід уникати. Замість
символічних констант краще використовувати const або enum, а замість макросів - вбудовані функції і
шаблони.
Простір імен
Іменовані області служать для логічного групування оголошень і обмеження доступу до них. Чим
більше програма, тим більш актуально використання іменованих областей. Найпростішим прикладом
застосування є відділення коду, написаного однією людиною, від коду, написаного іншим. При використанні
єдиного глобального контекста форматувати програму з окремих частин дуже складно через можливі збіги і
конфлікти імен. Використання іменованих областей (простору імен, namespace) перешкоджає доступу до
непотрібних засобів.
Простір імен (namespace) являє собою деяку оголошувану за допомогою спеціальної конструкції
область, яка служить для локалізації (обмеження області дії) імен змінних і об'єктів з метою уникнення
конфліктів при використанні однакових імен. Ситуація, коли в програмі застосовується кілька однакових
ідентифікаторів, зазвичай виникає у разі використання бібліотек функцій і класів різних виробників. При
використанні простору імен, крім необов'язкового глобального простору імен, виділяється кілька приватних
просторів. У різних приватних просторах можна використовувати однакові ідентифікатори змінних, класів і
об'єктів, які беруть участь в рамках свого приватного простору імен і мають в ньому свій сенс.
Простір імен задається наступним чином:
namespace ім'я
{
// оголошення
}
тут:
namespace - ключове слово, що задає простір імен; ім'я - будь-який ідентифікатор.
У фігурних дужках знаходиться сам простір імен, що має назву із заданим ім'ям. В результаті такого
визначення простору все імена, що входять в даний простір імен, будуть видимі тільки всередині введеного
простору.
Приклад 1. Визначення і використання простору імен.
#include <iostream>
namespace A
{ int x,y;
class M
{
public:
void wodx(int с) {х=с;}
int dostx() {return x; }
}
void vyvod (int k) {cout << ;}
}
9
void main()
{ int z,t,y;
y=3;
A::y=7;
A::M b;
z=9;
b.vvodx (z) ;
A::x=z;
cout << "\n A::x=";
A::vyvod(A::x);
cout << "\n y=";
A::vyvod(y);
cout << "\n A::y=" << A::y;
t=b.dostx() ;
cout << "\n t=" << t;
}
В результаті роботи програми на екран буде видано наступні повідомлення:
А::х=9
У=3
А::у=7
t=9
У прикладі змінні х, у, клас М і функція vyvod() знаходяться в області видимості, визначеної простором
імен А. Опис функції main() знаходиться в глобальному просторі імен. У глобальному просторі імен видимі
змінні r, t і у. Таким чином, в програмі оголошені дві змінні з одним ім'ям у: перша видима в просторі імен
А, а друга - в глобальному просторі.
Застосування імен ідентифікаторів всередині простору імен проводиться звичайним чином. При
використанні імен ідентифікаторів іншого простору імен необхідно застосовувати операцію :: вказівки
області видимості. Наприклад, у функції main() оператор А::x=z; присвоює значення змінної z (з
глобального простору імен) змінній х, що належить простору імен A. Для оголошення об'єкта b класу М, опис
якого знаходиться в просторі імен А, необхідно також використовувати операцію :: вказівки області
видимості при оголошенні об'єкту: А :: М b ;.
Існує можливість введення більш одного простору імен з одним і тим же ім'ям, причому наступні
оголошення розглядаються як розширення попередніх. Це дозволяє розділити простір імен на кілька файлів
або на кілька частин всередині одного файлу.
Наприклад:
namespace L { int x;}
namespace L { float у;}
Тут оголошені дві частини одного простору імен L.
Пример 2.
namespace demo
{ int i = 1
int k = 0
void funcl (int);
void func2 (int) { /* … */ }
}
namespace demo{ // Розширення
// int I = 2 невірно - подвійне визначення
void funcl (double); // Перевантаження
void func2 (int); // Вірно (повторне оголошення)
}
В оголошенні іменованої області можуть бути присутніми як оголошення, так і визначення. Логічно
поміщати в неї тільки оголошення, а визначати їх пізніше за допомогою імені області та оператора доступу до
області видимості ::, наприклад:
void demo :: funcl (int) {/* … */}
Це застосовується для поділу інтерфейсу і реалізації. Таким способом не можна оголосити новий елемент
простору імен.
10
Об'єкти, оголошені всередині області, є видимими з моменту оголошення. До них можна явно звертатися за
допомогою імені області та оператора доступу до області видимості ::, наприклад:
demo :: i = 100; demo :: func2(10);
Якщо необхідно часто застосовувати операцію вказівки області видимості, то доцільно використовувати
оператор using, який дозволяє отримати доступ до конкретного імені в просторі імен, або до всіх імен
простору імен.
Для отримання доступу до окремого імені в просторі імен використовується оператор в наступному
форматі:
using імя_пространства_імен :: ім'я;
Наприклад, для того, щоб забезпечити доступ до змінної х простору імен А з функції main() приведеної
програми, досить виконати оператор using A :: х.
Для забезпечення доступу до всіх імен деякого простору імен використовується оператор using в
наступному форматі:
using namespace імя_пространства_імен;
Після виконання такого оператора вказаний простір імен додається до поточної області видимості.
Оператори using і using namespace можна використовувати і всередині оголошення пойменованої області,
щоб зробити в ній доступними оголошення з іншої області:
namespace Department_of_Applied_Mathematics
{ using demo:: i;
// …
}
Імена, оголошені в пойменованій області явно або за допомогою оператора using, мають пріоритет по
відношенню до імен, оголошених за допомогою оператора using namespace (це має значення при
включенні декількох названих областей, що містять співпадаючі імена).
Короткі імена просторів імен можуть увійти в конфлікт один з одним, а довгі непрактичні при написанні
реального коду, тому допускається вводити синоніми імен:
namespace DAM = Department_of_Applied_Mathematics;
Для зменшення ймовірності конфлікту імен в останніх версіях компіляторів бібліотека C ++ визначена у
власному просторі імен std. Використання оператора using namespace std; дає можливість додати в
поточну область видимості все імена, що містяться всередині бібліотеки C ++.
У мові C ++ можна оголосити безіменний простір імен:
namespace {// оголошення }
Безіменні простори імен дають можливість повністю закрити доступ ззовні до імен простору імен, оскільки
отримати доступ до імен за допомогою оператора using або операції :: неможливо, т.к. відсутнє ім'я у
простору імен.
Якщо ім'я області не задано, компілятор визначає його самостійно за допомогою унікального
ідентифікатора, різного для кожного модуля. Оголошення об'єкта в різних неіменованій області рівнозначно
його опису як глобального з модифікатором static. Поміщати оголошення в таку область корисно для того,
щоб зберегти локальність коду. Не можна отримати доступ з одного файлу до елементу неіменованої області
іншого файлу.
Приклад 2. Застосування декількох просторів імен.
#include <iostream>
//Визначення першого простору імен
namespace name_A
{ int i;
class Р
{
int х;
public:
P(int newy) {x=newy;}
void izmx(int c) {x=c;}
int dost() {return x;}
};
char str[]="\n Простір імен";
}
11
//Визначення першої частини другого простору імен
namespace name_B
{ int у;
}
//Визначення другої частини другого простору імен
namespace name_B
{int z; }
int main()
{
//Створення об’єкта b класа Р
name_A :: P b(2) ;
cout<<"\n Значення елемента даних х об’єкта b:"<<b.dost();
cout << endl;
b.izmx (15);
cout<<"Нове значення елемента даних х об’єкта b:"<<b.dost();
//Завдання видимості рядка str в поточному просторі імен
using name_A :: str;
cout << str << endl;
// Завдання видимості простору імен name_A в поточному просторі імен
using namespace name_A;
for (i=l;i<ll;i++)
cout << i << " ";
//Використання обох частин другого простору імен
name_B :: у=100;
cout << "\n name_B :: у=" << name_B :: у;
name_B :: z=200;
cout << "\n name_B :: z=" << name_B :: z;
//Приєднання простору імен name_B до поточного простору імен
using namespace name_B;
Р by(у), bz(z);
cout<<"\n Значення елемента даних х об’єкта by:"<<by.dost();
cout<<"\n Значення елемента даних х об’єкта bz:"<<bz.dost();
cout << endl;
return 0;
}
В результаті роботи програми на екран буде виведено наступне:
Значення елемента даних х об’єкта b:2
Нове значення елемента даних х об’єкта b:15
Простір імен
12345678910
name_B :: у=100
name_B :: z=200
Значення елемента даних х об’єкта by:100
Значення елемента даних х об’єкта bz:200
У наведеній програмі визначені два простори імен: name_А і name_B. Крім того, існує глобальний простір
імен програми. У просторі імен name_А видимі клас Р, змінна i і рядок str. Простір name_В складається з
двох частин. У першій частині простору name_В видима змінна у, а в другій - змінна z. Опис функції main()
знаходиться в глобальному просторі імен.
При розширенні області видимості за допомогою оператора using необхідно стежити за тим, щоб в
отриманому спільному просторі імен були відсутні однакові імена. У разі наявності таких, чинним є ім'я, яке
оголошено в поточному просторі імен.
Механізм просторів імен разом з директивою #include забезпечують необхідну при написанні великих
програм гнучкість шляхом поєднання логічного групування пов'язаних величин і обмеження доступу.
Як правило, в будь-якому функціонально закінченому фрагменті програми можна виділити інтерфейсну
частину (наприклад, заголовки функцій, опису типів), необхідну для використання цього фрагмента, і частину
реалізації, тобто допоміжні змінні, функції та інші засоби, доступ до яких ззовні не потрібен. Простори імен
дозволяють приховати деталі реалізації і, отже, спростити структуру програми і зменшити кількість
потенційних помилок. Продумане розбиття програми на модулі, чітка специфікація інтерфейсів і обмеження
доступу дозволяють організувати ефективну роботу над проектом групи програмістів.
12
Таблиця 5. Операції С++
Унарні:
& отримання адреси операнда
* звертання за адресою (розіменування)
+ унарний плюс
- унарний мінус, змінює знак арифметичного операнда
~ поразрядне інвертування внутрішнього двійкового кода (побітовое заперечення)
! логичене заперечення (НЕ). Вякості логічних значень використовуються 0 - брехня и
не 0 - істина, запереченям 0 буде 1, запереченням будь-якого ненульового числа буде
0.
++ збільшення на одиницю:
префіксна операція - збільшує операнд до його використання,
постфіксна операція - збільшує операнд після його використання.
-- зменшення на одиницю:
префіксна операція - зменшує операнд до його використання,
постфіксна операція - зменшує операнд після його використання.
sizeof обчислення розміру (в байтах) для об’єкта того типу, який має операнд
new виділення пам'яті
delete звільнення пам'яті
:: доступ до області видимості
(тип)значение перетворення типу
. вибор елемента за іменем
-> вибор елемента за вказівником
[] вибор елемента за індексом
.* вибор елемента за іменем через вказівник
->* вибор елемента за вказівником через вказівник
() виклик функції
typeid идентифікація типу (запрошує дані про тип)
const_cast перетоворення константної змінної в неконстантну
dynamic_cast динамичне перетворення типів (має сенс для поліморфних класів)
static_cast перетворення одного типу в інший, але вона не може буть використана для виконання
неприпустимого перетворення (наприклад, значення у вказівник)
reinterpret_cast операція приведення типів даних, не може бути використана для приведення
іерархії класів або перетоврення константних змінних
Бінарні операції:
Аддитивні:
+ бінарний плюс (додавання арифметичних операндів)
- бінарний минус (віднімання арифметичних операндів)
Мультиплікативні:
* множення операндів арифметичного типу
/ ділення операндів арифметичного типу (якщо операнды цілочислові, то виконується цілочислове
ділення)
% отримання остачі від ділення цілочислових операндів
Операції зсуву (визначені тільки для цілочислових операндів).
Формат виразу з операцією зсуву:
операнд_лівий операція_зсуву операнд_правий
<< зсув вліво бітового подання значення лівого цілочисельного операнда на кількість розрядів, що
дорівнює значенню правого операнда
>> зсув вправо бітового подання значення правого цілочисельного операнда на кількість розрядів, що
дорівнює значенню правого операнда
Поразрядні операції:
& поразрядна кон'юнкція (І) бітових подань значень цілочислових операндів
| поразрядна диз'юнкція (АБО) бітових подань значень цілочислових операндів
^ поразрядне виключаення АБО бітових подань значень цілочислових операндів
13
Логичні бинарні операції:
&& кон'юнкція (І) цілочислових операндів або відношень, цілочисельний результат неправда (0) або
істина (1)
|| диз'юнкція (АБО) цілочислових операндів або відношень, цілочисельний результат неправда (0) або
істина (1)
Операції сравнения:
< менше, чим
> більше, чим
<= менше або дорівнює
>= більше або дорівнює
== дорівнює
!= не дорівнює
Операції присвоєння:
= просте присвоєння
*= присвоїти результат множення
/= присвоїти результат ділення
%= присвоїти остаток от ділення
+= присвоїти результат додавання
-= присвоїти результат віднімання
<<= присвоїти результат зсуву вліво
>>= присвоїти результат зсуву вправо
&= присвоїти результат поразрядної (побітової) кон'юнкції
^= присвоїти результат поразрядного (побітового) виключающого АБО
|= присвоїти результат поразрядної (побітової) диз'юнкції
Післядовність виразів:
, послідовне виконання виразів
Тернарна операція:
? : якщо вираз до ? не дорівнює 0, виконується оператор до :
якщо дорівнює 0, виконується оператор після :
14
Таблиця 6. Пріоритети операцій С++
При- Знаки операцій Назва операцій Порядок
орі- обчислення
тет
1 :: доступ до області видимості зліва
2 . вибор елемента за іменем зліва
-> вибор елемента за вказівником
[] вибор елемента за індексом
() виклик функції або конструювання значення
++ постфіксний інкремент
-- постфіксний декремент
typeid ідентифікація типу
dynamic_cast перетворення з перевіркою при виконанні
static_cast перетворення з перевіркою при компіляції
reinterpret_cast перетворення без перевірки
const_cast константне перетворення
3 sizeof розмір операнда в байтах зправа
++ префіксний інкремент
-- префіксний декремент
~ інверсія (поразрядне НЕ)
! логічне НЕ
+ унарний плюс
- унарний мінус
& адреса
* розйменування
new виділення пам'яті або створення
delete звільнення пам'яті або знищення
(имя_типу) перетворення типу
4 .* вибір елемента за іменем через вказівник зліва
->* вибір елемента за вказівником через вказівник
5 * множення зліва
/ ділення
% остача від ділення цілих (ділення за модулем)
6 + додавання зліва
- віднімання
7 << зсув вліво зліва
>> зсув вправо
8 < менше зліва
> більше
<= менше або дорівнює
>= більше або дорівнює
9 == дорівнює зліва
!= не дорівнює
10 & поразрядне І зліва
11 ^ поразрядне виключаче АБО зліва
12 | поразрядне АБО зліва
13 && логическое І зліва
14 || логическое АБО зліва
15 ?: умовна (тернарна) зправа
16 = присвоєння (просте та складені) зправа
*=, /=, %=
+=, -=
<<=, >>=
&=, ^=, |=
17 throw генерація виключень зправа
18 , послідовність виразів зліва
15
Перетворення Типів
Велике розмаїття типів в C ++ дозволяє обрати тип, що задовольняє необхідним вимогам.
Однак крім користі це розмаїття ще й ускладнює комп'ютеру життя. Наприклад, при складанні двох
значень, що мають тип short, можуть використовуватися апаратні інструкції, відмінні від
застосовуваних при складанні двох значень long. При наявності 11 цілочісловіх типів і 3 типів чисел з
плаваючою точкою комп'ютер стикається з безліччю випадків їх обробки, особливо якщо змішуєте
різні типи. Щоб впоратися з потенційною плутаниною в типах, багато перетвореннь типів в C ++
здійснюються автоматично.
• C ++ перетворює значення під час присвоєння значення одного арифметичного типу
змінній, яка відноситься до іншого арифметичного типу.
• C ++ перетворює значення при комбінуванні різних типів в виразах.
• C ++ перетворює значення при передачі аргументів функціям.
Перетворення бувають двох типів, які:
1. змінюють внутрішнє подання величин (з втратою точності або без втрати точності);
2. змінюють тільки інтерпретацію внутрішнього уявлення.
До першого типу відносяться, наприклад, перетворення цілого числа в дійсне (без втрати
точності) і навпаки (можливо, з втратою точності), до другого - перетворення знакового цілого в
беззнакове.
Якщо ви не розумієте, що відбувається під час автоматичного перетворення типів, то деякі
результати виконання ваших програм можуть виявитися дещо несподіваними.
При ініціалізації с4 відомо, що х має значення 66, але для компілятора х - це змінна, яка,
ймовірно, може мати якесь інше, набагато більше значення. В обов'язки компілятора не входить
відстеження того, що може статися зі змінною х між моментом її ініціалізації і моментом, коли вона
задіяна в спробі ініціалізації с4.
Перетворення у виразах
У вираз можуть входити операнди різних типів. Якщо операнди мають однаковий тип, то
результат операції матиме той же тип. Якщо операнди різного типу, перед обчисленнями
виконуються перетворення типів за певними правилами, що забезпечує перетворення більш коротких
типів в більш довгі для збереження значущості і точності. У подібних випадках C ++ виконує два
види автоматичних перетворень. По-перше, деякі типи автоматично перетворюються всюди, де вони
зустрічаються. По-друге, деякі типи перетворюються, коли вони скомбіновані з іншими типами в
виразі.
Спочатку розглянемо автоматичні перетворення. Коли C ++ оцінює вирази, значення bool,
char, unsigned char, signed char і short перетворюються в int. Зокрема, значення true
перетвориться в 1, a false - в 0. Такі перетворення називаються цілочисельними розширеннями. Як
приклад розглянемо наступні оператори:
Приведення типу не змінює значення самої змінної thorn; замість цього створюється нове
значення зазначеного типу, яке потім можна використовувати в виразі, наприклад:
cout << int ('Q'); // відображає цілочисельний код для 'Q'
Синтаксис:
Перша форма являє стиль С, а друга - стиль C ++. Ідея нової форми полягає в тому, щоб
оформити приведення типу точно так же, як і виклик функції. В результаті приведення для
вбудованих типів будуть виглядати так само, як і перетворення типів, що розробляються для
визначених користувачами класів.
Оператори
Оператор переходу goto мітка - передає управління на оператор з міткою.
Оператор безумовного переходу goto має формат:
goto мітка;
Використовується для виходу з вкладених керуючих операторів.
Область дії обмежена поточної функцією.
У тілі тієї ж функції повинна бути присутня рівно одна конструкція виду:
Мітка: оператор;
Мітка - це звичайний ідентифікатор, область видимості якого є функцією, в тілі якої він
заданий.
Приклад goto ABC;
Використання оператора безумовного переходу виправдано в двох випадках:
20
1. примусовий вихід вниз по тексту програми з декількох вкладених циклів або перемикачів;
2. перехід з декількох місць функції в одне (наприклад, якщо перед виходом з функції завжди
необхідно виконувати будь-які дії).
В інших випадках для запису будь-якого алгоритму існують більш підходящі засоби, а
використання goto призводить тільки до ускладнення структури програми і утруднення
налагодження. Застосування goto порушує принципи структурного і модульного програмування, за
якими всі блоки, з яких складається програма, повинні мати тільки один вихід і один вхід.
У будь-якому разі не слід передавати управління всередину операторів if, switch і циклів.
Не можна переходити всередину блоків, що містять ініціалізацію змінних, на оператори розташовані
після неї, оскільки в цьому випадку ініціалізація не буде виконана:
int k; …
goto Mitka; …
{int a = 3, b = 4;
k = a + b;
Mitka: int m = k + 1 …
}
Після виконання цього фрагмента програми значення змінної m не визначено
Цикли
Найпоширеніші помилки програмування циклів - використання в тілі циклу неініціалізованих
змінних і невірний запис умови виходу з циклу.
Щоб уникнути помилок рекомендується:
• перевірити, чи всім змінним, які зустрічаються в правій частині операторів присвоєння в тілі циклу,
присвоєні до цього початкові значення;
• перевірити, чи змінюється в циклі хоча б одна змінна, що входить в умову виходу з циклу;
• передбачити аварійний вихід з циклу по досягненню деякої кількості ітерацій;
• і звичайно, не забувати про те, що якщо в тілі циклу потрібно виконати більше одного оператора,
потрібно укладати їх у фігурні дужки.
Оператори циклу взаємозамінні. Але можна навести деякі рекомендації по вибору найкращого в
кожному конкретному випадку.
Оператор do while зазвичай використовують, коли цикл потрібно виконати хоча б раз.
Оператором while зручніше користуватися у випадках, коли число ітерацій заздалегідь невідомо,
очевидних параметрів циклу немає.
Оператор for кращий в більшості інших випадків (однозначно для організації циклів із лічильниками).
В С++ він є більш гнучким засобом, ніж аналогічні оператори циклів в інших мовах програмування.
Синтаксис:
#include <stdlib.h>
int rand (void);
/ * (В модулі stdlib.h) * /
#define RAND_MAX 32767;
Приклад:
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int i;
printf ( "10 випадкових чисел від 0 до 99 \ n \ n");
for (i = 0; i <10; i ++)
printf ( "% d \ n", rand ()% 100);
return 0;
}
Синтаксис:
#include <time.h>
void srand (unsigned seed);
Прототип:
void srand (unsigned seed);
ініціалізує генератор випадкових чисел. Початкова ініціалізація відбувається викликом srand з аргументом 1.
Нове початкове значення встановлюють викликом цієї функції з іншим аргументом.
Приклад.
#include <iostream>
#include <time.h>
int main () {
int a;
23
srand (time (0)); // ініціалізація генератора псевдовипадкових чисел
a = rand (); // отримуємо чергове псевдовипадкове число
cout << a; // вивід на екран
cin.get ();
}
/* srand - Це ініціалізація генератора випадкових чисел, time - повертає
поточний календарний час системи, в якості аргументу вона приймає вказівник на
змінну типу time_t, якій і буде присвоєний календарний час.
Прототип функції виглядає так:
time_t time (time_t * time);
Календарний час і повертається функцією, і поміщається в переданий аргумент,
але можна передавати нульовий вказівник (тобто 0 або null). 0 в даному випадку
не число, а нульовий вказівник. * /
приклад:
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
int main ()
{
int num;
// ініціалізувавши генератор випадкових чисел
srand (time (NULL));
printf ( "RAND_MAX:% d \ n", RAND_MAX);
num = rand ();
printf ( "Guess the number:% d \ n", num);
num = rand ()% 10;
printf ( "Guess the number (0 to 9):% d \ n", num);
num = rand ()% 10 + 1;
printf ( "Guess the number (1 to 10):% d \ n", num);
num = (rand ()% 2) * 2 - 1;
printf ( "Guess the number (-1 or 1):% d \ n", num);
getch ();
return 0;
}
Приклад.
/ * Встановлює генератор випадкових чисел в початковий стан і зберігає в масиві
20 перших згенерованих випадкових чисел. * /
24
Посилання
Змінні посилального типу можуть використовуватися в насиупних цілях:
• посилання замість передачі в функцію об'єкта за значенням;
• для визначення конструктора копії;
• для перевантаження унарних операцій;
• для запобігання неявного виклику конструктора копії при передачі у функцію за значенням об'єкта
визначеного користувачем класу
Зазвичай посилання використовуються при передачі аргументів функцій, при поверненні значень функцій
через посилання, а також при створенні псевдонімів об'єктів (змінних). У останньому випадку мова йде про
незалежне посилання (independent reference), яка використовується в якості псевдоніма (іншого імені) змінної,
зазначеного при ініціалізації посилання. Для визначення посилання використовується символ &, що
вказується перед змінною-посиланням. Формат оголошення посилання:
тип & ім'я;
де тип - це тип величини, на яку вказує посилання, &-оператор посилання, що означає, що наступне за
ним ім'я є ім'ям змінної посилального типу. Значення посилання - це значення змінної, на яку посилання
виконане (в цьому сенсі вона відрізняється від вказівника, значенням якого є адреса комірки, на яку
посилається вказівник). При оголошенні посилання необхідно відразу вказувати, на яку змінну вона
посилається (в цьому випадку говорять про ініціалізацію посилання). Посилання ініціалізується тільки один
раз. Надалі ім'я посилання служить ще однією альтернативною назвою для змінної, на яку виконано
посилання.
int n;
int & copy = n;
copy = 100;
n ++; // n = 101;
Використання незалежних посилань:
#include <iostream.h>
int main ()
{іnt t = 15, & r = t // ініціалізація посилання на t тепер
// r синонім імені t, а & r-адреса
cout << "t =" << t; // виводить 15
r + = 10; // зміна значення t через посилання
count << "t =" << t; // виводить 25
return 0;
}
int n,*p,*q;
int ©=n;
p=&n;
copy=100;
(*p)/=10;
q=©
n++;
cout << n << "\n" ;
cout<<copy<<" \n";
cout << * p << "\n";
cout << * q << "\n" ;
cout <<p<<"\n";
cout<<q<<" \n";
Запам'ятайте такі правила.
• Змінна посилання повинна явно ініціалізуватися при її описі, крім випадків, коли вона є параметром
функції, описана як extern або посилається на поле даних класа.
• Після ініціалізації посиланню не може бути присвоєна інша змінна.
• Тип посилання повинен збігатися з типом величини, на яку воне посилається.
• Не дозволяється визначати вказівники на посилання, створювати масиви посилань і посилання на
посилання.
• Не можна взяти адресу змінної посилального типу.
• Заборонено використовувати посилання на бітові поля.
Вони виключно важливі при роботі з функціями і складають основу одного з двох фундаментальних
механізмів передачі функції аргументів. Що стосується незалежних посилань, то їх широке використання на
практиці залишається під питанням, оскільки наявність псевдонімів у змінних, як правило, свідчить про
погану організацію програми. Проте, при роботі з класами посилання бувають дуже корисними.
25
Масиви символів
У C ++ не існує вбудованого типу для текстових даних. Текстові рядки реалізуються у вигляді масивів
символів або у вигляді об'єктів класу string.
За великим рахунком масив символів мало чим відрізняється від масивів інших типів. Головна
особливість пов'язана з тим, що при оголошенні масиву із символів необхідно зарезервувати достатньо місця
для того, щоб в такий масив можна було записувати рядки різної довжини. Іншими словами, при реалізації
рядків у вигляді масивів існує принципове обмеження на довжину рядка. Таке обмеження існує і при
використанні статичних масивів інших типів. Однак там ця проблема не настільки актуальна. Оскільки при
роботі із символьними масивами мова йде про подання типу даних, а не одиничного об'єкта, необхідно
передбачити можливість частої зміни даних. Важливими показниками при роботі з текстовими даними є
довжина рядка. Оскільки в символьному масиві кожен символ рядка відповідає елементу масиву, довжина
рядка безпосередньо має відношення до розміру символьного масиву. З проблемою обмеженості розміру
рядків пов'язана ще одна проблема. Навіть якщо розмір масиву досить великий для того, щоб записувати в
нього строкові значення, необхідний індикатор, який дозволяв би визначити, де в масиві записана корисна
інформація, а де починається неінформативний «хвіст». В якості такого індикатора використовують
спеціальний символ '\0', який називається нуль-символом.
Таким чином, рядкова змінна реалізується в програмі у вигляді масиву символів. Ознакою закінчення
рядка є нуль-символ. Щоб вписати в масив рядок, необхідно, щоб розмір масиву принаймні на одиницю
перевищував кількість символів в рядку. Цей додатковий елемент необхідний для запису нуль-символу '\0'
закінчення рядка. Оголошуються масиви символів, як і інші масиви: вказується тип елементів масиву (для
символьних масивів це char), назва і розмір масиву. Приклад оголошення символьного масиву:
char str [80];
В даному випадку оголошений масив із 80 символів. При цьому в такий масив можна записати рядок з
максимальною довжиною в 79 символів. Ініціалізуватися символьні масиви можуть так само, як і, наприклад,
числові: після імені та розміру масиву вказується знак рівності і в фігурних дужках список символів, які є
значеннями елементів масиву.
Однак існує ще один більш зручний спосіб ініціалізації символьного масиву: замість списку символів
вказується в подвійних лапках текстовий рядок:
char str1[20]= "hello";
char str2[20] = {'h', 'e', 'l', l', 'o', '\0'};
cout << str1 << "\n" ;
cout << str2 << “\n" ;
В обох випадках оголошується масив з 20 символів. Ініціалізація в першому випадку виконується шляхом
вказівки текстового значення (значення укладено в подвійні лапки). У другому випадку це ж значення
передається у вигляді укладеного у фігурні дужки списку, причому в явному вигляді вказується нуль-символ
закінчення рядка. При ініціалізації масиву за допомогою текстового літерала нуль-символ додається
автоматично.
Для виведення значень символьного масиву на екран його ім'я вказується праворуч від оператора
виведення
cout << str1 и cout << str2.
Раніше зазначалося, що якщо масив ініціалізується при оголошенні, розмір масиву вказувати
необов'язково. Розглянемо два наступних способи ініціалізації символьного масиву
char str1[] ="hello" ;
char str2[]={'h', 'e', 'l', l', 'o', '\0'};
Формально і в одному, і в іншому випадку значенням масиву є один і той же текстовий рядок hello.
Однак формуються масиви по-різному. Масив, ініціалізіємий командою
char str1[ ] ="hello" ,
складається з 6 елементів (хоча букв в слові hello тільки 5). Як уже зазначалося, якщо символьний масив
ініціалізується значенням-рядком (значення в подвійних лапках), то в масив в якості елементів послідовно
заносяться всі символи рядка і ще один символ в кінці масиву - символ закінчення рядка, тобто нуль-символ
'\0'.
При ініціалізації символьного масиву командою
char str2 [] = {'h','е','l','l','o'}
нічого подібного не відбувається. В цьому випадку масив складається з 5 елементів - відповідно до кількості
символьних елементів в списку значень масиву.
При зчитуванні значення текстового рядка з клавіатури із занесенням цього значення в символьний масив
ім'я масиву, в який віконується запис, вказується праворуч від оператора введення.
26
// Зчитування рядка з клавіатури
#include <iostream>
using namespace std;
int main ()
{
char str[100];
cout<<”nter your text, please: ";
cin>> str;
cout<<"Your text is: "<<str<<endl;
return 0 ;
}
Командою char str[100] оголошується символьний масив зі 100 елементів, а командою cin>>str;
з клавіатури зчитується текстове значення і заноситься в цей масив. Якщо ввести на запит програми слово
Hello, результат виконання програми буде виглядати наступним чином:
Enter your text, please: Hello
Your text is: Hello
Формально в масив можна занести текст довжиною до 99 символів. Однак на практиці все обмежується
одним словом. Справа в тому, що оператор введення >> зчитує інформацію, поки не зустріне символ переходу
на новий рядок, символ табуляції або пробіл. Оскільки слова в тексті розділяються пробілами, буде
зчитуватися тільки перше слово. Наприклад, якщо на запит програми ввести текст Hello, World!,
результат отримаємо наступний:
Enter your text, please: Hello, World!
Your text is: Hello,
Проблема вирішується, якщо замість оператора введення >> використовувати функцію gets().
Аргументом функції вказується назва масиву, в який заноситься значення. Після внесення змін програмний
код буде виглядати так:
// Зчитування рядка з клавіатури, використовуючи функцію gets()
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
char str[100];
cout<<"Enter your text, please: ";
// Для зчитування рядка використана функція gets():
gets(str);
cout<<"Your text is: "<<str<<endl;
return 0 ;
}
У загальному випадку для використання функції gets() необхідно підключити заголовок <cstdio>.
Результат виконання програми в цьому випадку може мати такий вигляд:
Enter your text, please: Hello, World!
Your text is: Hello, World!
При поелементному заповненні символьного масиву вже після оголошення необхідно пам'ятати, що
останнім символом після введення символів рядка повинен бути нуль-символ. Приклад програми з
поелементним заповненням символьного масиву.
// поелементне заповнення символьного масиву
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
char str[30];
int n=26;
char s=’а’;
for(int i=0 ; i<n; i++,s++)
str[i]=s;
str[n] ='\0';
cout << str <<endl;
return 0 ;
27
}
Результат виконання цієї команди має вигляд:
abcdefghijklmnopqrstuvwxyz
Звертаємо увагу на спосіб зміни символьної змінної s: незважаючи на те, що змінна має тип char, для її
зміни використовується оператор інкремента ++. З таким же успіхом замість оператора циклу можна
використовувати наступний оператор циклу (інші команди незмінні):
for(int i=0 ;i<n;i++)
str[i]=s+i;
Якщо після цього заповнити масив новими значеннями, заповнюється тільки початкова частина масиву.
При цьому ті елементи масиву, які не потрапили під перевизначення, залишаються незмінними.
// Поелементне заповнення масива
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int i;
const int n=20;
char str[20];
for(i=0; i<n; i++)
{
str[i] = 'A'+i;
cout<<str [i] ;
}
cout<<endl;
cout<<"Enter a string: " ;
gets(str);
cout<<str<<endl;
for (i = 0; i<n; i++)
cout<<str[i];
cout<<endl;
return 0 ;
}
Спочатку поелементно заповнюється символьний масив str розміру 20 (розмір заданий через
цілочислову константу n). Занесені в масив значення також поелементно виводяться на екран одним рядком.
Перебираються всі елементи масиву. Для заповнення масиву та виведення значень на екран використовується
оператор циклу. Далі користувачеві пропонується ввести текстовий рядок. При зчитуванні рядок заноситься в
масив. Рядок командою cout << str << endl виводиться на екран, після чого за допомогою оператора
циклу виводяться всі елементи масиву. Якщо в якості рядка користувача ввести фразу Hello, World!, то
результат виконання програми може бути наступним:
ABCDEFGHIJKLMNOPQRSТ
Enter a string: Hello, World!
Hello, World!
Hello, World! OPQRST
Інтерес представляють передостанній і останній рядки виведення результатів виконання програми. У
передостанньому рядку виводиться фраза Hello, World! (Та фраза, що введена користувачем з
клавіатури). Це результат виконання команди cout << str. При зчитуванні фрази користувача і запису її в
масив останнім після безпосередньо тексту рядка автоматично додається нуль-символ. Тому при виконанні
команди cout << str виведення на екран здійснюється до цього символу.
Останній рядок виводу результатів отримуємо при виконанні команд cout << str [i] в рамках
останнього оператора циклу. Оскільки перебираються всі елементи масиву, то на екран виводяться не тільки
елементи масиву до нуль-символу, а й елементи, розміщені після цього символу.
Сам нуль-символ відображається у вигляді пропуску. У цьому сенсі нуль-символ є дуже важливим
індикатором, який відокремлює корисний, робочий текст, занесений в символьний масив, від "сміття",
розміщеного в хвості масиву.
//Операції з рядками
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main()
{
char s1 [20],s2 [20];
strcpy(s1,"My name is ") ;
strcpy(s2,"Alex") ;
for(int i=0; s2[i]; s2[i]=toupper(s2[i]),i++);
strcat(s1,s2);
cout<<s1<<endl;
return 0 ;
}
Результатом виконання цієї програми є рядок
My name is ALEX
Спочатку за допомогою функції strcpy() записуються значення в масиви s1 і s2, після чого в рядку s2
всі символи змінюються на прописні (в операторі циклу використана функція toupper()), рядок s2 додається
командою strcat(s1, s2) в кінець рядка s1 , і результат виводиться на екран.
Рядкові літерали
Як уже зазначалося, рядкові літерали в C ++ зберігаються у вигляді символьного масиву, причому на цей
літерал автоматично створюється посилання.
Ця важлива обставина істотно спрощує роботу з літералами текстового типу.
// Робота з літералами
# include <iostream>
using namespace std;
intt main()
{
char *p,*q;
p="Hello, World'";
q="Hello, World!"+7;
cout<<p<<endl;
cout<<q<<endl;
cout<<*p<<endl;
p++;
cout<<*p<<endl;
return 0 ;
}
# include<iostream.h>
void pr(char); //прототип
void main()
{
char x;
do
{cout<<"\n введіть символ";
cin>>x;
pr(x); //виклик як оператор
}
while(x!= 'C');
}
void pr(char a)
{cout<<"\n введений символ:" <<a;}
Одновимірні масиви
Масиви можуть бути параметрами функцій і функції в якості результату можуть повертати вказівник
на масив. При використанні масивів як параметрів функції виникає необхідність визначення в тілі функції
кількості елементів масиву, який є аргументом при зверненні до функції.
При роботі з рядками, тобто з масивами типу char[], проблема вирішується просто, оскільки останній
елемент рядка має значення '\0'. Тому при обробці масиву-аргументу кожен його елемент аналізується на
наявність символу кінця рядка.
#include <iostream.h>
#include <conio.h>
int dl(char[]); // прототип функції dl
void main()
{
clrscr();
char c[]="kafedra KNIT";
cout << "\n kol-vo simvolov:" << dl(c);
35
getch();
}
void main()
{
float z[5],y;
for (int i=0; i<5; i++)
{
cout <<"\nВведіть поточний елемент масива:";
cin >> z[i];
}
y=sum(z) ;
cout <<"\n Сума елементів масива:"<< y;
}
В результаті виконання програми на екран буде виведено значення суми елементів масиву z.
В даному прикладі відсутній прототип функції sum(), оскільки опис функції sum() компілятор аналізує
раніше, ніж звернення до неї. У наведеному прикладі заздалегідь відомо число елементів - 5, тому в функції
всього один параметр - масив х. Оскільки функція повертає значення, то виклик функції може бути тільки
виразом або частиною виразу. У програмі оператор присвоювання y = sum(z); містить такий вираз в
правій частині. Тут аргументом функції є масив z.
void main()
{
float z[6];
for (int i=0;i<6;i++)
{
cout <<"\nВведіть поточний елемент масива:";
cin >> z[i];
}
cout <<"\nМаксимальний елемент масива:"<< max(6,z);
}
#include <iostream.h>
void der(int, float[]); // прототип функції
void main() {
const int k=10;
float a[k];
int i;
for (i=0; i<k; i++)
{
cout << "\nВведіть елемент масива ";
cin >> a[i];
}
der(k,a); // виклик функції
for (i=0; i<k; i++)
cout << "\ta[" << i << "]=" << a[i];
}
void der (int k1, float a1[])
{
int j;
for (j=0; j<k1;j++)
a1[j]--;
}
void main()
{
37
clrscr();
int a[]={0,1,2,3,4};
int b[]={5,6,0,7,1};
int d[5];
maxl (5,a,b,d) ;
cout << "\n";
for (int i=0;i<5;i++)
cout << "\t" << d[i] ;
getch();
}
В результаті виконання програми на екран монітора будуть видані елементи результуючого масиву d:
5 6 2 7 4.
У наведеній програмі у функції є чотири параметри: число n елементів в масивах і три вказівника х, у,
z. При виконанні функції max1(5, a, b, d) (за допомогою оператора, оскільки відсутнє значення, що
повертається) в якості аргументів використовуються імена вихідних масивів а й b і результуючого d.
Багатовимірні масиви
Особливістю мови C ++ є несамовиизначенність масивів, тобто за іменем масиву неможливо дізнатися його
розмірність і розміри по кожному виміру. Крім того, в C ++ багатовимірні масиви не визначені. Наприклад,
якщо оголошений масив float d [3] [4] [5], то це не тривимірний, а одновимірний масив d, що
включає три елементи, кожен з яких має тип float [4] [5]. У свою чергу, кожен з чотирьох елементів
типу float [5]. І, відповідно, кожен з цих елементів є масивом з п'яти елементів типу float. Ці
особливості ускладнюють використання масивів як параметрів функцій.
При передачі масивів як параметрів через заголовок функції слід враховувати, що передавати масиви
можна тільки з однієї невизначеною розмірністю (ця розмірність повинна бути найлівішій).
#include <iostream.h>
float summa(int n,float a[][3])
{
float s=0;
for (int i=0;i<n;i++)
for (int j=0;j<3;j++)
s=s+a[i][j];
return s;
}
void main()
{
float z[4][3] = {0,1,2,3,4,5,6,7,7,6,5,4};
cout << "\n Сума елементів матриці дорівнює " << summa(4,z);
}
#include <iostream.h>
void main()
{
float d[3][4]={1,2,-2,4,5,0,-3,18,-9,6,7,9};
float *r[]={(float*)&d[0], (float*)&d[1], (float*)&d[2]};
int m=3;
int n=4;
cout<<"\n Мінімальний елемент матриці дорівнює "<<min(m,n,r);
}
Динамічні масиви
При використанні в якості параметра масиву в функцію передається вказівник на його перший елемент,
тобто масив завжди передаеться за адресою. При цьому інформація про кількість елементів масиву
втрачається, і слід передавати його розмірність через окремий параметр.
void main()
{
int **pi; // вказівник на масив вказівників
int m1; // кількість рядків в матриці
cout << "\n Введіть кількість рядків матриці:";
cin >> m1;
int n1; // кількість стовпців в матриці
cout << "\n Введіть кількість стовпців матриці:";
cin >> n1;
int i,j;
// виділення допоміжного масива вказівників
pi=new int* [m1];
for (i=0;i<m1;i++)
// формування i-й рядків матриці
pi[i]=new int [n1];
fun(m1,n1,pi); // виклик функції
for (i=0;i<m1;i++) // цикл перебора рядків
{
cout << "\n рядків " << i+1 << ":";
for (j=0;j<n1;j++)
cout << "\t" << pi[i][j];
}
//
for (i=0;i<m1;i++)
delete []pi[i];
delete []pi;
}
Якщо при введенні задати розмірність матриці 3x4, то в результаті виконання програми на екран буде
видано три наступні рядки:
рядок 1: 0 1 2 3
рядок 2: 4 5 6 7
рядок 3: 8 9 10 11
У функції fun() є три наступні параметри: int m - число рядків, int n - число стовпців матриці і int
**pi - вказівнік на масив вказівників.
У функції main для визначеності значення числа рядків m1 задано 3, а число стовпців матриці n1 задано 4.
При виконанні оператора присвоювання pi = new int * [m1]; операцією new виділяється пам'ять для
допоміжного масиву вказівників. В результаті виконання цієї операции для масиву вказівників виділяється
необхідна кількість пам'яті, а адреса першого елемента виділеної пам'яті присвоюється вказівнику pi. При
виконанні в тілі циклу другої операції new формуються m1 рядків матриці.
40
При зверненні до функції відбувається прісвоєння елементам матриці послідовно значень цілих чисел від 0
до 11. Наприкінці програми за допомогою delete виділена пам'ять звільняється.
Багатовимірний масив зі змінними розмірами, який формується в тілі функції, неможливо цілком
повернути в якості результату в викликаючу функцію. Однак повертаємим значенням функції може бути
вказівник на одновимірний масив вказівників на одновимірні масиви з елементами заданого типу.
void main()
{
int n1; //порядок матриці
cout << "\n Введіть порядок матриці:";
cin >> n1;
int **ma; //вказівник для формування матриці
ma=matr(n1); //виклик функції
//друк елементів матриці
for (int i=0;i<n1;i++)
{
cout << "\n рядок " << i+1 << ":";
for (int j=0;j<n1;j++)
cout << "\t" << ma[i][j];
}
//звільнення пам’яті
for (i=0;i<n1;i++)
delete []ma[i]; //видаляємо вміст рядків
delete []ma; //видаляемо масив вказівників на рядки
}
41
При виконанні програми на екрані з'явиться підказка:
Введіть порядок матриці:
Якщо у відповідь на підказку користувач введе з клавіатури порядок матриці, що дорівнює 5, то на екрані
дисплея він отримає повідомлення з п'яти рядків виду:
рядок 1: 1 0 0 0 0
рядок 2: 0 1 0 0 0
рядок 3: 0 0 1 0 0
рядок 4: 0 0 0 1 0
рядок 5: 0 0 0 0 1
У даній програмі функція matr() має один параметр int n - порядок матриці. У тілі функції формується
квадратна матриця розміром nxn (створюються n одновимірних масивів з елементами типу int і масив
вказівників на ці одновимірні масиви) і її елементи заповнюються необхідними значеннями. Значенням
функції matr() є значення вказівника на сформовану матрицю.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <malloc.h>
int sum(const int *a, const int nstr, const int nstb);
int main()
{
clrscr();
int b[2][2]= {{2, 2},{4,3}};
printf ("Cума елементів b: %d\n", sum(&b[0][0], 2, 2));
// ім'я масиву передавати в sum не можна через невідповідність типів
int i,j, nstr, nstb, *a;
printf ("Введіть кількість рядків і стовпців: \n");
scanf("%d%d", &nstr, &nstb);
a=(int *)malloc(nstr* nstb* sizeof(int)); //1
printf ("Введіть елементи масива: \n");
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
scanf("%d", &a[i * nstb + j]); //2
printf("Сума елементів a: %d\n", sum(a, nstr, nstb));
free(a);
return 0;
}
int sum(const int *a, const int nstr, const int nstb)
{
int i,j,s=0;
for (i=0; i<nstr; i++)
for (j = 0; j<nstb; j++)
s += a[I * nstb + j];
return s;
}
Пам'ять виділяється відразу під всі елементи масиву (оператор 1), тобто, по суті двовимірний динамічний
масив займає суцільну ділянку пам'яті. При заповненні масиву використовується формула для визначення
індексу відповідного елемента (2). В кінці роботи програми масив видаляється.
Можна вирішити цю задачу по-іншому виділяючи пам'ять (програма написана в стилі С++).
#include <iostream.h>
42
int main()
{
int nstr, nstb;
cout<<" Введіть кількість рядків і стовпців: \n";
cin >> nstr >> nstb;
int i,j, **a;
a=new int *[nstr];
for (i=0; i<nstr; i++)
a[i]=new int [nstb];
cout<<" Введіть елементи масива: \n";
for (i=0; i<nstr; i++)
for (j=0; j<nstb; j++)
cin>>a[i][j];
cout<<" Сума елементів a:"<<sum(a, nstr, nstb);
//звільнення пам’яті
for (i=0;i<nstr;i++)
delete []ma[i]; //видаляємо вміст рядків
delete []ma; //видаляємо масив вказівників на рядки
return 0;
}
У цьому випадку пам'ять спочатку виділяється під стовпець вказівників на рядки матриці, а потім в циклі
під кожен рядок. Звільнення пам'яті повинно виконуватися у зворотному порядку.
#include <iostream.h>
double pr(double a, ...)
{
double b=1.0; // b - добуток
double *p=&a;
if(*p==0.0) return 0.0;
for(; *p; p++) b* =*p;
return b;
}
void main()
{
cout << "\n pr(6e0,5e0,3e0,0e0)=" << pr(6e0,5e0,3e0,0e0);
cout << "\n pr(l.5,2.0,0.0,3.0,0.0)=" << pr(1.5,2.0,0.0,3.0,0.0);
cout << "\n pr(0.0)=" << pr(0.0);
}
Результат виконання програми:
pr(6е0,5е0,3е0,0е0)=90
pr(1.5,2.0,0.0,3.0,0.0)=3
pr(0.0)=0
У функції рr() переміщення по списку аргументів здійснюється за рахунок зміни значення вказівника р.
При другому зверненні до функції в середині списку аргументів знаходиться аргумент з нульовим значенням.
Він сприйнмається функцією як індикатор, який сигналізує про закінчення списку аргументів. У наведеній
програмі, так само як і в першому прикладі, аргументи повинні мати один тип, але не обов'язково int або
double.
Для використання в програмі функцій зі змінним числом параметрів, при виклику яких можна
використовувати аргументи різних типів, необхідно включати в текст програми спеціальний набір
макроозначень. Ці макровизначення знаходяться в заголовку stdarg.h.
Функція main()
Функція, якій передається управління після запуску програми, повинна мати ім'я main. Вона може
повертати значення в систему і приймати параметри з зовнішнього оточення. Значення, що повертається
повинно бути цілого типу. Стандарт передбачає два формати функції:
// без параметрів:
тип main () {/ * - * /}
// з двома параметрами:
тип main (int argc, char * argv []) {/ * - * /}
При запуску програми параметри розділяються пробілами. Імена параметрів в програмі можуть бути будь-
якими, але прийнято використовувати argc u argv. Перший параметр (argc) визначить кількість
параметрів, переданих функції, включаючи ім'я самої програми, другий параметр (argv) є вказівником на
масив вказівників типу char *. Кожен елемент масиву містить вказівник на окремий параметр командного
рядка, що зберігається в вигляді С-рядка, що закінчується нуль-символом. Перший елемент масиву
(argv[0]) посилається на повне ім'я файлу запускається на виконання, наступний (argv [l]) вказує на
перший параметр, argv [2] - на другий параметр, і так далі. Параметр argv [argc] має дорівнювати 0.
// програма виводить на екран всі передані в командний рядок
// параметри функції
45
#include <iostream.h>
void main (argc, char* argv[]) {
for (int i = 0; i<argc; i++)
cout << argv[i] << '\n';
}
Перший цілочисельний argc аргумент особливих питань не викликає. Другий аргумент, оголошений
як char * argv [] - масив, елементами якого є текстові рядки, реалізовані у вигляді символьних масивів.
Індексна змінна i пробігає значення відповідно до розміру масиву (від 0 до size-1 включно). Для кожного
значення цього індексу на екран виводиться відповідний параметр командного рядка, для чого
використовується посилання argv [i].
Якщо функція main() нічого не повертає, викликаюча система отримає значення, що означає успішне
завершення. Ненульове значення означає аварійне завершення. Оператор повернення з main() можна
опускати. Якщо параметри в командний рядок передавати не планується, аргументи для методу main() можна
не вказувати (а можна вказувати).
Значення, що повертається
Механізм повернення з функції у функцію, яка її викликала, реалізується оператором
return [вираз];
Значення виразу, якщо воно задано, повертається в викликаючу функцію в якості значення, що
викликається. Якщо вираз опущений, то значення, яке повертається, не визначене. Вираз може бути
укладено в (). Функція може містити кілька операторів return (це визначається потребами алгоритму).
Вираз, вказаний після return, неявно перетвориться до типу значення, що повертається функцією і
передається в точку виклику функції. Якщо в функції відсутній оператор return, то передача управління в
викликаючу функцію відбувається після останнього оператора функції, що викликається. При цьому значення,
що повертається не визначене.
Приклади:
int f1 () {return 1;} // правильно
Рекурсивні функції
Для реалізації рекурсивних алгоритмів в C ++ передбачена можливість створення рекурсивних функцій.
Рекурсивна функція являє собою функцію, в тілі якої здійснюється виклик цієї ж функції.
Приклад 1. Використання рекурсивної функції для обчислення факторіала.
Нехай потрібно скласти програму обчислення факторіала довільного позитивного числа.
#include <iostream.h>
int fact(int n)
{
int a;
if (n<0) return 0;
if (n==0) return 1;
a =n * fact(n-l);
return a;
}
48
void main()
{
int m;
cout << "\nВведіть ціле число:";
cin >> m;
cout << "\n Фактoріал числа " << m << " дорівнює " << fact(m);
}
Для негативного аргументу факторіала не існує, тому функція в цьому випадку повертає нульове значення.
Так як факторіал 0 дорівнює 1 за визначенням, то в тілі функції передбачений і цей варіант. У разі коли
аргумент функції fact() різниться від 0 і 1, то викликається функція fact() зі зменшеним на одиницю
значенням параметра і результат множиться на значення поточного параметра. Таким чином, в результаті
вбудованих викликів функцій буде повернуто наступний результат:
n *(n-1) *(n-2) *. . . * 2 * 1 * 1
Остання одиниця при формуванні результату належить виклику fact(0).
Оскільки в мові C ++ відсутня операція піднесення до степеня, то для виконання піднесення до степеня
дійсного ненульового числа можна використовувати рекурсивну функцію.
Приклад 2. Використання рекурсивної функції для піднесення до степеня.
Нехай потрібно скласти програму піднесення до позитивного цілого степеня дійсного ненульового числа.
/ * Піднесення основи х до степеня n * /
#include <iostream.h>
float st (float x, int n)
{
if (n==0) return 1;
if (x==0) return 0;
if (n>0) return x*st(x,n-l);
if (n<0) return st(x,n+l)/x;
}
void main()
{
int m;
float a,y;
cout << "\nВведіть основу степеня:";
cin >> a;
cout << "\nВведіть степень числа:";
cin >> m;
y=st(a,m);
cout << "\n Число " << a << " в степені " << m << " дорівнює:" << у;
}
У функції передбачені чотири гілки виконання піднесення до степеня дійсного числа:
• степінь числа дорівнює нулю;
• основа дорівнює нулю;
• степінь більше нуля;
• ступінь менше нуля.
При виконанні функції, коли основа степеня більше нуля, наприклад, якщо при введенні задані значення
змінних а = 2.0 і m = 4, тоді виклик st(a, m) призводить до наступного обчислення:
2.0 * 2.0 * 2.0 * 2.0 * 1
Виклик функції для негативного степеня, наприклад, коли значення аргументів рівні а = 3.0 і m = -2
призводить до обчислення наступного виразу:
1 / 3.0 / 3.0
Перевантаження функцій.
// Перший варіант функції:
void showArgs (double x) {cout << "Double-number" << x << endl;}
// Другий варіант функції:
void showArgs (double x, double y)
{cout << "Double-numbers" << x << "and" << y << endl;}
// Третій варіант функції:
void showArgs (char s)
{cout << "Symbol" << s << endl;}
// Четвертий варіант функції:
49
int showArgs (int n)
{return n;}
int main ()
{int n = 3;
double x = 2.5, y = 5.1;
char s = 'w';
// Перший варіант функції:
showArgs (x);
// Другий варіант функції:
showArgs (х, у);
// Третій варіант функції:
showArgs (s);
// Четвертий варіант функції:
cout << "Int-number" << showArgs (n) << endl;
}
У програмі описано чотири варіанти функції showArgs(), призначення якої полягає у виведенні на екран
інформації про її аргументи. Однак в залежності від кількості та типу аргументів виконуються трохи різні дії.
Зокрема, передбачені такі варіанти передачі аргументів:
1. Один аргумент типу double
2. Два аргументи типу double
3. Один аргумент типу char
4. Один аргумент типу int
У перших трьох випадках результат функцією не повертається (тип функції void), а на екран виводиться
повідомлення про тип і значення аргументу (або аргументів). У четвертому випадку функцією в якості
значення повертається аргумент.
У головному методі програми функція showArgs() викликається в різному контексті: виклик
showArgs(х) відповідає варіанту функції для одного аргументу типу double, виклик showArgs(х, у)
відповідає варіанту функції для двох аргументів типу double, виклик showArgs(s) відповідає варіанту
функції для одного аргументу типу char, виклик cout<<"Int-number"<<showArgs(n)<<endl
відповідає варіанту функції з одним аргументом типу int. Результат виконання програми буде наступним:
Double-number 2.5
Double-numbers 2.5 and 5.1
Symbol w
Int-number 3
При роботі з перевантаженими функціями не слід забувати про автоматичне приведення типів. Ці два
механізми в симбіозі можуть давати дуже цікаві, а іноді і дивні результати. Розглянемо приклад.
// Автоматичне приведення без перевантаження
void ndiv(double x, double y)
{cout<<"x/y= " << x/y << endl; }
int main ()
{ double x=l0 ,y=3;
int n=10,m=3;
ndiv(x,y);
ndiv(n,m);
}
Результат виконання такої програми буде наступним:
х/у= 3.33333
х/у= 3.33333
Перший рядок, який є результатом виконання команди ndiv(х, у), думається, питань не викликає.
Результатом виклику функції є частка двох чисел - аргументів функції. Хоча в команді ndiv(n, m)
аргументами функції вказані цілі числа, завдяки автоматичному приведення типів цілочіслові значення
розширюються до типу double, після чого обчислюється потрібний результат.
Однак варто змінити програмний код, перевантаживши функцію ndiv(), і результат зміниться
кардинально.
// Перевантаження і автоматичне приведення типів
void ndiv(double x, double y)
{ cout<<"x/y= "<<x/y<<endl;}
void ndiv(int n, int m)
{ cout<<"n/m= "<<n/m<<endl;}
int main()
50
{ double x=10,y=3;
int n=10,m=3;
ndiv(x,y);
ndiv(n,m);
}
Зокрема, на екрані в результаті виконання програми з'явиться повідомлення:
х/у= 3.33333
n/m= 3
Справа в тому, що змінений програмний код містить перевантажену функцію ndiv(), для якої
передбачений виклик як з двома аргументами типу double, так і з двома аргументами типу int. У
останньому випадку, хоча результат функції формально обчислюється так само, як і в початковому варіанті,
для цілих чисел оператор / означає цілочислове ділення з відкиданням остачі. Тому в результаті отримуємо
число 3, а не 3.33333, як для виклику функції з double -аргументами. Більш того, нерідко трапляються
ситуації, коли з урахуванням автоматичного приведення типів неможливо однозначно визначити, який з
варіантів перевантаженої функції необхідно викликати. Наприклад, якщо для перевантаженої функції
передбачені два варіанти передачі аргументу для типу даних float і для типу даних double, то передача
функції як аргумент int-змінної призведе до помилки компіляції, оскільки незрозуміло, до якого формату
даних (float або double) потрібно перетворювати тип int. Такі ситуації відносяться до розряду логічних
помилок, і їх потрібно уважно відстежувати.
Як і звичайні функції, перевантажені функції можуть мати аргументи зі значеннями, що
використовуються за замовчуванням. Причому для кожного варіанта перевантаженої функції ці значення за
замовчуванням можуть бути різними. Єдине, за чим необхідно постійно стежити, щоб наявність значень за
замовчуванням у аргументів не призводило до неоднозначних ситуацій при виклику перевантаженої функції.
// Перевантаження і значення за замовчуванням
void hello()
{cout<<"Hello, my friend! \n";}
void hello (char str[], char name[] = “Alex")
{ cout << str << " , "<<name<<" ! "<<endl; }
int main()
{
hello();
hello("Hello","Peter");
hello("Hi");
return 0 ;
}
У перевантаженої функції hello() два варіанти: без аргументів і з двома аргументами-масивами типу
char. Причому в останньому випадку другий аргумент має значення за замовчуванням.
Якщо функція викликається без аргументів, в результаті її виконання на екран виводиться
повідомлення Hello, my friend !. При виконанні функції з двома аргументами (кожен аргумент -
текстовий рядок, реалізований у вигляді масиву символів) на екран послідовно виводяться текстові значення
аргументів функції. Оскільки другий аргумент має значення за замовчуванням Alex, то формально функцію
можна викликати і з одним текстовим аргументом. У головному методі функція hello() послідовно
викликається без аргументів (команда hello()), з двома аргументами (команда hello( "Hello",
"Peter")) і з одним аргументом (команда hello( "Hi")). Результат виконання програми виглядає
наступним чином:
Hello, my friend!
Hello, Peter!
Hi, Alex!
Але якщо спробувати для другого варіанту функції (з двома аргументами) вказати значення за
замовчуванням і для першого аргументу, виникне помилка. Причина в тому, що наявність у кожного з
аргументів функції hello() значення за замовчуванням призводить до неоднозначної ситуації: якщо функція
при виклику вказана без аргументів, неможливо визначити, викликається цей варіант функції без аргументів,
або потрібно використовувати варіант функції з двома аргументами зі значеннями за замовчуванням. Як уже
зазначалося, на подібні ситуації слід постійно звертати увагу при створенні програмних кодів у процесі
перевантаження функцій.
51
Структури (struct)
На відміну від масиву, всі елементи якого однотипні, структура може містити елементи різних типів. У мові
С++ структура є видом класу і має всі його властивості, але в багатьох випадках достатньо використовувати
структури так, як вони визначені в мові С:
Struct [Ім'я типу]
{ Тип1 елемент1;
Тип2 елемент2;
Типn елементn;
} [список описателів];
Елементи структури називаються полями структури і можуть мати будь-який тип, крім типу цієї ж структури,
але можуть бути вказівниками на нього. Якщо відсутнє ім'я типу, повинен бути вказаний список описателів
змінних, вказівників або масивів. У цьому випадку опис структури служить визначенням елементів цього
списку: Розмір структури не обов'язково дорівнює сумі розмірів її елементів, оскільки вони можуть бути
вирівняні по межах слова.
После фігурних дужок, які закінчують безпосередньо опис структури, можна вказати (а можна і не вказувати)
список змінних структури. Справа в тому, що сам по собі опис структури цю структуру не створює. Опис
структури - це всього лише якийсь шаблон, за яким потім створюються змінні. Процес створення змінних
структури можна не відкладати в довгий ящик, а створити їх відразу, вказавши список з назвами після опису
структури.
// Визначення масиву структур і вказівника на структуру:
struct One
{ char fio[30]
int date, code;
double salary;
}staff [100], * ps;
Якщо список відсутній, опис структури визначає новий тип, ім'я якого можна використовувати в подальшому
поряд зі стандартними типами, наприклад:
Struct Worker
{char fio[30];
int date, code;
double salary
}; // Опис завершується капкою з комою
Worker a; //змінна а типу Worker
З точки зору використання в програмі має сенс говорити лише про змінні структури. Фактично змінна
структури - це об'єкт, який має ім'я (ім'я змінної структури) і поля, тип і назви яких визначаються описом
структури. Щоб в програмі створити змінну структури, необхідно вказати ім'я структури, відповідно до опису
якої створюється змінна, і ім'я цієї змінної. Іншими словами, змінна структури в програмі створюється точно
так же, як і змінна будь-якого базового типу, тільки замість назви типу змінної вказується назва структури.
Сама по собі змінна структури інтерес представляє більше спортивний, ніж практичний. Всі дані, які можуть
знадобитися в процесі виконання програми, записані в поля змінної. Звернення до поля змінної структури
здійснюється через так званий точковий синтаксис (стандартний синтаксис для об'єктно-орієнтованого
програмування) - вказується ім'я змінної структури, і через точку ім'я поля, до якого віконується звернення,
тобто в форматі структура.поле.
Доступ до полів структури виконується за допомогою операцій вибору (точка) при зверненні до поля через
ім'я структури і через -> при зверненні через вказівник, наприклад:
Worker worker, staff[100], * ps;
…
worker.fio = ”Страусенко”;
staff[8].code = 215;
ps->salary = 0.12;
Якщо елементом структури є інша структура, то доступ до її елементів виконується через дві операции
вибору:
struct A { int a; double x;};
struct B { int a; double x;} x[2];
x[0].a.a=1;
x[1].x=0.1;
Як видно з прикладу поля різних структур можуть мати однакові імена, оскільки у них різна область
видимості. Більш того, можна оголошувати в одній області видимості структуру і інший об'єкт (наприклад,
змінну або масив) з однаковими іменами, якщо при визначенні структурної змінної використовувати слово
struct, але не варто це робити - заплутати компілятор важче, ніж себе.
// Оголошення та використання структури
52
struct Marks
{ char name[80];
int phys;
int chem;
int maths;
} ivanov,petrov,sidorov;
struct Exams
{ double phys;
double chem;
double maths;
};
int main()
{ strcpy (ivanov.name, "Sergei Ivanov") ;
ivanov.phys=4;
ivanov.chem=3;
ivanov.maths=3;
strcpy (petrov.name, "Igor Petrov");
petrov.phys=5;
petrov.chem=4;
petrov.maths=4;
strcpy(sidorov.name,"Ivan Sidorov");
sidorov.phys=5;
sidorov.chem=4;
sidorov.maths=3;
Exams LastYear,ThisYear;
LastYear.chem=4.33333;
LastYear.phys=3.66667;
LastYear.maths=3.33333;
ThisYear.chem=(double)(ivanov.chem + petrov.chem + sidorov.chem)/3;
ThisYear.phys=(double)(ivanov.phys + petrov.phys + sidorov.phys)/3;
ThisYear.maths=(double)(ivanov.maths + petrov.maths + sidorov.maths)/3;
cout<<"Last year marks:" << endl;
cout<<"Physics" << LastYear.phys<<endl;
cout<<"Chemistry"<<LastYear.chem<<endl;
cout<<"Mathematics"<<LastYear.maths<<endl;
cout<<"This year marks:" << endl;
cout<<"Physics"<<ThisYear.phys<<endl;
cout<<"Chemistry"<<ThisYear.chem<<endl;
cout<<"Mathematics "<<ThisYear.maths<<endl;
return 0;
}
У програмі оголошені дві структури: Marks і Exams. У структуру Marks входять чотири поля: символьний
масив name типу char і цілочислові поля (тип int) phys, chem і maths. Одночасно з оголошенням
структури віконується оголошення і змінних структури з іменами ivanov, petrov і sidorov.
Структура Marks створюється в програмі для зберігання інформації про успішність учнів. Оцінки з трьох
предметів (фізика, хімія і математика) заносяться в цілочислові поля phys, chem і maths відповідно. Поле-
масив name визначено для запису імені та прізвища учня.
Опис структури є лише загальним шаблоном, відповідно до якого формуються змінні структури. Щоб було
куди записати оцінки, а також ім'я та прізвище, необхідно створити змінну структури. В даному випадку
змінні структури створені шляхом перерахування назв в кінці опису структури.
Призначення структури Exams полягає в зберіганні середніх оцінок по кожному з предметів в різні роки.
Кожному року відповідає змінна структури. Змінні структури створюються безпосередньо в головному методі
програми. У структури три поля, які мають такі ж назви (phys, chem і maths), однак тепер поля оголошені
як такі, що тип double. Справа в тому, що середня оцінка, на відміну від оцінки учня з предмета, в
загальному випадку не є цілим числом.
У головному методі програми спочатку виконується ініціалізація полів змінних структури Marks. Зокрема,
заповнення масиву name для змінної ivanov виконується командою strcpy (ivanov.name, "Sergei
Ivanov") (копіювання рядка "Sergei Ivanov" в поле-масив name).
ivanov.phys = 4;
ivanov.chem = 3;
53
ivanov.maths = 3;
Точно так же заповнюються поля двох інших змінних (petrov і sidorov) структури Marks.
Командою Exams LastYear, ThisYear в головному методі програми оголошуються дві змінні LastYear і
ThisYear структури Exams. Змінна LastYear призначена для запису середніх оцінок минулого року.
Оцінки (середні) за поточний рік записуються в змінну ThisYear структури Exams. Значення полів змінної
структури LastYear присвоюються командами
LastYear.chem = 4.33333;
LastYear.phys = 3.66667;
LastYear.maths = 3.33333;
Відповідні поля змінної структури визначаються як середнє арифметичне по набору учнів (їх в даному
випадку всього три):
ThisYear.chem = (double) (ivanov.chem + petrov.chem + sidorov.chem) / 3;
ThisYear.phys = (double) (ivanov.phys + petrov.phys + sidorov.phys) / 3;
ThisYear.maths = (double) (ivanov.maths + petrov.maths + sidorov.maths) / 3;
Інструкція (double) використана для явного приведення типів, оскільки за замовчуванням для цілочислових
операндів операція ділення віконується як ділення з відкиданням дробової частини (цілочислове ділення). Далі
отримані значення для полів змінних LastYear і ThisYear структури Exams виводяться на екран.
Результат має наступний вигляд:
Last year marks:
Physics 3.66667
Chemistry 4.33333
Mathematics 3.33333
This year marks:
Physics 4.66667
Chemistry 3.66667
Mathematics 3.33333.
Для змінних одного і того ж структурного типу визначена операція присвоювання, при цьому відбувається
поелементне копіювання. В результаті такого присвоювання з однієї змінної в іншу копіюються значення всіх
полів структури:
ivanov=petrov;
Для ініціалізації структури значення її елементів перераховують в фігурних дужках в порядку їх опису:
Struct Worker
{
char fio[30];
int date, code;
double salary
} worker = { “Страусенкo“, 31,215,3400.55};
Масиви структур
Як і звичайні змінні, структури можуть бути елементами масивів.
// Масиви структур
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
struct Marks
{
char name[80];
int phys;
int chem;
int maths;
};
int main()
{
const int n=3;
bool state;
char s [80];
Marks students[n];
for (int i=0 ;i<n;i++)
{
54
cout << ("Student name:");
gets(students[i].name);
students[i].phys=3+rand()%3;
students[i].chem=3+rand()%3;
students[i].maths=3+rand()%3;
}
do
{
cout<<"What is the student name?";
gets (s);
if (!strcmp(s,"exit")) return 0 ;
state=true;
for(int i=0 ;i<n;i++)
{
if (!strcmp(students[i].name,s))
{ state=false;
cout<<"Physiscs:"<<students[i].phys<<endl;
cout<<"Chemistry:"<<students[i].chem<<endl;
cout<<"Mathematics:"<<students[i].maths<<endl;
break;
}
}
if (state) cout<<"There is no student with such name\n";
}while(true);
}
Приклад виконання програми може виглядати наступним чином:
Student name: Sergei Ivanov
Student name: Igor Petrov
Student name: Ivan Sidorov
What is the student name? Sergei Ivanov
Physics: 5
Chemistry: 5
Mathematics: 4
What is the student name? Ivan Sidorov
Physics: 3
Chemistry: 3
Mathematics: 4
What is the student name? Igor Petrovski
There is no student with such name
What is the student name? Igor Petrov
Physics: 4
Chemistry: 5
Mathematics: 4
What is the student name? exit
У програмі командою Marks student s [n] визначається масив students змінних структури Marks з n
елементів (попередньо оголошена цілочисельна константа const int n = 3). Заповнення елементів
масиву виконується в рамках оператора циклу. Індексна змінна i пробігає значення від 0 до n-1 включно.
Кожен раз буде запропоновано ввести ім'я учня і це ім'я командою gets(students[i].name) записується
в поле name змінної структури students[i] -елемента масиву students. Оцінки з трьох предметів для
кожного учня визначаються як випадкові числа командами students[i].
phys=3+rand()%3, students[i].chem=3+rand()%3 і students[i].maths=3+rand()%3.
До базової оцінки 3 додається ціле випадкове число в діапазоні від 0 до 2 включно (результат команди
rand()%3 - остача від ділення випадкового числа на 3).
Командою char s[80] оголошується символьний масив, в який буде виконуватися зчитування імені учня,
яке вводиться користувачем, для відображення його оцінок. Цей масив використовується в операторі циклу do
{...} while(true). Причому варто звернути увагу, що в якості умови, яка перевіряється, вказано логічне
значення true, що означає, що формально цикл нескінченний. Можливість виходу з цього циклу передбачена
в самому циклі. На початку циклу командою cout << "What is the student name?" Вам буде
запропоновано ввести ім'я учня. Введене користувачем ім'я за допомогою команди gets(s) зчитується і
записується в масив s. Після того, як ім'я учня введено, віконується умовний оператор
55
if (! strcmp(s, "exit")) return 0.
Цією командою передбачена можливість не тільки виходу з оператора циклу, але і завершення роботи
програми: якщо користувач в якості імені введе exit, робота програми завершується (інструкція return 0).
Там же командою state = true логічній змінній state (змінна використовується для індикації того,
знайдено збіг імен чи ні) присвоюється значення true. Після цього за допомогою оператора циклу
віконується послідовний перебір елементів масиву students і проводиться порівняння значень рядків,
записаних в масив s і в поле-масив students[i].name. Для порівняння рядків використовується функція
strcmp(). Нагадаємо, що якщо рядки збігаються, то в якості значення функцією повертається 0, тому в
умовному операторі зазначена умова !Strcmp(students[i].name, s). Якщо умова істинна, командою
state = false змінюється стан логічної змінної state, після чого виводиться інформація про оцінки
відповідного учня. В кінці умовного оператора розміщена команда break для передчасного виходу з
оператора циклу - якщо збіг знайдено, продовжувати пошук не має сенсу. Якщо збігу немає (тобто введене
користувачем ім'я не знайдено при перегляді полів елементів масиву students), змінна state має значення
true. На цей випадок передбачена команда
if (state) cout << "There is no student with such name\n".
При ініціалізації масивів структур слід укладати в фігурні дужки кожен елемент масиву:
struct Complex
{
float real, im;
}compl[2][3]={{{1.1},{1.1},{1,1}},/*Строка1*/{{2.2},{2.2},{2.2}}}; //Строка 2,
// тобто масив compl[1]
Вказівники на структури
При оголошенні вказівника на структуру, як і в разі створення вказівників на значення базових типів,
вказується тип структури, а перед ім'ям змінної-вказівника ставиться оператор *. Цей же оператор
використовується для отримання доступу до змінної структури за вказівником на цю змінну.
Крім того, через вказівнік на структуру можна звертатися безпосередньо до полів структури, для чого
використовують оператор -> (стрілка, складається з двох символів – і >). Наприклад, якщо в програмі
визначено вказівнік на змінну структури, у якій є поле, то доступ до цього поля можна отримати за допомогою
інструкції вказівнік-> поле.
// Вказівники на структуру>
#include <iostream>
using namespace std;
struct Numbers
{ int integer;
double real;
char symbol;
};
void show(Numbers x )
{ cout << "Integer:"<<x.integer<<endl;
cout << “Real:"<<x.real<<endl;
cout<<"Symbol:" << x.symbol<<endl;
57
}
int main()
{ Numbers a,b;
Numbers *p,*q;
p=&a;
q=&b;
p->integer=1;
p->real=2.5;
p->symbol=’a’;
(*q).integer=2;
(*q).real=5.1;
(*q).symbol='b';
show(a);
show(*q);
return 0;
}
У програмі оголошена структура Numbers, що має три поля: типу int, типу double і типу char. Крім
цього, описана функція show(), аргументом якої є змінна структури Numbers, а в результаті виконання
функції виводяться значення всіх полів аргументу.
У головному методі програми командою Numbers а, b оголошуються змінні а і b структури Numbers, а
командою Numbers * р, * q оголошені вказівники р і q на змінні структури Numbers. Значення
вказівникам (адреси) присвоюються командами р=&а і q=&b відповідно. При цьому використаний, як і в разі
змінних базових типів, оператор отримання адреси &. Поля змінної а заповнюються за допомогою команд p-
>integer=1, p->real=2.5 і p->symbol= а'. У цьому випадку посилання на поле реалізується через
вказівнік р і оператор ->. Поля змінної b заповнюються більш консервативним способом. Для цього
використані команди (* q).integer=2,(*q).real=5.1 і (*q).symbol='b' відповідно. Тут прийнято
до уваги, що інструкція *q є нічим іншим, як тією змінною, на яку посилається вказівнік q (тобто змінна b).
Аналогічно при виконанні функції show() її аргументом можна вказати як ім'я змінної (а чи b) структури
Numbers, так і інструкцію виду *р або *q. В результаті виконання програми отримаємо наступне:
Integer: 1
Real: 2.5
Symbol: а
Integer: 2
Real: 5.1
Symbol: b
Ім'я структури можна використовувати відразу після його оголошення в тих випадках, коли компілятору не
потрібно знати розмір структури, наприклад:
struct List; // Оголошення структури List
struct Link
{
List * p; // Вказівник на структуру List
Link * prev, * succ; // Указатели на структуру Link
};
struct List { / * Визначення структуры List */ };
Це дозволяє створювати зв'язні списки структур.
Вказівники на структури знаходять широке застосування, і в першу чергу при складанні динамічних списків.
Бітові поля
Бітові поля - це особливий вид полів структури. Вони використовуються для щільної упаковки даних,
наприклад, прапорців типу «так/ні». Мінімальна адресуєма комірка пам'яті - 1 байт, а для зберігання прапорця
досить одного біта. При описі полів структури в явному вигляді можна вказувати розмір полів. Мінімальний
розмір поля структури - один біт. Загальний синтаксис оголошення структури з явним зазначенням розмірів
полів має вигляд:
struct Ім'я
{
тип_поля1 ім'я_поля1: розмір1;
тип_поля2 ім'я_поля2: розмір2;
тип_поляN ім'я_поляN: розмірN;
58
};
При описі бітового поля після імені через двокрапку вказується довжина поля в бітах (ціла позитивна
константа):
struct Options
{
bool cеnterX: 1;
bool centerY: 1;
unsigned int shadow : 2;
unsigned int pаlette : 4;
};
Бітові поля можуть бути будь-якого цілого типу. При цьому для частини полів можна вказувати розмір, а для
інших - ні. Якщо поле має розмір в один біт, як його тип вказується unsigned (у числа, реалізованого за
допомогою одного біта, не може бути знака). Ім'я поля може бути відсутнім, такі поля служать для
вирівнювання на апаратну границю. Доступ до поля здійснюється звичайним способом - по імені. Адресу поля
отримати не можна, однак в іншому бітові поля можна використовувати точно так же, як звичайні поля
структури. Слід враховувати, що операції з окремими бітами реалізуються набагато менш ефективно, ніж з
байтами і словами, так як компілятор повинен генерувати спеціальні коди, і економія пам'яті під змінні
обертається збільшенням обсягу коду програми. Розміщення бітових полів в пам'яті залежить від компілятора
і апаратури.
// Явна вказівка розмірів полів структури
#include <iostream>
using namespace std;
struct BitFields
{ unsigned int state:1;
int n:2;
int m;
} str;
int main ()
{ cout<<"Enter a number:";
cin>>str.m;
str.state=str.m%2;
str.n=str.m%4-2;
cout<<"state=" << str.state<<endl;
cout<<"n=”<< str.n<<endl;
return 0;
}
У структури BitFields три поля: однобітове цілочислове поле state, двухбітове цілочислове поле n і
цілочислове поле m (розмір поля не вказаний).
Поле state може приймати всього два значення: 0 або 1. Поле n приймає цілочислові значення в діапазоні
від -2 до 1 включно, тобто всього 4 можливих значення: нагадаємо, що старший біт визначає знак числа, тому
двухбітове число 11 відповідає числу -2, двухбітове число 10 відповідає числу -1, число 00 відповідає нулю і
число 01 відповідає числу 1.
У головному методі програми буде запропоновано ввести користувачу ціле число. Це число записується в
поле m змінної str структури BitFields. У змінну state заноситься остача від ділення цього числа на 2, а
в змінну n записується остача від ділення введеного користувачем числа на 4 мінус 2. Значення полів змінної
str структури BitFields виводяться на екран. В результаті можемо отримати щось схоже на це:
Enter a number: 9
state = 1
n = -1
Таким чином, шляхом явної вказівки розмірів полів структури вдалося домогтися економії системних
ресурсів: для запису значень використовується мінімальна необхідна кількість бітів. Виникає природне
запитання: а що буде, якщо присвоюється бітовому полю (тобто полю, для якого явно вказано розмір)
значення, що виходить за допустимі для цього поля границі? Наприклад, що буде, якщо полю n значення
привласнювати за допомогою команди str.n = str.m% 4-4? Відповідь така: в зазначених випадках
відбувається автоматичне відкидання зайвих бітів. Зокрема, якщо поле m дорівнює 9, а поле n визначається як
str.n = str.m% 4-4, то цьому полю буде присвоєно значення 1. Пояснимо це. Так, остачею від ділення 9
на 4 є 1. Якщо відняти 4, отримаємо -3. У двійковому поданні число -3 має вигляд (останні три біта, всі старші
біти рівні 1) 101. Старші біти відкидаються, і в результаті в двухбітовому поданні отримуємо 01, що відповідає
числу 1.
59
Об'єднання (union)
Об'єднання (union) являє собою окремий випадок структури, всі поля якої розташовуються за однією і тією ж
адресою. Формат опису такий ж, як у структури, тільки замість ключового слова struct використовується
слово union. Довжина об'єднання дорівнює найбільшому з довжин його полів. У кожен момент часу в
змінній типу об'єднання зберігається тільки одне значення, і відповідальність за його правильне використання
лежить на програмістові.
Об'єднання застосовують для економії пам'яті в тих випадках, коли відомо, що більше одного поля одночасно
не потрібно:
#include <iostream.h>
int main()
{
enum Paytype {CARD, CHECK};
Paytype ptype;
union payment
{ char card[25];
long check;
} info;
/*присвоєння значень info та ptype*/
Switch (ptype)
{
case CARD: cout <<Оплата картою: “<<info.card; break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
Об'єднання часто використовують в якості поля структури, при цьому в структуру зручно включити додаткове
поле, яке визначає, який саме елемент об'єднання використовується в кожен момент. Ім'я об'єднання можна не
вказувати, що дозволяє звертатися до його полів безпосередньо:
#include <iostream.h>
int main()
{ enum Paytype {CARD, CHECK};
struct
{ Paytype ptype;
Union
{ char card[25];
long check;
};
} info;
... /*присвоєння значень info*/
switch (info.ptype)
{ case CARD: cout <<Оплата картою: “<<info.card: break;
case CHECK: cout <<Оплата чеком: “<<info.check; break;
}
return 0;
}
//Використання об'єднань
#include <iostream>
using namespace std;
union nums
{ unsigned short int n;
short int m;
};
void show(nums a )
{ cout << "n =”<< a.n << endl;
cout << "m = "<<a.m<<endl;
cout<<endl ;
};
int main()
{ nums un;
un.m=1;
60
show(un);
un.m=32767;
show(un);
un.m=65535;
show(un);
un.m=-1;
show(un);
un.m=-65536;
show(un);
return 0;
}
Оголошене в програмі об'єднання nums містить два члена: цілочислову беззнакову змінну n типу unsigned
short int і цілочислову змінну m типу short int. Підкрякщомо, що для запису обох змінних
використовується загальна область пам'яті. Зазвичай обсяг пам'яті для зберігання даних об'єднання
вибирається виходячи з розміру найбільшою (за типом) змінної. В даному випадку розмір пам'яті для
зберігання змінних однаковий, різниця тільки в інтерпретації записаних в цю пам'ять значень.
Ситуація наступна: при зверненні до змінної n або m, що є членами об'єднання, проглядається одна і та ж
область пам'яті. Тому змінюючи змінну n ми змінюємо змінну m і навпаки. На все, що відбувається можна
подивитися і з іншої точки зору: є область пам'яті, до якої можна звертатися через різні змінні, і в залежності
від типу змінної по-різному інтерпретувати записане в пам'ять значення. Щось подібне ілюструє і наведений у
лістингу програмний код: у головному методі програми створюється екземпляр un об'єднання nums, одному з
членів екземпляра об'єднання присвоюється значення, після чого перевіряється значення іншого члена. Для
зручності в програмі описана функція show (), якою виводяться на екран значення членів екземпляра
об'єднання (ім'я екземпляра об'єднання вказується аргументом функції).
Простежимо, яким буде результат виконання програмного коду. Для цього необхідно згадати деякі
особливості форматів unsigned short int і short int. Для використовуваного компілятора діапазон
зміни чисел типу short int становить від -32768 до 32767. Значення для чисел типу unsigned short
int лежать в межах від 0 до 65535. І в тому, і в іншому випадку числа реалізуються у вигляді 16-бітових
двійкових послідовностей. Іншими словами, в область пам'яті, виділену під екземпляр об'єднання, записується
послідовність з 16-ти нулів і одиниць. Якщо звернення до області пам'яті здійснюється через змінну m, то цей
двійковий код інтерпретується як число типу short int, а при зверненні до пам'яті через змінну n код
інтерпретується як число типу unsigned short int. Але сам код один і той же! Спочатку виконується
присвоєння m = 1. У цьому випадку як член n, так і член m отримують однакові значення (точніше, одне і те
ж значення інтерпретується однаково), тому результатом виконання команди show (un) буде
n = 1
m = 1
Аналогічний результат отримуємо в результаті присвоєння un.m = 32767 (обидва члени мають однакове
значення):
n = 32767
m = 32767
Ситуація принципово змінюється після виконання команди un.m = 65535. В результаті отримаємо:
n = 65535
m = -1
Щоб зрозуміти, чому так відбувається, більш детально розглянемо процедуру інтерпретації чисел в
двійковому коді для різних типів даних. Значення 65535 виходить за допустимі межі діапазону даних short
int, тому при запису значення в змінну m старші біти в двійковому поданні числа відкидаються. Само по собі
значення 65535 в двійковому коді представляється у вигляді 0 ... 00111 ... 11, тобто 16-ть одиниць з нульовими
старшими бітами (загальна кількість біт залежить від специфіки використовуваного компілятора). Після
відкидання "зайвих" бітів залишаються останні 16 одиниць.
Таким чином, в пам'ять, виділену під екземпляр об'єднання, записано значення 111 ... 11. Якщо це значення
інтерпретується як unsigned short int (змінна n), отримуємо в десятковій системі значення 65535. Якщо же
значення інтерпретується як таке, що відноситься до типу short int, ситуація дещо інша. В цьому випадку
старший біт відповідає за знак числа, і одиничний старший біт означає, що число негативне. Для отримання
відповідного десяткового значення необхідно виконати побітову інверсію, додати одиницю, перевести число в
десяткову систему числення і додати "мінус". После побітової інверсії отримуємо 000. ..00. Додавши одиницю,
отримуємо число виду 000. ..01, що відповідає числу 1, а зі знаком "мінус" це -1 (значення змінної m).
Аналогічна ситуація складається после виконання команди un.m = -1.
61
Описані вище перетворення необхідно виконати в зворотному порядку, после чого стане ясно, що значенням
змінної n буде 65535. До речі, такий же результат отримаємо, якщо дамо un.n = 65535. А ось якщо
виконати команду un.m = -65536 і потім вивести значення змінних екземпляра об'єднання, отримаємо
n = 0
m = 0
Щоб перевести число -65536 в двійкове подання, необхідно модуль цього числа (тобто 65536) перевести в
двійковий код, інвертувати кожен біт і додати одиницю. Число 65536 в двійковому поданні має вигляд 000 ...
001 000 ... 00 (в даному випадку кількість N старших незначущих нульових бітів значення не має). Після
інвертування отримуємо 111 ... 110 111 ... 11. Додавши одиницю, отримаємо 1000 ... 001000 ... 00.
У змінну m записуються останні 16 біт, тобто число 000 ... 00, або 0. Таке ж значення і у змінної n.
Об'єднання застосовуються також для різної інтерпретації одного і того ж бітового подання (але, як правило, в
цьому випадку краще використовувати явні операції перетворення типів). Як приклад розглянемо роботу зі
структурою, що містить бітові поля:
struct Options
{ bool centerX:1;
bool centerY:2;
unsigned int shadow:2;
unsigned int palette:4;
};
union
{ unsigned char ch;
options bit:
} option = {0xC4};
cout << option.bit.palette;
option.ch= 0xF0; // накладення маски
У порівнянні зі структурами на об'єднання накладаються деякі обмеження. Сенс деяких з них стане
зрозумілий пізніше:
1. Об'єднання може ініціалізуватися тільки значенням його першого елемента;
2. Об'єднання не може містити бітові поля;
3. Об'єднання не може містити віртуальні методи, конструктори, деструктори і операцію присвоєння;
4. Об'єднання не може вxодити в ієрархію класів.
Командою enum color {red, green, blue, yellow, black} car створюється перерахування
color. Змінні, що відносяться до цього типу (тобто відносяться до перерахування color), можуть набувати
значень red, green, blue, yellow і black. Це не текстові значення, а числові. Іншими словами,
ключові слова red, green, blue, yellow і black - це назви цілочислових констант. За замовчуванням
перша в списку значень константа дорівнює 0, а кожна наступна на одиницю більше попередньої. Таким
чином, red відповідає значенню 0, green відповідає значенню 1, blue відповідає значенню 2, yellow
відповідає значенню 3 і black відповідає значенню 4.
У програмі оголошуються дві змінні перерахування color. Змінна саr вказана при оголошенні
перерахування одразу после фігурної дужки варіанти вибору. Змінна bus оголошена командою color bus.
Значення цих змінних присвоюються командами car = green і bus = yellow відповідно. Проте слід
пам'ятати, що змінні, насправді, отримують цілочислові значення. У цьому легко переконатися за результатом
виконання програми. Зокрема, на екрані з'явиться повідомлення
Car is 1 and bus is 3
62
Легко здогадатися, що в даному випадку замість ідентифікаторів green і yellow при виведенні на екран
використовувалися відповідні числові значення.
Використовувані за замовчанням значення для цілочислових констант в списку можливих значень можуть
бути змінені. В цьому випадку после відповідного імені через знак рівності вказується значення відповідної
константи.
// У списку перерахування явно вказані значення констант
enum color {red=10, green, blue=100, yellow, black};
color car,bus,van;
car=green; bus=blue; van=black;
cout<<”Car is “<<car<<", bus is " << bus << “and van is “<<van<<endl;
В результаті виконання програми з'явиться рядок:
Константі red явно присвоєно значення 10, тому константа green має значення 11 (на одиницю більше). Для
константи blue явно вказано значення 100, тому константи yellow і black мають значення 101 і 102
відповідно.
Скориставшись оголошенням виду typedef тип нове_ім'я_типу, в програмі створюють нове ім'я для
типу даних.
Командою typedef int * IntPointer оголошено нове ім’я IntPointer для даних "вказівник
на ціле число". Згодом ідентифікатор IntPointer використовується при оголошенні нових змінних,
причому результат, наприклад, команди IntPointer * р, виглядає як якщо б це було оголошення int *
р. В результаті виконання команди з'явиться повідомлення n = 101.