1. ОСНОВНІ СТАНДАРТНІ
ТИПИ МОВ С ТА С++.
2. ВИДИ КОНСТАНТ ТА
КОНСТАНТНІ ВИРАЗИ.
3. ВИРАЗИ ТА ОПЕРАЦІЇ,
ПРІОРИТЕТ ОПЕРАЦІЙ ТА ПОРЯДОК ОБЧИСЛЕННЯ ВИРАЗІВ.
6. МАСИВИ ТА ЇХ ЗВ’ЯЗОК
З ВКАЗІВНИКАМИ; СИМВОЛЬНІ МАСИВИ; ДВОВИМІРНІ МАСИВИ.
7. СТАНДАРТНИЙ
ФОРМАТОВАНИЙ ВВІД ТА ВИВІД ІНФОРМАЦІЇ В С.
8. СТАНДАРТНІ ПОТОКИ
ВВЕДЕННЯ ТА ВИВЕДЕННЯ ІНФОРМАЦІЇ В С++.
10. ОБЛАСТІ ВИДИМОСТІ
ТА КЛАСИ ПАМ’ЯТІ ЗМІННИХ ТА ФУНКЦІЙ В С/С++.
11. СТРУКТУРИ В С ТА
С++, ЇХ ВИКОРИСТАННЯ У ЗВ’ЯЗАНИХ СПИСКАХ.
13. ОРГАНІЗАЦІЯ ФАЙЛІВ
ТА РОБОТА З НИМИ В МОВІ С.
14. ОСНОВНІ ДИРЕКТИВИ
ПРЕПРОЦЕСОРА, МАКРОВИЗНАЧЕННЯ ТА ПРОБЛЕМА ЇХ ВИКОРИСТАННЯ.
15. ЗАГАЛЬНА СТРУКТУРА
ПРОГРАМИ В МОВІ С.
17. СТРУКТУРА ПРОГРАМИ
МОВОЮ С.
18. ОСНОВНІ ПРИНЦИПИ
ООП, ЇХ РЕАЛІЗАЦІЯ В МОВІ С++. КЛАСИ, ОБ’ЄКТИ, ЕКЗЕМПЛЯРИ.
20. ПАРАМЕТРИ ФУНКЦІЙ
ЗА ЗАМОВЧУВАННЯМ.
24. СТРУКТУРИ В МОВІ
С++, ЇХ ВІДМІНИ ВІД СТРУКТУР В МОВІ С.
26. ДАНІ-ЧЛЕНИ ТА
ФУНКЦІЇ-ЧЛЕНИ КЛАСУ, ВКАЗІВНИК THIS.
27. СТАТИЧНІ ЧЛЕНИ
КЛАСУ – ДАНІ ТА МЕТОДИ.
28. КОНСТРУКТОРИ ТА
ДЕСТРУКТОР КЛАСУ, КОНСТРУКТОР КОПІЮВАННЯ.
29. СПАДКУВАННЯ:
БАЗОВИЙ ТА ПОХІДНИЙ КЛАС, ВІРТУАЛЬНІ ФУНКЦІЇ, ПОЛІМОРФІЗМ, АБСТРАКТНИЙ КЛАС.
30. ПЕРЕВАНТАЖЕННЯ
ОПЕРАТОРІВ.
31. ШАБЛОНИ ФУНКЦІЙ ТА
КЛАСІВ.
32. ФАЙЛОВІ ПОТОКИ;
СТАНДАРТНІ ПОТОКИ ВВЕДЕННЯ-ВИВЕДЕННЯ, ФОРМАТУВАННЯ.
34. ПОНЯТТЯ ПРО ПРОЕКТ
В МОВІ С++. ВИКОРИСТАННЯ ТА ПРИЗНАЧЕННЯ ЗАГОЛОВОЧНИХ ФАЙЛІВ.
В С
(С++) будь-яка змінна програми має бути описана раніше ніж бути використаною.
Як правило (але не обов’язково), описи (або декларації) всіх змінних поміщають
на початку функції.
Декларація
має вид: <тип> <список
змінних>; або <тип> <
змінна> = < початкове значення>;
Присвоїти
подібним чином початкове значення можна лише змінній стандартного типу.
Присвоєння відбувається на етапі компіляції програми. Основні стандартні типи
містяться в таблиці:
Тип |
Кількість
байтів |
Діапазон
значень |
int
(цілий) |
2 |
-32767..32768 |
unsigned
(цілий без знаку) |
2 |
0..65535 |
short
(короткий цілий) |
2 |
Аналогічний
типу int |
long
(довгий цілий) |
4 |
|
float
(дійсний) |
4 |
|
double
(довгий дійсний) |
8 |
|
char
(символьний) |
1 |
-127..128 |
unsigned char (без знаку) |
1 |
0..255 |
Зауваження. 1.
Таким чином символьний тип є підмножиною цілого типу. Значенням змінної
символьного типу є її код в кодовій таблиці символів.
2. При
визначенні констант до ідентифікатора будь-якої з них можна приписати літери u
(unsigned) або l (long).
Приклади декларацій змінних:
int i = 0, size_of_array = 10;
float x, y, z;
char ch = ‘A’;
unsigned score = 0;
unsigned long lnum = 0L;
Константи в мовах
С та С++ бувають чотирьох типів.
1.
Цілі константи задаються в
десятковій, вісімковій або шістнадцятковій системах числення:
А)
десяткові константи складаються із
десяткових цифр від 0 до 9, причому перша не може бути нулем, наприклад: 100, 1024, -1, 0;
Б)
вісімкові константи завжди
починаються з 0 і включають цифри від 0 до 7, наприклад: 017, 0124;
В)
шістнадцяткові константи починаються
з 0, за яким має знаходитись символ Х або х, а потім одна або більше
шістнадцяткових цифр (тобто цифри від 0 до 9 та латинські літери від A
до F або від a до f),
наприклад: 0X17, 0XA1; 0x12b.
2.
Дійсні константи мають
десяткову крапку або експоненційну частину, або те і інше, наприклад: 1.2, 1е-10, -1.5е+05.
3.
Символьна (інколи літерна)
константи – це один символ, узятий в одинарні лапки, наприклад ‘a’
або ‘0’. Така константа має ціле значення, яке збігається з кодом даного
символу в таблиці ASCII. Деякі символьні константи
використовуються як керуючі символи для позначення, наприклад, нового рядка
– ‘\n’ або
табуляції – ‘\t’. Ці керуючі символи ще називаються esc-послідовностями і
зображуються двома літерами, перша з яких обернена похила риска (слеш).
Будь-який символ кодової таблиці можна в свою чергу задати у вигляді esc-послідовності
з вісімковим або шістнадцятковим кодом, наприклад, ‘\0’ – це символ з нульовим
кодом (так звана null-літера), а не цифра 0; ‘\013’ – це те
саме, що ‘\v’ – символ вертикальної табуляції.
Символи слеш, одинарні та подвійні лапки із зрозумілих міркувань теж
зображуються у вигляді esc-послідовностей :‘\\’ – слеш; ’\’’ –
одинарні лапки; ‘\”’ – подвійні лапки.
4.
Стрінгова (рядкова або
текстова) константа – це нуль або більше символів, узятих в подвійні лапки,
наприклад “String constant”, “” –
порожній стрінг. У внутрішньому поданні стрінгова константа займає на одну
одиницю пам’яті більше, ніж кількість символів в стрінгу, бо останнім символом
автоматично записується null-літера \0, яка завершує будь-який
стрінг. Тому зрозуміло, що ‘x’ та “x”
– різні речі.
Вираз –
це конструкція з лексем мови. Елементи виразу називаються операндами, а дії над
ними – операціями. Операції бувають унарними, якщо виконуються над одним
операндом, та бінарними, якщо мають два операнди. Крім того, вони об’єднуються
в наступні групи операції.
1.
Арифметичні операції (+,-,*,/,%(взяття
залишку від ділення- тільки для двох цілих операндів)).
2.
Операції відношення (==; !=;
<– ; >;<=; >=;).Всі операції відношення визначають цілий результат.
Він рівний 1, якщо результатом перевірки є істина, і 0 у супротивному випадку.
3.
Логічні операції.
Логічні
операції виконуються над операндами цілих, дійсних, символьного типів. Операнд
трактується як “хибність”, якщо він рівний 0 і як “істина” у супротивному
випадку. Логічні операції позначаються символами: (&&–логічне множення
або логічне “І”); (||–логічне додавання або логічне “АБО”); (! –логічне заперечення); Приклади: x
&& y еквівалентно (x
!= 0) && (y != 0); !(x
|| y) еквівалентно (x
= 0) && (y = 0);
!x
еквівалентно (x == 0);
4.
Побітові операції.
Полягають
у виконанні певних логічних операцій над бітами внутрішнього подання операндів тільки
цілих типів. Побітовими операціями є наступні: (& –побітове “І” (кон’юнкція бітів));
(|–побітове “АБО” (диз’юнкція бітів)); (^ –побітове
виключне “АБО”); (<<(>>) –
побітовий зсув вліво(вправо)); (~ –побітове заперечення).
5.
Операції присвоєння. <змінна> = <вираз>; або ще можна <змінна> op=
<вираз>; (op - + - * / %
<< >> & | ^) <=>
<змінна> = <змінна> op
<вираз>;
6.
Операції інкременту та декременту.
Операція
інкременту є унарною (тобто застосовується тільки до одного операнда),
збільшуючи змінну одного з цілих типів на 1 і позначається ++ , а операція
декременту – зменшує на 1 і позначається --. Знак будь-якої з цих операцій може
стояти перед змінною, наприклад ++k
(префіксна форма), або після змінної, наприклад k++
(постфіксна форма). В першому випадку змінна відповідним чином змінюється, а
потім використовується. В другому – навпаки: спочатку використовується значення
змінної, а потім вона змінюється.
7. Умовна
операція (умовний вираз).
Синтаксис
умовної операції:
вираз1 ? вираз2 : вираз3;
ПРІОРИТЕТ
ОПЕРАЦІЙ:
Операції |
Порядок
виконання |
! ~ ++ --
+ - * (унарні операції) |
Зправа
наліво |
*
/ % (операції типу множення) |
Зліва
направо |
+
- (операції типу додавання) |
Зліва
направо |
<<
>> (операції зсувів) |
Зліва
направо |
< <=
> >= (операції
відношення) |
Зліва
направо |
== != (операції відношення) |
Зліва
направо |
&
(побітове множення) |
Зліва
направо |
^
(побітове виключне АБО) |
Зліва
направо |
|
(побітове додавання) |
Зліва
направо |
&&
(логічне множення) |
Зліва
направо |
||
(логічне додавання) |
Зліва
направо |
?
: ; (умовний вираз) |
Зправа
наліво |
= +=
-= *= /=
%= &= ^=
|= <<= >>= (операції
присвоєння) |
Зправа
наліво |
• ІНСТРУКЦІЇ ВИБОРУ ( IF; IF-ELSE; SWITCH );
синтаксис: if<вираз><інструкція> ;
синтаксис: if<вираз><інструкція 1>; else <інструкція 2>;
синтаксис: switch (вираз) {
case константний_вираз_1: інструкції
case константний_вираз_2: інструкції
case константний_вираз_N: інструкції
default : інструкції }
Тут типом виразу є будь-який з цілих типів. Всі
константні
вирази повинні мати той самий тип. Гілка default необов'язкова. Виконання перемикача
відбувається
як "перехід на мітку". Щоб перервати виконання перемикача, необхідна
інструкція break.
• ІНСТРУКЦІЇ ЦИКЛІВ ( FOR; WHILE; DO-WHILE );
Цикл
for: for (вираз1; вираз2; виразЗ) <інструкція>;
Цикл while (цикл з передумовою ): while (вираз) <інструкція>;
Цикл
do-while (цикл з післяумовою): do <інструкція>; while (вираз);
• ІНСТРУКЦІЇ BREAK; CONTINUE;
Інструкція break; викликає вихід з будь-якого циклу або розгалуження.
Інструкція continue; викликає перехід до перевірки умови у циклах while та do-while. У циклі for - перехід до обчислення виразу 3.
Вказівник
–
це
змінна, яка може містити адресу деякого іншого об'єкта. Ним може бути в
найпростішому випадку інша змінна або масив, структура, функція, клас (в C++) тощо.
Синтаксис визначення
вказівника: <тип> *
<ідентифікатор_вказівника>;
Приклади: int *
р_і; // р_і
- вказівник на
тип int; char * р_с;
// р_с -
вказівник на тип char
В цих прикладах вказівники поки
що не вказують ні на які об'єкти - вони не проініціалізовані.
В мові С для
ініціалізації вказівника можна використати один з двох
способів:ініціалізація адресою існуючої змінної або ініціалізація адресою динамічно виділеної
пам'яті (з допомогою функції malloc (), calloc
()).
Приклади.
int і; int *р_і
= &і; //
вказівник р_і вказує
на і // унарна
операція & -
взяття адреси
*р_і =
10; // змінній
і присвоєно значення 10
++*р_і; //
змінна і збільшена
на 1
cout «
*р_і; //
виводимо значення і
Зауваження. У виразі
(*рі)++; дужки обов'язкові на відміну від виразу ++*р_і; із-за порядку
виконання операцій.
Адресна арифметика
Для будь-яких вказівників на один
тип допустимі перевірки на рівність та нерівність їх між собою та
присвоєння, а також присвоєння та порівняння з вказівником null. Якщо р1 та р2 - вказівники и на один
і той самий масив, то допустимими операціями з ними додатково є такі:
р1 < р2 перевірка: "р1
вказує на елемент масиву з меншим індексом, ніж той, на який вказує р2";
р1 <= р2 перевірка: "р1 вказує на елемент масиву
з індексом не більшим, ніж той, на який вказує р2";
р1 > р2 перевірка: "р1
вказує на елемент масиву з більшиміндексом, ніж той, на який вказує р2";
р1 >= р2 перевірка: "р1
вказує на елемент масиву з індексом не меншим, ніж той, на який вказує
р2";
р2 - р1 кількість елементів
масиву між елементами, що адресуються р2 та р1;
p1 + k
вказівник на k-ий елемент масиву, рахуючи вперед від
елементу, що адресується р1.
p1 - k вказівник на k-ий елемент
масиву, рахуючи назад від елементу, що адресується р1.
Динамічний розподіл пам'яті.
Динамічним розподілом пам'яті
керують функції із файлу <stdiib.h>
(<cstdlib> у просторі імен std):
void* malloc (size_t size); - виділяє size байтів
пам'яті в області Heap і повертає вказівник на початок цієї
області або вказівник null, якщо пам'ять невиділена. pi = (int*) malloc (sizeof(int)); // вказівник
на 1 елем.
void* calloc (size_t n, size_t size) ; - повертає вказівник на початок області
пам'яті, достатньої для збереження n
об'єктів розміру size або вказівник null,
якщо пам'ять не виділена. p
= (int*) calloc(5, sizeof(int));
void* realloc (void* p, size_t size); - змінює розмір пам'яті, на яку вказує
вказівник р, на size
та повертає вказівник на нову область, або вказівник null, якщо зміна неможлива. Для частини пам'яті
розміру, рівному найменшому із старого та нового значень, зміст не зміниться. p_i = (int*) realloc (p_i, 10*sizeof(int));
void free (void* p); - звільняє область пам'яті, на яку вказує
вказівник р. free (p);
Синтаксис
визначення масиву: <тип>
<ідентифікатор_масиву> [кількість_елементів];
Тут<тип> визначає тип
елементів масиву, які індексуються від 0 до кількість_елементів - 1.
Приклади:
int іАrrау
[10] ; визначили масив із 10
елементів
for (int і = 0; і < 10; і++)
іАrrау [і] = і*і;
Увага: ідентифікатор масиву - вказівник на його нульовий елемент, таким чином, іАггау [і] - це *(іАrrау + і) , а &іАrrау [і] - іАrrау + і. Масив
можна проініціалізувати в момент визначення:
char text [ ]
= {'Н','е','1','1','о','\0'}; -
символьний масив.
int* arr = new int [розмірність
масиву]; або void* malloc
(sizeof(int) * <розмірність масиву>);
char* arr = new char [розмірність
масиву]; або void* malloc
(sizeof(char) * <розмірність масиву>);
С та С++
дають можливість задавати масиви вимірності більшої за 1.
Синтаксис
визначення двовимірного масиву (аналог матриці в алгебрі):
<тип_елементів>
<ідентифікатор_масиву> [кількість_рядків] [кількість_стовпчиків];
Хоча ми
інтерпретуємо багатовимірні масиви як прямокутні таблиці, в пам’яті елементи
розташовані послідовно, таким чином, що найшвидше змінюється останній індекс,
тобто у випадку двовимірного масиву – по рядках.
1. Форматований стандартний вивід в С.
Форматований
стандартний ввід в С здійснює функція: int printf (char* format, arg1,arg2,..);
Вона повертає
кількість успішно виведених аргументів, перетворює, форматує і друкує свої
аргументи в стандартному вихідному файлі під керівництвом формату – стрінгу format.
Останній містить два види об’єктів: звичайні літери, які безпосередньо
копіюються в стандартний вивід, і специфікації перетворень, кожна з яких
починається з символа % . Кількість аргументів функції printf
має відповідати кількості специфікацій формату. Кожна специфікація завершується
спеціальною літерою-специфікатором. Вони наведені в наступній таблиці.
%[маркер][ширина поля
виводу][.точність]літера-специфікатор
маркер - це знаки: + (обов'язковий вивід знаку числа) або - (притискання до лівого краю поля
виводу) Підкреслені елементи є обов'язковими.
Таблиця
літер-специфікаторів функції printf
Символ |
Тип аргументу |
Вид друку |
d, i |
int |
десяткове ціле |
o |
int |
вісімкове ціле без знаку |
x, X |
int |
16-кове ціле без знаку |
u |
int |
десяткове ціле без знаку |
c |
int |
символ |
s |
char* |
друкуються символи до ’\0’ або в кількості, визначеною точністю |
f |
double |
[-]m.dddddd (за
замовчуванням точність – 6 знаків) |
e, E |
double |
[-]m.ddddddE |
g, G |
double |
використовує формат %e або %f |
p |
void* |
вказівник |
% |
|
аргумент не перетворюється, друкується % |
Приклад: char* s = “Holiday”; printf (“\n %-10.5s”, s); /* Поле виводу має вид: Holid______ */
2. Форматований стандартний ввід в С.
Форматований
стандартний ввід в С здійснює функція: int scanf (char* format, arg1,arg2,..);
Вона зчитує
символи зі стандартного вхідного потоку, інтерпретує їх згідно зі
специфікаціями стрінгу format і розсилає результати у свої аргументи,
кожен з яких має бути вказівником. Повертає кількість успішно введених
аргументів. Стрінг format містить два види об'єктів: звичайні
літери, які, як очікується, мають з'явитись у стандартному вводі, і
специфікації перетворень, кожна з яких починається із символу % . Специфікація
формату вводу має такий вид: %[маркер][ширина
поля введення]літера-специфікатор маркер - це знак * (ігнорувати присвоєння) Підкреслені
елементи є обов'язковими.
Таблиця
літер-специфікаторів функції
scanf
Символ |
Тип аргументу |
Вид вхідних даних |
d |
int* |
десяткове ціле |
i |
int* |
будь-яке ціле |
o |
int* |
вісімкове ціле без знаку |
x |
int* |
16-кове ціле без знаку |
u |
int* |
десяткове ціле без знаку |
c |
char* |
символ |
s |
char* |
стрінг |
f, e, g |
double* |
дійсне число |
% |
|
знак %, ніяке присвоєння не виконується |
Пр-д: scanf (“\n
%*2d %с %*2d”,&ch);
-
cin відповідає
стандартному вводу (тобто для вводу об’єктів стандартних типів), зв’язаний з
клавіатурою;
-
cout відповідає
стандартному виводу (тобто для виводу об’єктів стандартних типів), зв’язаний з
дисплеєм;
-
cerr відповідає
стандартному виводу для повідомлень про помилки, зв’язаний з дисплеєм;
-
cprn відповідає
стандартному виводу (тобто для виводу об’єктів стандартних типів), зв’язаний з принтером;
Вивід
здійснюється за допомогою оператора << , ввід – з допомогою оператора
>>.
Для керування
станом потоків використовуються так звані маніпулятори потоків. Їх визначення
містить файл <iomanip>, який
необхідно підключити командою #include. Якщо
деякий маніпулятор з'являється в потоці, то він змінює стан потоку до тих пір,
поки в потік не буде відправлений інший маніпулятор.
Наприклад, за
замовчуванням, значення цілих типів читаються та записуються у десятковому
форматі. Маніпулятори hex, oct, dec
змінюють формат відповідно на 16-ковий, 8-ковий, 10-ковий. Маніпулятори showbase, noshowbase встановлюють та відміняють виведення перед числом
початкових символів 0 або 0x для позначення
основи системи числення. Маніпулятори uppercase, nouppercase визначають вигляд 16-кових цифр. При введенні та
виведенні дійсних значень за замовчуванням діє маніпулятор fixed
(формат з фіксованою десятковою крапкою). Маніпулятор scientific змінює цей формат на формат з рухомою десятковою
крапкою. Маніпулятор setprecision (n) або
функція-член precision (n)
визначають точність n знаків (за замовчуванням точність 6).
Декларація функції в C/C++
Декларація функції-це можливість
зробити функцію видимою для компілятора. Декларація виглядає так, як і
визначення функції, проте не містить тіла функції. В одному програмному файлі допустимо
кілька декларації і лише одне визначення. В декларації функції ідентифікатори формальних
параметрів необов'язкові.
Нові можливості функцій в мові
C++
Визначення
функції в C/C++
Визначення
функції включає
наступні елементи(ANSI-стандарт): static або extern(вживається
за замовчуванням) -> вказівка про можливість використання функції поза даним
файлом;
(static або extern(вживається за замовчуванням)) <тип результату(або void)>
ім’я_функції (<параметри ф-ї>) { // тіло ф-ї; return <вираз(тип виразу
повинен збігатись з типом результату функції)>; }
Рекурсія.
Рекурсивною
називається функція, яка безпосередньо або непрямим чином (тобто через деяку
іншу функцію) викликає саму себе. Розглянемо приклад рекурентної функції. Приклад 4. (Керніган & Рітчі). /*
Функція переводить цілу змінну n у послідовність
символів, що зображують її */
void itostr ( int n )
{ int temp;
if ( n < 0 )
{
putchar ('-') ; n
= -n;
}
if (temp
= n/10) itostr (temp);
/* Це рекурсія*/ putchar
( n%10 + '
0');
}
Зрозуміло,
що в рекурентній функції має бути умова виходу з рекурсії, інакше функція
викликатиме саму себе, аж доки не переповниться стек. У даному прикладі такою
умовою є перевірка на нуль залишку від ділення п на ю. Після виходу з кожного
рівня рекурсії функція друкує черговий символ числа п. Зверніть увагу на
останній рядок функції - функція putchar друкує
символ, код якого передається їй як аргумент. Тому, якщо n%10
- це чергова цифра числа, то n%10 + '
0' - це код цієї цифри.
Деякі
стандартні функції. Математичні функції містяться у файлі <math.h>.
Далі аргументи х та у мають тип double, n
- тип int; усі функції повертають значення типу double.
sin (x) -синус
х; cos
(x) - косинус х; tan (x)
-тангенс х;
Вказівник на функцію можна
визначити за допомогою інструкції typedef: typedef double (*p_function) (double);
Тепер p_function - це назва
типу вказівника на функцію, яка приймає аргумент типу double
та повертає результат типу double. Цьому
вказівнику можна присвоїти ідентифікатор будь-якої функції відповідного виду
(ідентифікатор функції є вказівником на неї).
typedef
double (*p_fun) (double);
void
fun(p_fun f)
{
cout << f(3.1415) << endl;
}
int
main() { fun(cos);}
Деякі стандартні функції. Математичні функції містяться у файлі <math.h>. Далі аргументи х та у мають тип double, n - тип int; усі функції повертають значення типу double.
sin
(x) -синус х; cos (x) -
косинус х; tan
(x) - тангенс х.
Областю
видимості
(областю дії) імені (об’єкту) називається та частина програми, де
це ім’я може використовуватись. За видимістю змінні поділяються на
локальні(видимі лише в певній частині програми) і глобальні(видимі для всієї
програми).
Класи пам'яті
об'єктів мов C/C++
Класи пам'яті зовнішніх глобальних об'єктів - ними є функції та змінні |
Класи пам'яті внутрішніх локальних об'єктів - ними є
лише змінні |
||
extern (за
замовчуванням) |
Доступ можливий з усіх файлів програми |
auto(за замовчуванням) |
Доступ можливий лише у блоці, де визначений об'єкт |
static |
Доступ можливий лише у файлі, де визначений об'єкт |
register |
Компілятор намагатиметься розміщувати об'єкт на регістрах |
Структура в мові С(С++) – це тип даних, який
складається з визначеної кількості елементів, що називаються членами структури
(інколи – полями структури). Члени структури можуть мати різні типи. Синтаксис
визначення структури:
struct tag_name
{
<тип_1> mem_1;
<тип_2> mem_2;
...
<тип_n> mem_n;
};
Тут mem_1,
mem_2 , ... , mem_n
– члени структури, визначеної з тегом (тег – дослівно означає наклейка) tag_name.
Тепер змінну, яка відноситься до структури такого типу, або, як ще кажуть екземпляр цієї структури, можна
визначити таким чином:
struct
tag_name <змінна-структура>;
Зауваження.
1. При
визначенні структури тег не є обов’язковим. Але тоді екземпляри структури,
визначеної анонімно – без тегу, повинні визначатись відразу після фігурної
дужки, що закриває тіло структури.
2. При визначенні екземпляру структури в мові С
вживання службового слова struct обов’язкове, а
в С++ – може бути пропущене.
3. Обмежень на типи членів структури немає, крім
одного – членом структури не може бути екземпляр даної структури, проте може
бути вказівник на неї.
Допустимі операції з структурами.
Із структурами можна виконувати наступні дії:
-
доступ до членів структури ;
-
копіювання та присвоєння структур;
-
взяття адреси структури.
Масиви структур.
Можна визначати масиви,
які мають структури своїми елементами. Наприклад, масив
struct Person group [25];
містить 25 елементів типу
структура Person.
Вкладеність структур.
Оскільки структури можуть включати члени довільних
типів, природно виникають вкладені структури, тобто такі, які є членами іншої
структури. Розглянемо черговий приклад.
Приклад
/* Вкладеність структур */
struct Date
{ /* визначаємо структуру, що містить дату */
unsigned
day, month, year;
}
struct Person
{ /*
визначаємо структуру, що містить інформацію про особу */
int num;
char name
[20];
char address
[20];
struct Date birthday;
};
Структури із
вказівниками на себе.
Однією з поширених форм організації даних є списки
та дерева. Списком називається послідовність структур, зв’язаних в один або в
обидва боки між собою. Дерево (бінарне) – це граф, вузлами якого є структури,
кожна з яких має не більше двох зв’язків.
В
будь-якій з вказаних форм організації даних використовуються структури, членами
яких є вказівники на саму структуру. Розглянемо для прикладу однозв’язний
список (тобто список, між елементами якого встановлені зв’язки в один бік).
Приклад /* Однозв’язний список – кожна структура
містить вказівник last на попередню структуру */
struct
Person
{
unsigned key;
char name
[20];
struct Person *last;
};
/* Функція build_list будує однозв’язний список заданої довжини n і повертає вказівник на початок списку*/
struct
Person* build_list (unsigned n)
{
struct Person
*p, *q;
int i;
q = NULL;
for
(i=1; i<=n; i++)
{
p
= (struct Person*)malloc (sizeof(struct Person));
if ( p != NULL )
{
p->key
= i;
p->last = q;
q
= p;
}
}
return q;
}
Об’єднання в С(С++)
– це тип даних, який в одній і тій самій області пам’яті може містити (в різні моменти
часу) об’єкти різних типів. Це певний аналог варіантної частини записів в
Паскалі. Іншими словами, об’єднання можна визначити як структуру з нульовим
зміщенням членів структури відносно початку структури. Доступ до членів
об’єднання здійснюється так само, як і до членів структури. І взагалі,
об’єднання може бути членом структури та навпаки.
Синтаксис визначення об’єднання:
union tag_name
{
<тип_1> mem_1;
<тип_2> mem_2;
...
<тип_n> mem_n;
};
Приклад
/* Об’єднання
table може містити одну з трьох змінних ival,
fval, *ps */
union table
{ int ival;
float fval;
char *ps;} u, *pu;
Змінна u буде достатньо великою, щоб помістити будь-яку зміну вказаних типів. Записавши,
наприклад,
u.ival = 1; ми
матимемо справу з цілою змінною, яку можна використовувати в будь-яких виразах,
дозволених синтаксисом. Після ініціалізації вказівника pu = (union
table)malloc
(sizeof(union table)); можна
здійснювати доступ до членів об’єднання з допомогою конструкції: pu->fval = 1.0; яка тепер є дійсною змінною.
Переліки в С
(С++).
Перелік (або зліченний тип) – це унікальний тип,
значення якого покриваються множиною іменованих констант, які, власне, і
називаються переліком. Синтаксис визначення переліків запозичений у структур та
об’єднань: enum tag_enum
{<перелік констант>};
Бітові поля в C/C++
Бітові поля в мові
С(С++) - це структура, яка складається з визначеної кількості окремих бітів.
Для кожного члена такої структури вказується його довжина в бітах. Синтаксис :
struct bit_field
{
<тип_1> mem_l : nl; // nl - довжина в
бітах
<тип_2> mem_2 : n2; //
п2 - довжина в бітах
<тип_п> mem_n : nn; // nn -
довжина в бітах
};
Окремий іменований набір
інформаційних записів, утворений на зовнішньому пристрої, називається файлом
(фізичним). Доступ до елементів файлу – послідовний.
Для керування роботою з файлами
програма повинна повідомляти системі певну інформацію про файл, наприклад, його
ідентифікатор, повний шлях до нього, характер роботи з файлом (читання чи
запис), місце знаходження поточного запису в файлі, чи були попередні помилки
при роботі з файлом. Процедура початкової ініціалізації цієї інформації
називається відкриттям файлу. Отже, будь-який файл перед
виконанням першої операції з ним має бути відкритим з допомогою стандартної
функції fopen , яка заносить початкову інформацію про
файл в стандартну структуру з іменем FILE .
Декларація цієї стандартної
функції має вид: FILE* fopen ( char* filename, char* mode);
Перший параметр – це стрінг, який
має містити повне ім’я файлу, другий параметр, також стрінг, визначає режим роботи
з файлом, який може бути одним з наступних:
“r” – відкрити існуючий файл для вводу з нього
(читання);
“w” –
створити новий файл або відкрити (та звільнити від попереднього вмісту)
існуючий файл для виводу (запису) в його початок;
“a” –
створити новий файл для виводу або відкрити існуючий файл для виводу в кінець файлу;
“r+” –
відкрити існуючий файл для оновлення (тобто читання та запису), яке буде
здійснюватись з його початку;
“w+” –
створити новий файл або відкрити існуючий файл для оновлення, яке буде
здійснюватись з його початку;
“a+” –
створити новий файл або відкрити існуючий файл та підстроїтись в його кінець
для оновлення.
В разі успішного відкриття файлу
ця функція повертає так звану файлову змінну – вказівник на структуру FILE;
в разі виникнення помилки при відкритті файлу ця функція повертає NULL.
Якщо до символів, які задають режим, дописана літера t
, то файл використовується як текстовий (цей режим діє за замовчуванням), якщо
ж літера b – то як
двійковий.
int fflash (FILE* fp); - виконує дозапис всіх даних, що ще
залишились в буфері.
Для закриття файлу (тобто звільнення буферів і
файлової змінної та знищення інформації про файл) - int fclose (FILE* fp); Ця процедура
повертає 0 в разі успішного завершення та EOF
(стандартна константа, визначена в stdio.h)
– в разі помилки.
Приклад
FILE *fp;
fp = fopen (“A:\\my_file”,
“w”); /* Зверніть увагу на знак \\ */
if (fp
!= NULL)
{
/*
Тут працюємо з файлом, після завершення
роботи з файлом – закриваємо його: */
fclose
(fp);
}
Деякі стандартні функції для
роботи з файлами.
1. int fgetc (FILE*fp); -
здійснює введення чергового символу із вказаного файлу. Якщо повертає EOF,
то сталась помилка або досягнуто кінця файлу.
2. int fputc (int ch, FILE*fp); - виводить символ з кодом ch у
вказаний файл. Повертає змінну з кодом ch або EOF,
якщо сталась помилка.
3. char * fgets (char *s, int n, FILE *fp); - ця функція здійснює введення в стрінг s
(буфер) символи із файлу, який ідентифікується файловою змінною fp,
до тих пір поки не виконається одна з умов: 1). виникає символ ‘\n’
(початок нового рядку); 2). досягнутий кінець файлу; 3). прочитано n-1
символів.
Після цього стрінг доповнюється ‘\0’ . В першому випадку перед
‘\0’ записується ‘\n’. Якщо читання з файлу завершено
успішно, то повертає вказівник на стрінг s, в разі
помилки повертає NULL.
4. int fputs (char *s, FILE*fp); - ця
функція виводить стрінг s у
вказаний файл. Повертає 0 або EOF, якщо сталась
помилка.
5. int fprintf (FILE *fp , char *format, …); - ця
функція здійснює форматований вивід своїх аргументів в файл, який
ідентифікується файловою змінною fp, під
керівництвом стрінгу format. Відносно цієї функції справедливо все
те, що було сказано про функцію стандартного форматованого виводу printf
(див. раніше).
6. int fscanf (FILE *fp , char *format, …); - здійснює форматоване, визначене стрінгом format,
введення з вказаного файлу. Відносно
цієї функції справедливо все те, що було сказано про функцію стандартного
форматованого виводу scanf (див. раніше).
7. int feof (FILE *fp); - повертає не 0 (тобто ІСТИНУ), якщо при
читанні з файлу був досягнутий кінець файлу і 0 (тобто ХИБНІСТЬ) у супротивному
випадку.
8. int ferror (FILE *fp); -
повертає не 0 (тобто ІСТИНУ), якщо при виконанні операцій введення або
виведення з файлом fp виникали помилки і 0 (тобто ХИБНІСТЬ) у
супротивному випадку.
9. void rewind (FILE *fp); - вказівник поточної позиції в файлі
пересувається на початок файлу. При цьому вказівник кінця файлу та вказівник помилок зануляються.
Файли прямого доступу.
Хоча
доступ до елементів файлу послідовний (тобто до кожного елементу файлу можна
дістатись, лише починаючи з початку файлу), є можливість використати стандартні
функції, які дозволяють організувати роботу з файлами так званого прямого
доступу.
1. int fgetpos (
FILE *fp, fpos_t *pos); - заносить в змінну pos , типом якої
є fpos_t (це еквівалент типу long int),
значення поточної позиції в файлі fp. Повертає 0, якщо позиція pos успішно
визначена.
2. int fsetpos (
FILE *fp, fpos_t *pos); - встановлює в файлі fp поточну позицію
в положення, визначене вказівником pos. Останній мав бути раніше визначений
функцією fgetpos. Повертає 0, якщо позиція pos успішно
встановлена.
3. int fseek (
FILE *fp, long offset, int from_where ); - встановлює в файлі fp поточну позицію
в положення, що відстоїть на offset байтів від
положення, визначеного аргументом from_where
в сторону кінця файлу при offset>0 і в
сторону початку при offset<0. Аргумент може набувати значення однієї з трьох
стандартних констант: SEEK_SET (=0) –
відлік від початку файлу; SEEK_CUR
(=1) – відлік від поточної позиції в файлі; SEEK_END
(=3) – відлік від кінця файлу. Повертає 0, якщо позиція успішно встановлена.
Макровизначення (макропідстановка
або просто макрос) в мові С(С++) - це вираз, який при компіляції файлу з кодом
програми підставляється замість імені, що визначає дану макропідстановку.
Макровизначення обробляються препроцесором - програмою, яка аналізує
програмний код і готує його для роботи компілятора. Команди, які має обробляти
препроцесор, записуються у вигляді директив. Директиви препроцесора починаються
із знака #: #define <ідентифікатор> <текст макросу>
Приклади: #define MAX_SIZE 10 int main () { int arr [MAX_SIZE]
; }
Зауваження
1.
Зверніть увагу - текст макросу просто "підставляється", тому
макровизначення виду:
#define
MAX_SIZE =10
або
#define MAX_SIZE
10;
є помилковими.
2. Допускається визначення макросів з
параметрами -звертання до них виглядає як звертання до функцій. Багато
стандартних функцій бібліотек C/C++ є
макровизначеннями.
3. Директива #undef
<ідентифікатор> зупиняє
дію ідентифікатора, який був
визначений раніше і дозволяє його повторне визначення в інших цілях.
4. Операція ## компілятором
сприймається як операція конкатенації.
Директива макровизначення з
параметрами:
#defіnе
<ідентифікатор>(<параметри>)
<текст макросу>
Дуже важливе
зауваження:
Між ідентифікатором макросу та
круглою дужкою, що відкриває список його параметрів, пробілу немає.
Приклад:
#define cube(x) х*х*х int main ()
{ int і = 1;
float x = 1.0;
cout « cube (і) « endl;// універсально для всіх
cout « cube (x) « endl;// типів параметрів!
}
#undef cube
Ще один приклад:
// Цей знак ## конкатенує аргументи
#define
concat(left, right) left ## right int main ()
{ визначаємо складений ідентифікатор ab
int concat (a,
b) = 100; // використовуємо цей ідентифікатор cout « concat (a, b) « endl;
Ще
один приклад:
#define max(x,
у) (х > у) ? х : у int main ()
{ int і = 1, j =2;
float x = 1.0, у = 2.0; int m = max (і, j) ; cout « m «
endl; float f = max(x, y) ; cout « f «
endl;
}
Проте - спробуємо проаналізувати,
як спрацюють дані макровизначення у наступних викликах:
int main ()
{ int і = 10, j = 20;
float x = 10,0, у = 20,0;
cout « cube (і + j) « endl; //???
cout « max (x,
y) « endl; //???
}
Останній рядок взагалі приведе до
синтаксичної помилки - низький пріоритет умовного виразу диктуватиме
першочергове виконання потокових операцій « :
(cout « (х
> у)) ? х
: (у « endl);
Внесемо очевидні виправлення у
тексти макровизначень:
#define cube(х) ((х)*(х)*(х))
#define max(x, у)
(((х) > (у))
? (х) :
(у))
Тепер їх використання не
приводить до проблем: int main ()
{ int і = 1, j = 2;
float x =
1.0, у = 2.0;
cout « cube (і + j) « endl; //!!!
cout « max (x,
y) « endl; //! ! !
}
Проте, чи правильно ви визначите
результати звертань: cout «
cube
(і++) « endl; cout « cube (++i) « endl;
Директиви компіляції
Ще одна група директив
препроцесора пов'язана з компіляцією програмних файлів. Для підключення так
званих header-файлів (файлів
заголовків), тобто файлів, які містять визначення констант та макросів,
декларації функцій, класів, шаблонів, тощо, використовуються директиви
#include
ідентифікатор header-файлу> та #include
"ідентифікатор header-файлу"
У першому випадку відповідний
файл має знаходитись у системних директоріях, а в другому - у поточній
директорії, яка і містить файл, що компілюється.
Інша група директив використовується для так званої
умовної компіляції – керування роботою препроцесора.
Це директиви #if, #elif, #else та #endif. Порядок
їх використання наступний :
#if
константний_вираз_1
#elif константний_вираз_2
#elif константний_вираз_3
#else #endif
Якщо цілий константний вираз у директиві #if
має ненульове значення (ІСТИНА), то при компіляції включаються всі наступні
рядки до #еіі£ або #endif або
#else (elif діє як гілка else-if)
Приклад. Змінюючи константу VERSION,
керуємо включенням файлів:
#define VERSION
З #if VERSION == 1
#define INCLUDE_FILE "file_l.h #elif VERSION == 2
#define INCLUDE_FILE "file_2.h #else
#define INCLUDE_FILE "file_3.h #endif #include
INCLUDE FILE
Для того, щоб позбавитись
повторних включень файлів заголовків, використовують наступні директиви:
#ifndef <ідентифікатор> #define
<ідентифікатор> // код, який включається у компіляцію #endif
Перша директива перевіряє, чи був
визначений <ідентифікатор> директивою #def ine
. Якщо ні, то наступна директива його визначає і при компіляції включається
текст аж до директиви #endif.
Інструкція
typedef
Інструкція
typedef дозволяє
визначати альтернативне
ім'я для існуючого типу. Приклад. typedef
unsigned short
WORD_; Тепер word_ є новою назвою для типу unsigned short.
З допомогою інструкції typedef
можна визначити і тип вказівника на функцію:
typedef double
(*p_function) (double);
#include
<iostream>
using namespace
std;
…… - тут містяться заголовки
файлів, що викликаються
Декларації функцій, що
використовуються в програмі.
Функція main().
Тіло функцій, що декларувалися!
У мові C
задані два вбудованих аргументу функції main: argc
і argv.
Виглядає це так: int
main (int argc, char * argv []) {...}
Аргумент argc типу integer містить у собі кількість
аргументів командного рядка.
Аргумент argv
типу char - покажчик на масив рядків. Кожен
елемент масиву вказує на аргументи командного рядка. Один параметр відокремлюється
від іншого пробілами.
argv [0] -
повне ім'я запущеної програми
argv[1] -
перший рядок записані після імені програми
argv[2] -
другий рядок записані після імені програми
argv [argc-1]
- останній рядок записані після імені програми
argv [argc] –
NULL
Приклад
int main(int argc, char
*argv[])
{
for
(int i = 0; i != argc; i++)
{
cout << argv[i] << endl;
}
system("PAUSE");
return EXIT_SUCCESS;
}
Розглянемо
наступний приклад. // Коментар може бути поміщений після двох
слешів
#include
<iostream>
using namespace
std;
// Виклик будь-якої програми
означає виклик функції Main()
int main()
{ //
Вивели заголовок
cout
<< "Перша програма"+"!!!";
//
Вивели запрошення
cout
<< "Введіть ціле число ";
int i; // Визначили цілу змінну
cin
>> i; // Прочитали цілу змінну
cout << i;
// Вивели її значення
system(“PAUSE”); //
return 0;
}
На що необхідно звернути увагу?
Перш за все, текст, поміщений між знаками /* */ є коментарем, тобто поясненнями до
коду програми, які ігноруються компілятором. Проте, такі пояснення конче
необхідні тим, хто працює з програмою і вважаються обов’язковими. Коментарем
також вважається і все, що знаходиться праворуч від знаків // – так зручно оформлювати коментарі до окремих рядків. І одразу
зауважимо, що мова C є регістро-залежною, отже великі та маленькі літери не слід плутати.
Підключаємо бібліотеку #include
<iostream>, де містяться ф-ї для вводу\виводу інформації. Далі, у рядку using namespace std; фіксується простір
імен std. Це спрощує звертання до потрібних нам методів
класів, зокрема до методів класу cout, необхідних при роботі консольних застосувань. Ф-я main() – основна ф-я.
ОСНОВИ
МОВИ С++
Концепція об’єктно-орієнтованого програмування базується на трьох
основних принципах. По-перше, об’єкт інкапсулює (приховує) у собі дані та
методи, які ними оперують, залишаючи назовні лише можливість послати або
одержати повідомлення – дозволену інформацію про себе. По-друге, існує
можливість створювати нові об’єкти з розширеним списком можливостей шляхом спадкування
від існуючих об’єктів. І нарешті, різні об’єкти можуть мати методи з однаковим
інтерфейсом, проте різним функціонуванням. Ця властивість реалізує підхід, що
одержав назву поліморфізму. Його ідея : один інтерфейс – багато методів.
Сучасні мови програмування, С++
зокрема, доросли таким чином до так званого «метапрограмування», або
програмування з допомогою шаблонів – можна створювати програми, які створюють
інші програми як результат своєї роботи.
Класи в мові C++
Характеризуючи тип даних, ми
визначаємо спосіб збереження відповідних змінних та об'єм пам'яті, необхідний
для них, але головним є набір допустимих операцій з даним типом. Для
стандартних типів даних ця інформація закладена в компіляторі. Створюючи власні
типи даних (а таку можливість надає мова C++), необхідно забезпечити реалізацію
всіх бажаних дій та операцій з типом. Класи мови C++ є реалізацією певної предметної
абстракції і зберігають не лише інформацію про об'єкт, а й визначають набір
допустимих дій з ним. Інформація про об'єкт - це дані-члени класу, а дії
реалізують функції-члени класу. Зазвичай дані-члени класу визначають закритими,
а методи, які ними оперують, відкритими. Хоча синтаксис дозволяє будь-які
можливості.
Вбудовані функції. Це невеликі за
обсягом коду функції, які позначаються службовим словом inline (і у
декларації, і у визначенні функції). Ідея їх використання полягає втому, що код
такої функції просто додається до програми ("вбудовується") в точці
виклику. Виграш при використанні inline-функції
на відміну від звичайної, полягає: у скорочення часу виконання програми.
Програш теж очевидний - зростає об'єм програми. При порівнянні із
макровизначеннями - виграш очевидний і безперечний - зникають всі проблеми з
параметрами, вони передаються звичайним чином - за значенням. Втім, слід
зазначити, що службове слово inline
- це лише прохання до компілятора, яке він може проігнорувати, якщо вважатиме,
що затрати на виклик функції менші за її включення в код. Безперечно, не буде вбудованою
рекурсивна функція.
Приклад.
inline double cube
(double x) {return
x*x*x;}
int main ()
{
double res, s = 10;
res = cube (++s);
// вірно: s = 11, res = 100
}
Параметри функцій за умовчанням. Мається
на увазі можливість задавати значення параметрам функцій, таким чином при
виклику відповідні аргументи, якщо вони пропущені, будуть замінені значеннями
за умовчанням.
Приклад. //
Функція ініціалізації текстового режиму на екрані
void InitScreen (int width = 80, int height =
24, char background = ‘ ‘)
{// код функції}
int main ()
{// Можливі виклики:
InitScreen (60) ;
InitScreen (60, 20);
InitScreen (60, 20, *#'); //
Неможливий виклик:
InitScreen (**'); }
Змінні-посилання. Зазвичай унарна
операція & означає взяття адреси об'єкта. Проте вона ж може бути
використана при визначенні так званих змінних-посилань.
Приклад.
int val = 10; //
Створили цілу змінну // Створили змінну-посилання на val
int & r_val
= val;
Тепер val та r_val
синоніми, проте різних типів. Вони мають одне й те саме значення і одну й ту
саму адресу в пам'яті. Зверніть увагу, проініціалізувати змінну-посилання можна
лише в момент визначення:
int val = 10;
int & r_val;
r_val = val;
//
помилка!
Використання змінних-посилань як
аргументів функцій. Як відомо,
класична мова С дозволяє передачу параметрів в функції лише за значенням. В
разі необхідності зміни значень параметрів функцій потрібні вказівники на них.
Тепер же змінні-посилання означають передачу by-reference
(за посиланням).
Приклад. // Функція переставляє свої параметри
void change (int &a, int &b)
{ int temp = a; a = b; b =
temp;
}
int main ()
{ int x = 10, у = 20;
change (x, y);
cout « x « y; // x і у реально
переставлені!
}
На відміну від передачі параметрів за значенням при
такому способі в стеку не створюється копія відповідного аргументу, а
передається змінна-посилання, яка є псевдонімом цього аргументу.
Змінні-посилання можуть використовуватись і як
результат функції.
Оператор
new
У C + + було
вирішено об'єднати всі дії, необхідні для створення об'єкта, в одну операцію,
яку виконує оператор new. При створенні об'єкту за допомогою
оператора new (так званого вираження new)
з купи виділяється блок пам'яті, розмір якого достатній для зберігання об'єкта,
а потім для цієї пам'яті викликається конструктор. Наприклад, розглянемо
наступну команду: MyType * fp - new MyТуре (1.2);
При її виконанні викликається якийсь еквівалент
функції malloc (sizeof (MyType)),
а потім викликається конструктор МуТуре, якому передається отриманий адресу у
вигляді покажчика this і список аргументів (1,2). До того
моменту, коли вказівник присвоюється fp, він
посилається вже на справжній ініціалізований об'єкт, причому до цього вам не
вдасться з ним нічого зробити. Об'єкт спочатку ставиться до правильного типу
МуТуре, тому приведення типу не потрібно.
Перед тим як передавати адресу конструктору, стандартна версія new
перевіряє, чи успішно була виділена пам'ять, тому вам не доведеться явно
організовувати таку перевірку в програмі. Пізніше в цьому розділі ви
дізнаєтеся, що відбувається при нестачі пам'яті.
Вирази new можуть
створюватися з використанням будь-якого конструктора, доступного для класу.
Якщо конструктор викликається без аргументів, то вираз new
записується без списку аргументів конструктора:
МуТуре * fp = new
МуТуре;
Зверніть увагу, яким простим став процес створення
об'єктів з купа - єдине вираження вирішує всі питання з визначенням розміру
об'єкта, перетворенням і перевірками, пов'язаними з безпекою. Створити об'єкт в
купі нітрохи не складніше, ніж у стеці.
Оператор
delete
У виразів new існують
парні вираження delete, в яких спочатку викликається
деструктор, а потім звільняється пам'ять (досить часто для цього використовується
функція free ()). За аналогією з тим, як вираз new
повертає покажчик на об'єкт, вираз delete повинна
отримувати адреса об'єкту: delete fp;
Наведена команда знищує динамічно створений об'єкт
МуТуре і звільняє займану їм пам'ять.
Оператор delete може
викликатися тільки для об'єктів, створених оператором new.
Якщо виділити пам'ять під об'єкт функцією malloc () (а
також calloc () або realloc ()), а
потім спробувати звільнити її викликом delete,
наслідки будуть непередбачувані. Так як більшість стандартних реалізацій
операторів new і delete
використовують функції malloc () і free (),
ймовірно, справа кінчиться звільненням пам'яті без виклику деструктора. Якщо
покажчик дорівнює нулю, при виклику delete нічого
не відбувається. З цієї причини багато фахівців рекомендують негайно обнулити
покажчик після виклику delete, щоб запобігти можливе повторне
видалення. Повторне видалення об'єкта безумовно не приведе ні до чого хорошого
і викличе проблеми.
Перевантаження функцій - простий поліморфізм.
Віднині функції можуть мати однакові ідентифікатори
при умові, що вони розрізняються сигнатурами.
Приклад.
void fun (int і) ;
int fun (int і); //
не перевантажується!
void fun (int і, int
j);
void fun
(double x) ;
void fun (int
*pi) ;
void fun (int &i); // не перевантажується!
void fun (char
*s) ;
void fun (const
char *s); // Це лише коли є вказівник!!!!
void fun (const
char s); // Не перевантажується!
Структура в мові С++ – це тип даних, який
складається з визначеної кількості елементів, що називаються членами структури
(інколи – полями структури). Члени структури можуть мати різні типи. Синтаксис
визначення структури:
struct tag_name
{
<тип_1> mem_1;
<тип_2> mem_2;
...
<тип_n> mem_n;
};
Тут mem_1,
mem_2 , ... , mem_n
– члени структури, визначеної з тегом (тег – дослівно означає наклейка) tag_name.
Тепер змінну, яка відноситься до структури такого типу, або, як ще кажуть екземпляр цієї структури, можна
визначити таким чином:
struct
tag_name <змінна-структура>;
Зауваження.
1. При
визначенні структури тег не є обов’язковим. Але тоді екземпляри структури,
визначеної анонімно – без тегу, повинні визначатись відразу після фігурної
дужки, що закриває тіло структури.
2. При визначенні екземпляру структури в мові С
вживання службового слова struct обов’язкове, а
в С++ – може бути пропущене.
3. Обмежень на типи членів структури немає, крім
одного – членом структури не може бути екземпляр даної структури, проте може
бути вказівник на неї.
Класи в мові C++
Характеризуючи тип даних, ми
визначаємо спосіб збереження відповідних змінних та об'єм пам'яті, необхідний
для них, але головним є набір допустимих операцій з даним типом. Для стандартних
типів даних ця інформація закладена в компіляторі. Створюючи власні типи даних
(а таку можливість надає мова C++), необхідно забезпечити реалізацію всіх
бажаних дій та операцій з типом. Класи мови C++ є реалізацією певної предметної
абстракції і зберігають не лише інформацію про об'єкт, а й визначають набір
допустимих дій з ним. Інформація про об'єкт - це дані-члени класу, а дії
реалізують функції-члени класу. Зазвичай дані-члени класу визначають закритими,
а методи, які ними оперують, відкритими. Хоча синтаксис дозволяє будь-які
можливості.
Синтаксис
визначення класу в C++.
class
<тег_класу>
{
// визначення даних-членів класу
private: // захищені члени класу - дані
<тип_1> <ідентифікатор_1>;
<тип_2> <ідентифікатор_2>;
…
// декларації функцій-членів класу
public:
//відкриті члени класу - функції
<тип_результату>
<ідентифікатор_1> (<параметри_функції>);
<тип_результату>
<ідентифікатор_2> (<параметри_функції>);
…
};
Що нам наразі
відомо про клас?
• Оголошення (декларація) класу схоже на
оголошення структури та може містити дані-члени та методи-члени класу.
• У класі є власний (закритий) блок, доступ до
елементів якого можливий лише для членів класу та загальний (відкритий) блок,
елементи якого доступні для всіх частин програми. Як правило, в закритому блоці
містяться дані класу, а відкритими є методи класу.
• Відкрита частина класу - це його
загальнодоступний інтерфейс, а закритий блок забезпечує інкапсуляцію
-приховування інформації та гарантує її цілісність.
• Комплект даних-членів існує окремо для
кожного екземпляру класу, методи - спільні для всіх екземплярів.
• Таким чином, клас реалізує наступні базові
принципи ООП - абстракцію та інкапсуляцію.
Яким чином можна створити та проініціалізувати
екземпляр класу?
Оскільки
доступ до даних-членів можливий лише для методів класу, очевидно, що
ініціалізацію даних при створенні екземпляру треба покласти на одну з
функцій-членів класу. Проте, треба бути впевненим, що така функція буде
викликатись кожного разу, коли створюватиметься черговий екземпляр даного
класу. Тому цей обов'язок покладається на компілятор, але йому треба вміти
розпізнавати потрібну функцію серед методів-членів класу. Це означає, що така
функція повинна мати зумовлене ім'я - ним є ім'я (тег) класу, а ця спеціальна
функція називається конструктором класу. Ще однією особливістю
конструктора (крім того, що його неможливо викликати безпосередньо - це
прерогатива компілятора) є відсутність результату, не вказується навіть
службове слово void.
// Конструктор класу має аргумент за умовчанням
Student (double av_mark_, double ex_mark_, char * name_ = "Noname" )
{
av_mark = av_mark_;
ex_mark = ex_mark_;
strncpy (name, name_, sizeof(name));
name [sizeof(name)-1] = '\0';
cout « "Create Student " « name « endl;
}
Можна створити динамічний екземпляр класу student з допомогою операцій new та delete.
Яким
чином можна знищити екземпляр класу?
Якщо екземпляр класу був
створений динамічно операцією new, то він має
бути знищений операцією delete. В інших випадках екземпляр існує, доки
не завершить роботу блок, де він був створений, і лише після цього екземпляр
знищується. В будь-якому разі, для знищення екземпляру викликається спеціальний
метод-член класу - деструктор. Якщо він не визначений в класі, то компілятор створить
деструктор за умовчанням. Те саме стосується і випадку, коли у класі не
створений конструктор. Деструктор має ім'я класу з префіксом - і не має ні типу
результату, ні параметрів.
В мові C++ існує проблема витоку
пам'яті, тому непотрібні екземпляри треба вчасно знищувати.
Визначення
методів-членів класу.
У попередніх прикладах
методи-члени класу визначались безпосередньо в класі. Такі функції за
умовчанням вважаються inline-функціями. Тому за винятком зовсім
невеликих за обсягом коду функцій, їх визначають, як правило, поза класом. В
такому випадку необхідно зазначити, що дана функція відноситься до певного
класу. Для цього використовується оператор області видимості : : . Якщо у попередньому прикладі
конструктор лише задекларувати в класі, а визначити поза класом, то його повне
ім'я буде наступним:
Student : : Student (double av_mark_, double ex mark ,
char * name ).
Вказівник this.
Ім'я this є службовим (ключовим) словом. Явно
описати чи визначити вказівник this не можна.
Відповідно до неявного визначення this є
константним вказівником, тобто змінювати його не можна, однак у кожної
приналежної класу функції він указує саме на той об'єкт, для якого функція
викликається. Говорять, що вказівник this є
додатковим (схованим) параметром кожної нестатичної компонентної функції.
Іншими словами, при вході в тіло приналежній класу функції вказівник this
ініціалізується значенням адреси того об'єкта, для якого викликана функція.
Об'єкт, що адресується вказівником this, стає
доступним усередині приналежної класу функції саме за допомогою вказівника this.
При роботі з компонентами класу усередині приналежної класу функції можна було
б скрізь використовувати цей вказівник. Наприклад, зовсім правильним буде таке
визначення класу:
struct ss
{ int si;
char sc;
ss(int in, char en) //
Конструктор об'єктів класу.
{ this->si = in;
this->sc = en; }
void print(void) // Функція
висновку відомостей про об'єкт.
{ cout << "\n si
= " << this->si;
cout << "\n sc =
" << this->se; }
};
При такому використанні
вказівника this немає ніяких переваг, тому що дані
конкретних об'єктів доступні в приналежних класу функціях і за допомогою імен
даних класу. Однак у деяких випадках вказівник this
корисний, а іноді просто незамінний.
Майже незамінним і дуже зручним
вказівник this стає в тих випадках, колив тілі
приналежної класу функції потрібно явно задати адресу того об'єкта, для якого
вона викликана. Наприклад, якщо в класі потрібна функція, що поміщає адресу
обраного об'єкта чи класу в масив яка включає конкретний об'єкт класу в список,
те таку функцію складно написати без застосування вказівника this. Дійсно, при
організації зв'язних списків, ланками яких повинні бути об'єкти класу,
необхідно включати в зв'язку ланок вказівник саме на той об'єкт, що у даний
момент обробляється. Це включення повинне виконати якась функція-компонент
класу. Однак конкретне ім'я об'єкта, що включається, у момент написання цієї
приналежної класу функції недоступно, тому що його набагато пізніше довільно
вибирає програміст, використовуючи клас як тип даних. Можна овільно вибирає
програміст, використовуючи клас як тип даних. Можна передавати такої функції
посилання чи вказівник на потрібний об'єкт, але набагато простіше
використовувати вказівник this. Отже, повторимо, коли вказівник this
використаний у функції, що належить класу, наприклад, з ім'ям ZOB,
те він має за замовчуванням тип ZOB *const
і завжди дорівнює адресі того об'єкта, для якого викликана компонентна функція.
Якщо в програмі для деякого класу X
визначити об'єкт: X factor(5); то при виклиці конструктора класу X,
що створює об'єкт factor, значенням
вказівника this буде
&factor.
Статичні
дані-члени (поля) класів використовуються для збереження даних,
спільних для всіх екземплярів класу, наприклад, кількість екземплярів класу,
вказівник на вершину динамічного списку, деяку глобальну для всього класу
константу, тощо. Статичний член класу має бути продекларованим у класі з
допомогою службового слова static, а процес
виділення під нього пам'яті та його ініціалізація відбувається поза
класом.Звертання до статичних членів можливе через ім'я класу або через
ідентифікатор екземпляру (В С# - тільки через ім'я класу). На статичні
члени розповсюджуються звичайні правила доступу.
Слід зауважити, що в класі
присутня лише декларація статичного члену, для його створення необхідно
виділити пам'ять під нього та в разі необхідності проініціалізувати - це
відбувається поза межами класу, навіть якщо статичний член задекларований як
закритий. Більше того, якщо статичний член класу (скалярного типу) помічений
службовим словом const, то він може бути проініціалізований в
класі, але пам'ять під нього все рівно має бути виділена поза класом!
Операція sizeof не
враховує пам'ять, виділену під статичні поля.
Приклад class Example
{ public :
static int num;
// декларація статичного члену класу
int key;
Example (int
key_) : key (key_) {} };
int Example :: num;//виділення пам'яті під статичний член, в разі відсутності ініціалізації він = 0
int main(int
argc, char *argv[])
{Example e (1),
f (10);
cout « Example :: num « endl; // звертання через ім'я класу
Example :: num =
100;
cout « e.num « endl; // звертання через ім'я екземпляру
cout « f.num. « endl; // звертання через ім'я екземпляру
e.num = 1000;
cout « e.num « endl;
cout « f.num « endl;}
Статичні функції-члени класів використовуються тільки для звертання до статичних даних-членів і не можуть використовувати звичайні дані та методи класу, адже вони не прив'язані до екземпляру, їм не передається вказівник this. Службове слово static вказується лише у декларації статичної функції, при її визначенні воно не повторюється. Звертання до статичних методів так само може відбуватись і через ім'я класу, і через ідентифікатор екземпляру. Слід зауважити,
що звичайні функції-члени класу мають право працювати із статичними членами
класу. Конструктор та деструктор не
можуть бути статичними!
Конструктори і деструктори
Необхідність
у ініціаллізації ще більш часто проявляється при роботі з об'єктами. Дійсно,
якщо звернутися до реальних проблем, то, фактично, для кожного створюваного
вами об'єкта потрібно якогось виду ініціалізація. Для вирішення цієї проблеми в
C + + є функція-конструктор (Constructor function), яка включається в опис класу. Конструктор класу
визується щоразу під час створення об'єкта цього класу. Таким чином, будь-яка
необхідна об'єкту ініціалізація за наявності конструктора виконується
автоматично. Конструктор має те ж ім'я, що й клас, частиною якого він є, і не
має що повертається значення. Наприклад, нижче представлений невеликий клас з
конструктором. Для глобальних об'єктів конструктор об'єкта викликається тоді,
коли починають нается виконання програми. Для локальних об'єктів конструктор
викликаючи- ється всякий раз при виконанні інструкції оголошення змінної.
Функцією, зворотного конструктору, є деструктор (destructor). Ця
функція викликається при видаленні об'єкта. Зазвичай при роботі з об'єктом в
момент його видалення повинні виконуватися деякі дії. Наприклад заходів, під
час створення об'єкта для нього виділяється пам'ять, яку необхідно звільнити
при його видаленні. Ім'я деструктора збігається з ім'ям класу, але з символом ~
(тильда) на початку. Деструктор класу викликається при видаленні об'єкта.
Локальні об'єкти видаляються тоді, коли вони виходять з області видимості.
Глобальні об'єкекти видаляються при завершенні програми. Адреси конструктора і
деструктора отримати неможливо.
Конструктори з параметрами
Конструктору
можна передавати аргументи. Для цього просто додайте необхідні параметри в
оголошення і визначення конструктора. Потім за оголошенні об'єкта задайте
параметри в якості аргументів. Щоб поняти, як це робиться, почнемо з короткого
прикладу:
Конструктор за
замовчуванням.
Так називають конструктор, який
дозволяє створювати екземпляри класів з неявною ініціалізацією даних. Як вже
зазначалось, такий конструктор автоматично створюється для класів, в яких не
визначений власний конструктор. Проте, як тільки в класі визначається хоч один
конструктор, такий автоматичний конструктор перестає діяти. В разі необхідності
створення екземплярів без ініціалізації, варто визначити в класі конструктор за
умовчанням. Найпростіший спосіб зробити це - перевантажити конструктор, або
визначити умовчання для всіх його параметрів.
Конструктор
копіювання.
Справа полягає в тому, що
параметр типу Student передається у функцію f
() за значенням, а отже, створюється, а потім знищується зі стеку. Тому і
виникає зайвий виклик деструктора. Але конструктор при створенні локального
екземпляру класу у стеку не викликався! Дійсно створенням екземплярів, які
необхідно ініціалізувати значенням вже існуючого екземпляру займається інший
конструктор -так званий конструктор копіювання. В даному прикладі був
викликаний такий конструктор, створений компілятором. Він просто поелементно
копіює даний екземпляр. Але це не завжди доречно, адже, можливо, необхідне
виділення пам'яті для даних членів, тощо. Для явного визначення конструктора
копіювання необхідно дотримуватись особливого синтаксису: <ім'я_класу> (const
<ім'я_класу> & );
Параметром конструктора
копіювання є стала змінна-посилання на екземпляр класу. Його призначення -
коректне створення копії екземпляру. Особливо важливо це у випадку, коли
членами класу є вказівники, пам'ять під які виділяється оператором new.
Адже тоді по-елементне копіювання копіює вказівник (поверхневе копіювання), а
не об'єкт, на який він посилається, - для цього необхідне глибоке копіювання.
Спадкування, похідні класи.
Спадкування
- це один з основних принципів об'єктно-орієнтованого програмування, який дозволяє
створювати об'єкти, що спадкують свої властивості від існуючих об'єктів,
додаючи власної функціональності. Похідний та базовий клас пов'язані СПІВВІДНОШЕННЯМ
«Є»: ЯКЩО SubBase - ПОХІДНИЙ клас
ВІД класу Base, TO SubBase Є Base. Синтаксис визначення похідного класу:
class SubBase : <вид_спадкування> Base
{
// тіло похідного класу
};
Деякі правила
спадкування.
1. Похідний клас спадкує (містить) всі відкриті
(public) та захищені (protected) члени базового класу. Закриті (private)
члени базового класу недоступні у похідному.
2. Конструктори
та деструктор не спадкуються похідним класом.
3. При створенні екземпляру похідного класу
автоматично викликається конструктор базового класу і лише потім конструктор
похідного. При знищенні екземпляру деструктори викликаються у зворотному
порядку.
4. Якщо у похідному класі відсутній
безпосередній виклик конструктора базового класу, то викликається конструктор
за умовчанням (без параметрів) базового класу. В разі його відсутності виникає
помилка.
5. Не спадкується також операція присвоєння,
якщо вона була перевантажена у базовому класі.
Приклад.
class Base
{protected : // закриті члени класу
double money; int key; public : // відкриті члени класу
Base (double money_ = 1000, int key_ = 1) :
money (money_) , key (key_) {cout « "Create
Base" « endl;} double show_money
() {return money;} int show_key () {return
key;}
};
class SubBase :
public Base
{public :
// Тут викликається конструктор базового класу
SubBase (double money_, int key_)
{
cout « "Create SubBase" « endl;
money = money_; key = key_; }};
Виклик конструктора базового класу.
class Base
{protected : // закриті члени класу
double money; int key;
public : // відкриті члени класу
Base (double money_ = 1000, int key_ = 1): money
(money_), key (key_)
{ }
double show_money
() {return money;}
int show_key () {return key;}
};
class SubBase :
public Base
{
public :
// Тут викликається конструктор базового класу SubBase (double money_, int key_): Base(money_, key_) { }
};
Доступ до членів
класу при різних видах спадкування
Види спадкування та доступ до членів базового класу у похідному
класі зазначений у наступній таблиці:
Вид спадкування |
Доступ у базовому класі |
Доступ у похідному класі |
public |
private protected public |
не доступний protected
public |
protected |
private protected public |
не доступний protected
protected |
private
(ДІЄ за умовчанням) |
private protected public |
не доступний private
private |
Зміна доступу до
членів базового класу
у похідному
класі.
Оголошення доступу у похідних
класах дають можливість:
- зробити знову відкритими або захищеними
відповідно захищені або відкриті члени базового класу у похідному класі;
- зробити знову відкритими відкриті члени
базового класу у закритому або захищеному похідному класі;
Поліморфізм
та віртуальні функції.
Похідні класи мають з базовим
класом зв'язки двох видів.
Перший з них полягає в тому, що
екземпляри похідних класів використовують всі відкриті члени базового класу –
зокрема методи базового класу.
Другий
вид зв'язку полягає в тому, що:
•
екземпляр базового класу можна створити як екземпляр похідного;
•
посилання на базовий клас може посилатись на
похідний;
•
вказівник на базовий клас може вказувати на
похідний.
Всі ці операції виконуються без
явного приведення типів і є реалізацією відношення «is-a».
SubBase sb;
Base bb = SubBase
();// екземпляр базового класу створюється як похідний
b = sb; // екземпляру базового
класу присвоюється похідний
Base & bbb = sb; // посилання на базовий
клас посилається на похідний
Base *p = &sb; // вказівник на базовий клас вказує на
похідний
// sb = b; // таке присвоєння неможливе!
Цілком зрозуміла заборона присвоєнь
у зворотному напрямку – адже якщо екземпляр похідного класу створюється як
базовий, то виникає проблема із викликом методів похідного класу, яких немає у
базовому.
Та обставина, що посилання та
вказівники базового класу можуть вказувати на екземпляри похідних класів,
приводить до низки цікавих можливостей – зокрема методи, які мають параметрами
посилання або вказівник на базовий клас, можуть викликатись із
аргументами-екземплярами похідних класів:
void
fun (Base & b) { b.meth();}
fun
(b); // так можливо | fun (sb); // і так теж можливо
Проте, в будь-якому разі, функція fun()
викликатиме метод базового класу.
Проте, можлива ситуація, коли
успадковані методи похідних класів повинні поводити себе інакше, ніж методи
базового класу. Така поведінка називається “поліморфною”. (Поліморфний – такий,
що має багато форм).
Реалізація
поліморфного спадкування здійснюється одним із двох способів.
1. Перевизначення методів базового класу у похідному класі
(заміщення методів) :
class
Base
{
public:
void meth ()
{ cout << "In Base: meth() "
<< endl; }
};
class
SubBase : public Base
{//
цей метод перекриває відповідний метод базового класу
void meth ()
{ cout << "In SubBase: meth()
" << endl;}};
int
main (void) {
Base
b; // екземпляр базового класу
SubBase sb; // екземпляр похідного класу
b.meth (); // виклик методу базового класу
sb.meth
(); // виклик методу похідного
класу
return 0;
}
В
усіх попередніх прикладах зв'язування екземпляру із конкретним методом
(функцією-членом класу) відбувалось на етапі компіляції (тобто ще до початку її
виконання). Ця процедура, як відомо, називається раннім зв'язуванням.
Альтернативний спосіб – пізнє зв'язування (інколи –
динамічне зв'язування, в С# - динамічний поліморфізм) дозволяє асоціювати
об'єкт із методом вже під час виконання програми.
2.
Використання віртуальних методів
.
Пізнє
зв'язування охоплює ряд функцій-членів (методів), які називаються віртуальними
функціями. Віртуальна функція (virtual)
оголошується в базовому класі і перевизначається у похідних класах. Сукупність
класів, в яких визначається і перевизначається віртуальна функція, називається поліморфним
кластером. У межах цього кластеру об'єкт пов'язується із конкретною
віртуальною функцією-членом під час виконання програми. Звичайна
функція-член також може бути перевизначена у похідному класі, як у попередньому
прикладі. Проте без атрибуту virtual
до неї буде застосоване лише раннє зв'язування.
class
Base
{
public:
virtual void virt () // віртуальний метод
{
cout << "In class Base"
<< endl;
}
};
class
SubBase : public Base
{public
:
//віртуальний метод
заміщається у похідному класі. Cлово virtual у
похідному класі – не обов’язкове
virtual void virt ()
{ cout << "In class SubBase"
<< endl;} };
Тепер, якщо визначити зовнішню функцію fun (Base
& b), як у попередньому прикладі, то ми побачимо реалізацію пізнього зв'язування.
Рішення
про те, який саме метод virt() базового
чи похідного класу має бути викликаний, приймається під час виконання
програми – це пізнє зв’язування. Віртуальні методи можуть
перевантажуватись, як звичайні функції.
Слід зазначити, що використання пізнього
зв'язування достатньо складний механізм, який вимагає суттєвих витрат пам'яті. Тому віртуальними слід робити лише
такі функції, які будуть перевизначатись у похідних класах.
Зауваження.
Конструктори не можуть бути
віртуальними – адже похідний клас не спадкує конструктор базового. А от деструктор може бути віртуальним.
Користь віртуального деструктора показує наступний приклад, висновком з якого
може бути правило: якщо клас не вимагає явного виконання деструктора, краще
визначити віртуальний деструктор, навіть якщо йому не має чого робити.
Повернемось
ще раз до перевизначення функцій. Якщо в похідному класі визначається метод,
одноіменний з віртуальним методом базового класу, але з відмінною сигнатурою,
він перекриває всі віртуальні методи базового класу. Це означає, що в
похідному класі вони не доступні.
Абстрактний
базовий клас (ABC
– Abstract Base Class).
Наразі
нам відомі правила простого спадкування та більш складного поліморфного
спадкування, яке включає використання віртуальних функцій. Наступний рівень
складності – абстрактний базовий клас. Необхідність в ньому виникає, коли
необхідно описати об'єкти, що мають складну природу, проте їх важко визначити
як базовий та похідний класи. Наприклад, розглядаючи такі об'єкти, як
прямокутник та ромб, неможливо встановити між ними відношення «Є» (“is-a”), хоча й очевидно, що
вони мають багато спільного: наприклад, поняття площі, повороту на площині. У
таких випадках необхідно виділити у об'єктів все спільне і створити клас, який
буде базовим для них всіх. Якщо реалізація окремих функцій можлива лише на
рівні похідних класів, у базовому їх визначають як чисто віртуальні
функції. Екземпляри такого базового класу неможливо створити, сам клас
називається абстрактним і використовується лише для створення
похідних класів.
class Figure
// абстрактний клас
{ protected :
double x_cntr, y_cntr; // координати центру фігури
public:
Figure
(double x = 0, double y = 0) : x_cntr (x), y_cntr (y) {}
// чисто віртуальна функція
virtual double Square () const = 0;
};
class Rhombus : public Figure // похідний клас ромб
{ private :
double
len, angle;
public
:
Rhombus (double l = 0,double a = 0,double x = 0,double y = 0);
double
Square () const { return
len*len*sin(angle); }
};
Клас
– це тип даних, визначений програмістом, а об'єкт – це змінна, екземпляр даного
типу. Мова С++ надає можливості зробити типи, що створюються, максимально
наближеними за можливостями до стандартних типів. Зокрема можливо перевизначити
(перевантажити) звичні знаки операцій таким чином, щоб вони стосувались
екземплярів класів. Наприклад, нехай визначений клас complex, що реалізує комплексне
число. Звісно, можна реалізувати дії з екземплярами цього класу як звертання до
певних функцій класу, проте значно зручніше було б оперувати звичними для
сприйняття виразами на кшталт: complex
a, b; complex
c = a + b; c = a*b;
1.
Дружні функції.
Як відомо, доступ до закритих (і захищених)
членів класу неможливий із зовнішнього
коду. Проте виняток становлять так звані “дружні” функції.
Достатньо помістити в класі декларацію зовнішньої функції із службовим словом friend, і вона матиме доступ
до всіх без винятку членів класу.
friend void excellent (Student &s);
// дружня функція
void excellent (Student
&s) // Ця функція робить відмінником будь-якого студента
{ s.av_mark = 60; s.ex_mark = 40; }
Зауваження про дружні функції.
1. Слід пам'ятати, що функція із модифікатором friend не є членом класу, хоча її декларація і фігурує в класі.
2. Дружня функція має такі самі
права доступу до всіх членів класу як і функція-член класу.
2.
Вказівник this.
Кожна функція-член класу (в тому числі конструктори та деструктор) мають
вказівник this.
Характерною особливістю його є те, що він вказує на екземпляр, для якого
здійснюється виклик.
Правила
перевантаження операцій в класі
Для перевантаження операцій використовуються
так звані операторні функції – функції із ідентифікатором operator@, де замість знаку @ стоїть знак операції, що перевантажується.
При визначенні операторної функції дотримуються спеціальних синтаксичних вимог. По-перше, операторна функція може бути
визначена як функція-член класу або як зовнішня дружня
функція. По-друге, по-різному
визначаються унарні та бінарні операції.
Бінарні операції
Декларація
функції-члену класу для
перевантаження бінарної операції:
<тип_результату>
operator@ (<
тип_операнду_2>);
При
цьому перший операнд операції передається неявно у вигляді вказівника this, тобто обов’язково має
тип классу. Таким чином, для реалізації операції виду a@b, де a та
b –
екземпляри відповідного класу, відбувається виклик операторної функції: a.operator@ (b)
Декларація
дружньої функції для
перевантаження бінарної операції:
friend
<тип_результату> operator@ (<тип_операнду_1>, <
тип_операнду_2>);
При
цьому для реалізації операції виду a@b відбувається виклик
операторної функції: operator@
(a,b)
Унарні операції
Декларація функції-члену
класу для перевантаження унарної операції: <тип_результату>
operator@ ();
При цьому
операнд операції передається неявно у вигляді вказівника this,
тобто обов’язково має тип классу. Для реалізації операції виду @a
відбувається виклик операторної функції: a.operator@ ()
Декларація дружньої
функції для перевантаження унарної операції:
friend <тип_результату> operator@ (<тип_операнду>);
Для реалізації
операції виду @a відбувається виклик операторної
функції: operator@ (a)
Особливості
використання операції присвоєння операцій.
1. Слід знати,
що за умовчанням операція = для двох екземплярів класу здійснює
поелементне копіювання даних-членів цих екземплярів. Якщо це саме те, що
потрібно для вашого класу, немає необхідності у перевантаженні операції =
.
2. Якщо членом
класу є вказівник, то за умовчанням відбуватиметься копіювання відповідних
вказівників, а не об'єктів, на які вони посилаються (чого, скоріше за все, ви
очікуєте при присвоєнні). В такому разі необхідно коректно перевантажити
операцію = .
3. Використання
параметром такої операторної функції посилання на об'єкт позбавить від
створення та знищення у стеку його копії і зекономить ресурси.
Ще
один нюанс визначення унарних операцій
пов’язаний із операціямии інкременту
та декременту. Як відомо, і інкремент,
і декремент можуть бути як префіксними, так і постфіксними (в мові С# вони не
розрізнялись). Мова С++ надає можливість реалізувати префіксні та постфіксні
операії по-різному. Для цього в операторній функції, що реалізує постфіксні
операції, використовується фіктивний параметр. Його наявність
дозволяє перевантажити відповідним чином операції:
//
префіксний інкремент: <тип_результату> operator++ ();
//
постфіксний інкремент: <тип_результату> operator++ (int unused);
Або, якщо
операції визначаються з допомогою дружніх функцій:
//
префіксний інкремент: friend
<тип_результату> operator++
(<тип_операнду>);
//
постфіксний інкремент: friend
<тип_результату> operator++ (<тип_операнду>, int
unused);
Абсолютно
аналогічні правила діють і для операції декременту.
Зауваження
про обмеження при перевантаженні операцій.
1. Перевантажена
операція класу повинна мати принаймні один операнд з типом даного класу – таким
чином забезпечується цілісність операцій зі стандартними типами даних: ви не
можете перевантажити операцію + для цілих змінних так, щоб вона реалізовувала,
наприклад, віднімання.
2. Перевантажена
операція не може змінювати синтаксис існуючих операцій, наприклад операція %
не може бути унарною. Так само неможливо змінити пріоритети існуючих операцій.
3. Неможливо
створювати нові символи для операцій, наприклад, x**y
неможливо реалізувати як піднесення x до
степеня y.
4. Не
перевантажуються наступні операції:
sizeof . (доступ до елементу) .*
(операція вказівник на елемент) ::
(оператор області видимості) ?:
(тернарна операція) та деякі інші, пов’язані з приведенням та перетворенням
типів.
Зауваження
про використання дружніх функцій при перевантаженні операцій.
1. В багатьох
випадках не має різниці, яку саме форму операторної функції (функція-член класу
чи дружня функція) ви використовуєте.
2. Якщо ж ви
намагаєтесь перевантажити деяку операцію, яка буде використовуватись у виразах,
де перший операнд не є екземпляром даного класу (скажімо, при множенні
скаляра на екземпляр класу, що реалізує вектор або матрицю), то операторна
функція обов'язково має бути реалізована
як дружня.
3. Більшість
операцій можуть бути перевантажені як функції-члени класу, так і з допомогою
дружніх функцій. Проте наступні операції можуть перевантажуватись лише з
допомогою функцій-членів класу: = (присвоєння) () (оператор
виклику функції) [] (оператор індексації) -> (доступ до членів
класу через вказівник).
Шаблони
(template - English) - це
засіб мови C++, призначений для створення кодів узагальнених алгоритмів або
класів, не прив'язаних до конкретного типу параметрів.
Навіщо
потрібні шаблони?
C++
дозволяє створювати об'єкти (зокрема змінні, функції) лише із визначенням їх
конкретного типу. Але в багатьох випадках алгоритми обробки таких об'єктів не
залежать від їх типу, наприклад, алгоритм сортування або обробка динамічних
структур, подібних до бінарного дерева, стеку чи зв'язаного списку. Програміст
в таких випадках може обрати один із наступних шляхів:
Зреалізувати
один і той самий алгоритм для кожного потрібного типу даних;
-використати
абстрактний базовий клас, в якому визначити даний алгоритм і заміщати його
відповідним чином у похідних класах;
-використовувати
засоби препроцесора і створити макровизначення.
Спробуйте
оцінити вади кожного із цих шляхів.
Шаблони
забезпечують
розв'язання цієї проблеми, і оскільки вони є засобами мови, для них
забезпечується належна підтримка контролю типів. Шаблон являє собою функцію або
клас, що реалізуються для одного чи навіть кількох абстрактних типів
даних, які невідомі на момент компіляції цього коду. В момент виклику у шаблон
передаються конкретні типи даних, для яких генерується функція чи клас із
відповідними типами даних.
З іншого боку не
варто забувати, що в результаті ми приходимо до певної філософської небезпеки,
адже таким чином породжуються програми, які створюють інші програми - ми маємо
справу із так званим метапрограмуванням.
1.
Шаблони функцій.
Шаблон
функції (її
інколи називають ще родовою функцією)- це узагальнене визначення
цілого сімейства функцій, які можуть бути викликані для даних різних типів.
Визначення шаблону починається із службового слова template,
після якого у кутових дужках міститься список параметрів шаблону - обов'язково
непорожній.
При визначенні
параметрів шаблону використовується службове слово class,
яке втім не має жодного відношення до поняття класу в мові C++, або службове
слово typename, затверджене лише нещодавно у стандарті
мови. Після нього вказується ідентифікатор параметру шаблону.
Розглянемо для
прикладу функцію, яка повертає максимум з двох своїх параметрів. (Туре - параметр шаблону)
template
<typename Type>
Type
maxi (Type a, Type b)
{ return (a < b)? b : а; }
Що відбувається
при виклику функції, визначеної шаблоном?
Коли
компілятор знаходить звертання до функції maxi, тип
даних, що передається у maxi при виклику, підставляється замість
імені Туре у всьому коді визначення шаблону, і компілятор створює завершену повноцінну
функцію, яка компілюється і викликається. Цей процес називається конкретизацією
(instantiation) шаблону. Таким чином, шаблони
дійсно відіграють роль генераторів програмних кодів.
//В цьому прикладі шаблон має кілька параметрів. Туре1 та Туре2 - параметри шаблону функції.Службове слово class (або typename) перед кожним параметром!
template <class Typel, class Type2>
Typel maxi (Typel a, Type2 b)
{return ((Typel)a < b)? (Typel) b : a;}
Крім параметрів-типів шаблони можуть
містити і звичайні параметри, як наприклад, у наступному прикладі:
// шаблон функції для пошуку мінімуму в масиві містить і звичайний параметр -змінну size для визначення кількості елементів в масиві
template <class Type, int size>
Type min_( Type (&r_array) [size] )
{ Type min_val = r_array[0] ; for ( int і = 1; і < size; i++ ) if
( r_array[i] < min_val )min_val = r_array[i];return min_val;}
В такому випадку конкретизація шаблону
буде відбуватись не тільки в залежності від типу елементів масиву, але й в
залежності від кількості цих елементів.
template <class Type, int size>
Type min_( Type (&r_array) [size]
);
Слід зазначити, що родові функції (шаблони ) можуть
перевантажуватись. Причому може існувати кілька шаблонів функцій з різними
наборами параметрів, а може також бути створена звичайна функція з іменем
шаблона.
В останньому випадку перевантажена функція може
перекривати («затіняти») шаблонну функцію, яку компілятор створив би для даного
конкретного виклику. Крім того, перевантаження шаблонів може привести до
складних і неоднозначних виборів функцій-кандидатів. Тому краще не
використовувати можливість перевантаження шаблонів.
2. Шаблони
класів.
Шаблон класу (або родовий
клас) – як і у випадку родової функції він містить всі необхідні
алгоритми обробки даних, а конкретні типи даних підставляються в момент
створення екземпляру даного класу. Таким чином, шаблон класу породжує ціле
сімейство класів із спільною логікою функціонування для різних типів даних.
Синтаксис визначення шаблону класу наступний:
template <class Type> class class_id { //
визначення класу };
Функції-члени
класу автоматично стають шаблонами функцій, хоча для них і нє вказується явно
службове слово template. У випадку, коли така функція лише
декларується у класі, а визначається поза ним, використовується достатньо
складний синтаксис:
template <class Type>
тип_результату_функції class_id <Туре> :: func_id (параметри_функції)
{ // тіло функції }
При створенні екземпляру шаблону класу
конкретні значення аргументів шаблону вказуються в кутових дужках: class_id <int> c_i; class_id <double> c_d;
Для даного прикладу буде згенеровано
екземпляр с_і класу class_id, в якому в ролі типу Туре
виступатиме тип int, та екземпляр c_dToro
самого класу class_id, але із типом double замість
типу Туре. Якщо шаблон має не один параметр, а більше, значення аргументів
шаблону при створенні екземпляру мають однозначно відповідати списку параметрів
шаблону. Замість службового слова class, що використовується у списку параметрів шаблону, так само можна
використовувати його синонім typename.
Деякі
правила визначення шаблонів:
^Шаблони
функцій не можуть бути віртуальними.
^Шаблони
класів можуть містити статичні елементи, дружні функції та класи.
^Шаблони
класів можуть бути похідними як від звичайних класів, так і від шаблонів, а
також бути базовими класами і для шаблонів, і для звичайних класів.
В
розпорядженні користувачів потужна бібліотека стандартних шаблонів (STL
-Standard Template Library),
яка містить багато шаблонів класів.
Огляд введення
та виведення в С++
Заголовочний файл iostream містить набір класів, що забезпечують
введення та виведення інформації в стилі мови С++. З точки зору програми С++
введення та виведення являє собою потік байтів. При введенні програма читає
байти з потоку вводу, при виведенні – вставляє байти в потік. Якщо програма
орієнтована на роботу з текстом (або навіть з числовою інформацією в текстовому
вигляді), кожний байт є представленням чергового символу. В загальному випадку
байти потоку є двійковим представленням даних довільного типу.
Байти потоку введення можуть поступати з клавіатури
або з зовнішнього пристрою пам'яті. Так само байти потоку виведення можуть бути
відправлені на екран, принтер, зовнішній пристрій. Тобто потік діє як посередник
між відправною точкою та точкою призначення потоку. Це дозволяє С++-програмі
обробляти потоки незалежно від того, звідки вони поступили або куди
направляються. Образно потік можна уявляти як трубу, по якій спрямовуються
байти інформації. Отже, для роботи потоком його треба приєднати до програми та
зв'язати з файлом, що містить інформацію. Більш ефективно введення та виведення
інформації можна здійснювати з допомогою буфера, який відіграє роль резервуара
для накопичення інформації щоб зменшити кількість звертань до зовнішніх
пристроїв пам'яті.
Ієрархія класів потоків мови С++.
Клас ifstream , який реалізує операції введення із файлу.
Один
із його конструкторів цього класу має вигляд:
ifstream :: ifstream (char*
pFileName,
int
mode = ios::in,
int
prot = filebuff::openprot);
Перший аргумент – ім’я файлу, що відкривається для введення. Другий
та третій задають режими відкриття файлу. Їх зміст представлений у таблицях
нижче.
Клас ofstream , який реалізує операції виведення у файл.
Один із його конструкторів цього класу має вигляд:
ofstream :: ofstream (char*
pFileName,
int
mode = ios::out,
int
prot = filebuff::openprot);
Перший аргумент – ім’я файлу, що відкривається для виводу. Другий
та третій задають режими відкриття файлу. Їх зміст представлений у таблицях
нижче.
Значення параметру mode
Прапорець |
Призначення |
ios::ate |
Перемістити вказівник
у кінець файлу після його відкриття |
ios::app |
Якщо файл існує –
дозапис у нього |
ios::in |
Відкрити файл для
введення (замовчування для istream)
|
ios::out |
Відкрити файл для
виведення (замовчування для ostream)
|
ios::nocreate |
Якщо файл не існує –
повернути помилку |
ios::noreplace |
Якщо файл існує –
повернути помилку |
ios::binary |
Відкрити файл як
двійковий (бінарний) (за замовчуванням як текстовий ) |
Значення параметру prot (залежить
від ОС)
Прапорець |
Призначення |
filebuff::openprot |
Режим спільного
читання та запису (за замовчуванням) |
filebuff::sh_none |
Режим монопольного
читання та запису |
filebuff::sh_read |
Режим спільного
читання |
filebuff::sh_write |
Режим спільного
запису |
Режими можуть
бути комбінованими з допомогою операції побітового АБО, наприклад, відкриття
двійкового файлу bin_file для до
запису в кінець: ofstream bin
(“bin_file”, ios::binary | ios::ate);
Функція-член bad()
повертає 1, якщо при створенні екземпляру виникла помилка:
if (bin.bad()) {cerr << “Opening file error”;}
Функція-член clear() скидає прапорець помилки (інакше спроби виведення блокуються):
bin.clear();
Бібліотека
стандартних шаблонів мови C++.
Ядро
бібліотеки стандартних шаблонів складають 3 основні елементи: контейнери, алгоритми, ітератори.Контейнер - об'єкт бібліотеки
стандартних шаблонів, призначений для збереження даних. Деякі контейнерні класи
наведені у таблиці нижче.
Контейнер |
Опис |
Заголовок |
bitset |
множина
бітів |
<bitset> |
queue |
черга |
<queue> |
list |
лінійний
список |
<list> |
set |
множина,
в якій кожний
елемент унікальний |
<set> |
stack |
стек |
<stack> |
vector |
вектор |
<vector> |
map |
асоційований
список для
збереження
пар ключ-значення |
<map> |
vector <int> v (n); // Вектор із n
цілих елементів
v.push_back (-1); // вставляє
в кінець елмент із заданим значенням
v.pop_back (); //
Видаляє елемент із кінця
v.insert (v.begin (), 5,
100); // Вставляє 5 копій елементу 100 перед
вказаним
v.insert (v.end(), 5, 100); // Вставляє 5 копій елементу 100 перед вказаним
vector <int> :: iterator p = v.begin (); // визначаємо ітератор p
v.erase (p-5, v.end()); // Видаляє
елементи між вказаними
Створення проекту.
Вдосконалимо попередній приклад,
розмістивши фрагменти коду в різних файлах згідно
прийнятому стилю ООП. Визначення класу помістимо у
файл з розширенням .ь (наприклад,
student.h). При
цьому використаємо команду препроцесора #if ndef,
щоб позбутись повторного включення коду. Визначення функцій класу помістимо у
файл з таким же іменем і розширенням . срр (student.срр) і
нарешті
код, який використовує даний клас, тобто клієнтський
файл - у файл use student. срр.
Алгоритм
створення проекту в середовищі DevCpp.
1. Пункт меню
Файл -> Створити -> Проект (створюється файл main.cpp-зберігти
його під потрібним іменем, наприклад, use_student.cpp і помістити в нього код використання
класу).
2. Файл
-> Створити -> Вихідний файл (на питання "Додати в проект?"
дати згоду). Зберігти файл під потрібним іменем (наприклад, student.h)
і помістити в нього визначення класу. На початку файлу включити директиви #ifndef
stud_h та #define stud_h.
В кінці файлу включити директиву #endif
3. Файл ->
Створити -> Вихідний файл (на питання "Додати в проект?" дати
згоду). Зберігти файл під потрібним іменем (наприклад, student,
cpp) і помістити в нього визначення функцій
класу. На початку файлу не забути про директиву #include пstudent.h".
Така сама директива має бути і в файлі use_student.срр.
4.
Створити окрему папку, в яку помістити всі файли проекту.