Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Лекція 1
Тема: Вступ, Історія мови програмування, мови програмування на базі стандарту мови С. Абетка мови. Структура програми. Базові бібліотеки. Стандартні типи.
Мета: Вивчення абетки мови програмування, ознайомлення зі стандартними типами даних та стуктурою програми
Питання:
Мова С була винайдена і реалізована Деннісом Рітчі ( Dennis Ritchie ) для комп'ютера DEC PDP -11 в операційній системі Unix. Ця мова була розроблена на основі мови BCPL, створеного свого часу Мартіном Річардсом ( Martin Richards ) . BCPL надав певний вплив на мову В, розроблений Кеном Томпсоном ( Ken Thompson ). У свою чергу розвиток мови В привело до створення в 1970 році мови С. Протягом багатьох років стандартом С була фактично версія , що поставляється разом з операційною системою Unix. Ця версія вперше була описана Брайаном Керніганом ( Brian Kernighan ) і Деннісом Рітчі в книзі The С Programming Language ( Englewood Cliffs , NJ : Prentice - Hall , 1978). Влітку 1983 був утворений комітет зі створення для мови С стандарту ANSI (American National Standards Institute - Національний інститут стандартизації США). Треба відзначити, що процес стандартизації зайняв вельми чималий термін - шість років. Стандарт ANSI був остаточно прийнятий у грудні 1989 року і вперше опублікований на початку 1990 року. Цей стандарт був також прийнятий організацією ISO ( International Standards Organization - Міжнародна організація по стандартизації), тому він називається стандартом ANSI / ISO мови С. У 1995 році була прийнята 1- а Поправка до стандарту С, згідно з якою, серед іншого , було додано декілька бібліотечних функцій. У 1989 році стандарт С разом з 1 -й Поправкою став базовим документом Стандарту C + + , визначального С як підмножина C + +. Версію С, визначену стандартом 1989 року, звичайно називають С89 . Протягом 90- х років увагу програмістів була прикута головним чином до розвитку стандарту C + +. Тим часом розробка С також продовжувалася , привівши в 1999 році до появи стандарту С, який прийнято називати С99 . У цілому С99 зберіг всі основні риси С89 , тобто , можна сказати , що мова С залишилася сама собою! Комісія стандартизації С99 приділила основну увагу двома напрямками: додаванню декількох чисельних бібліотек і розвитку нових вузькоспеціальних засобів, таких як масиви змінної довжини і модифікатор покажчика restrict . Завдяки цим нововведенням мова С знову опинився на передньому краї розвитку мов програмування.
Основні поняття та визначення.
Програма мовою С складається з синтаксичних конструкцій які називаються - команди (оператори, вказівки).
Команди будуються з лексем. Лексема неподільний елемент мови (слово, число, символи операцій). Ідентифікатор це назва (імя), яку користувач надає обєктам, наприклад змінним, сталим, функціям. Ідентифікатори записуються латинськими буквами, цифрами, знаком підкреслення. Розпочинаються ідентифікатори з латинських літер та знаку підкреслення.
При написаннні імені ідентифікатора враховується регіср. Ключові слова це зарезервовані ідентифікатори, які використовуються для написання команд.
Перелік зарезервованих слів: auto continue float interrupt short unsigned asm default for long signed void break do far near sizeof volatile case double goto pascal static while cdecl else huge switch struct char enum if register typedef const extern int return union
Перепроцесор - це програма, яка опрацьовує директиви. Директива перепроцесора це команди компілятора, які виконуються на початку програми
Директиви мови С розпочинаються символом #
#include директива підключення файлу, бібліотеки
Директива #define
#define має два значення:
Константа це величина, яка не змінюється протягом виконання всієї програми
Коментар це фрагмент тексту який призначено для пояснення програми або окремих фрагментів. Коментар записують:
Абетки(алфавіт) мови С.
Структура програми.
#include //опис бібліотек
void main() //головна функція
{
тіло програми;
}
На приклад:
#include
void main()
{
cout<<“моя перша програма мовою сі!\n”;
}
Стандартні типи даних
Вид |
Назва |
Діапазон |
Обсяг байтів |
Символьний |
unsigned char |
0 .. 255 |
1 |
Char |
-128 .. 127 |
1 |
|
Перелічувальний |
Enum |
-32,768 .. 32,767 |
2 |
Цілі числа |
unsigned int |
0 .. 65,535 |
2 |
short int |
-32,768 .. 32,767 |
2 |
|
Int |
-32,768 .. 32,767 |
2 |
|
unsigned long |
0 .. 4,294,967,295 |
4 |
|
Long |
-2,147,483,648 .. 2,147,483,647 |
4 |
|
long long або __int64 |
9 223 372 036 854 775 808 … 9 223 372 036 854 775 807 |
8 |
|
unsigned __int64 |
0 … 18446744073709551615 |
8 |
|
Дійсні числа |
Float |
3.4 * (10-38) .. 3.4 * (10+38 |
4 |
Double |
1.7 * (10-308) .. 1.7 * (10+308) |
8 |
|
long double |
3.4 * (10-4932) .. 1.1 * (10+4932) |
10 |
Закріплення вивченого матеріалу.
Практичне завдання:
Виберіть правильно записані ідентифікатори:
а) a, aa, _a, 1s, max znach;
б) q, f_1? A..a, a*s;
в) s1, Max_znach, x1,_y;
Які з тверджень правильні?
в) тип int займає 1 байт
г) тип long займає 4 байт
д) тип char займає 2 байт
е) тип unsigned char займає 4байт
є) тип short int займає 4 байти
Лекція №2
Тема: Оголошення змінних, ініціалізація, константи, введення та виведення даних. Побудова виразів. Привласнення. Бітові та арифметичні операції. Макроси.
Мета: Вивчення базових констукцій мови. Придбання знань та умінь при розробці та використання макросів при розробці програм. Придбання навичок при побудові складних виразів.
Питання:
Оголошення змінних, иниціалізація, константі.
Змінні оголошують в операторі опису. Оператор опису складається з специфікації типу та списку імен змінних, розділених комою. Наприкінці обов'язково повинна стояти крапка з комою. Оголошення змінної має наступний формат :
[ модифікатори ] спеціфікатор_тіпа ідентифікатор [ , ідентифікатор ] ...
Модифікатори - ключові слова signed , unsigned , short , long .
Специфікатор типу - ключове слово char або int , що б тип оголошуваною змінної.
Ідентифікатор - ім'я змінної .приклад : char x ; int a , b , c ; unsigned long long y ;
Таким чином , будуть оголошені змінні x , a , b , c , y . У змінну x можна буде записувати значення від -128 до 127. У змінні a , b , c - від -32768 до +32767 . У змінну y - від 0 до 18446744073709551615 .Ініціалізація значення змінної при оголошенні. При оголошенні змінну можна проініціалізувати , тобто присвоїти їй початкове значення. Зробити це можна таким чином. int x = 100;Таким чином , в змінну x при оголошенні відразу ж буде записано число 100. Краще уникати змішування ініціалізіруемих змінних в одному операторі опису , тобто ініціалізіруемих змінні краще оголошувати в окремих рядках .
Змінна будь-якого типу може бути оголошена як немодіфіціруемих . Це досягається додаванням ключового слова const до специфікаторами типу. Змінні з типом const представляють собою дані , використовувані тільки для читання , тобто цієї змінної не може бути присвоєно нове значення . Якщо після слова const відсутня специфікатор типу , то константи розглядаються як величини зі знаком , і їм присвоюється тип int або long int у відповідності зі значенням константи: якщо константа менше 32768 , то їй присвоюється тип int , в іншому випадку long int. приклад :
const long int k = 25;
const m = -50 ; / / мається на увазі const int m = -50;
const n = 100000 ; / / мається на увазі const long int n = 100000;
Введення виведення даних.
Бібліотека мови С++ підтримує наступні три рівні уведення - виведення:
При уведенні - виводі потоку всі дані розглядаються як потік окремих байтів. Для користувача потік - це або файл на диску, або фізичний пристрій, наприклад, дисплей, клавіатура або друкувальний пристрій, з якого або на яке направляється потік даних. Функції уведення - виводу для потоку дозволяють обробляти дані різних розмірів і форматів від одиночного символу до більших структур даних. Програміст може використати функції бібліотеки або розробляти власні й включати їх у бібліотеку. Для доступу до бібліотеки цих функцій треба включити в програму відповідні файли заголовків, наприклад
#include <iostream>.
За замовченням стандартні уведення, вивід і вивід повідомлень про помилки ставляться до консолі користувача (клавіатура й екран). Це значить, що щораз, коли програма очікує уведення зі стандартного потоку, дані повинні надходити із клавіатури, а якщо програма виводить, то на екран. У С++ є кілька бібліотек які містять функції введення - виводу. До них відносяться:
<stdio.h> містить більшість функцій уведення виводу ;
<ctype.h> містить функції для обробки символьних масивів;
<string.h> містить функції для перевірки символів.
Найчастіше в С++ використається потокове уведення даних, операції якого включені до складу класів istream або iostream. Він може здійснюватися з визначеним у цих класах вхідним потоком cin або вихідним потоком cout. Для читання символів із цього потоку вказується операція добування з потоку, що позначається за допомогою символів “ >> “ . Це перевантажена операція, визначена для всіх простих типів і покажчика на char.
Формат запису оператора cin має вигляд:
cin [ >> values ]; .
Наприклад, для уведення значень змінних Х та Y можна написати:
cin >> x >> y; .
Кожна операція “ >> “ передбачає уведення одного значення. При уведенні даних необхідно виконувати наступні вимоги:
для поділу двох чисел, що вводять послідовно, використається пробіл (дані типу сhаr розділяти пробілом не обов'язково) або ENTER;
якщо послідовно вводиться символ і число (або навпаки), пробіл треба записувати тільки в тому випадку, якщо вводить символ, що (типу сhаr) є цифрою, потік уведення ігнорує пробіли;
при введенні великої кількості даних одним оператором їх можна розташовувати в декількох рядках (використовуючи ENTER);
Оператор уведення з потоку припиняє свою роботу тоді, коли всі включені в нього змінні одержують значення.
Оскільки прикладі пробіл є роздільником між значеннями що уводяться, то для введення рядків, що містять пробіли у своєму складі, цей оператор використати не можна. У такому випадку треба використати методи класів такі як: getline( ), read( ), get( ) і ін. (формати цих функцій розглянемо при обробці символьних даних). У С++ введення - вивід може виконуватися з використанням операторів, а не функцій введення - виводу.
Вивід даних може бути неформатованим та форматованим. Найчастіше для виводу застосовується визначена операція “<<“, що записується разом з ім'ям вихідного потоку cout. У такий спосіб запис:
cout << x;
означає вивести значення змінної x (або записати в потік). Цей оператор вибирає необхідну функцію перетворення даних у потік байтів.
Формат запису cout відрізняється від синтаксису С++ .
cout << data [ << data ]; ,
де data це змінні, константи, вирази або комбінація типів.
Запис cout може виглядати так:
сout << "y=" << x + a - sin(x) << "\n"; .
При використанні булевських операцій, вирази треба брати в дужки.
сout << "p =" << ( a && b || c ) << "\n"; .
У С++ немає символьних значень. Символ переміщення каретки "\n" записується як рядкова константа, інакше він розглядається не як керуючий символ, а як число 10 ( код символу). Таких помилок можна уникнути шляхом присвоєння значення керуючих символів змінним, наприклад:
#define <<sp " "
#define <<ht "\t"
#define <<hl "\n"
Тепер оператор виводу можна записати:
сout << " y = " << x + a - sin(x) << hl; .
Треба пам'ятати, що cout не виконує автоматичне повернення каретки. Щоб надрукувати рядкову константу, треба помістити її за оператором cout у такому виді:
cout << " Я вивчаю програмування \n"; .
Приклад У наступному прикладі друкуються вихідні дані, додані деякі повідомлення, що пояснюють, і символи переведення рядка:
//P1_1.CPP ( друк значень змінних
// з відповідними позначками)
#include <iostream.h>
int main ( )
{
char first = 'W';
char middle = 'P';
char last = 'S';
int wozrast = 20;
int doplata = 2;
float zarplata = 309.75;
float prozent = 8.5;
// Друк результатів
cout << " Перевірка вихідних даних\n";
cout << first << middle << last << "\n\n";
cout << " Вік доплата зарплата відсоток: \n";
cout << wozrast << ' ' << doplata << ' ' << zarplata << ' ' << prozent;
return 0;
}
Останні два оператори можна представити із символами табуляції. Знак \t розміщає кожне наступне ім'я або число в наступну позицію табуляції (кожні вісім символів). Наприклад:
cout << " Вік\t доплата\t зарплата\t відсоток\t \n";
cout << wozrast<<"\t" << doplata<<"\t"<< zarplata<<"\t"<< prozent<<"\n " ;
Для додаткового керування вихідними даними використовуються маніпулятори: setw(n) і setprecision(k). Маніпулятор setw(n) необхіден для вказівки довжини поля, що приділяється для виведення наступного даного (де n - кількість позицій у рядку). setprecision(k) призначений для вказівки кількості позицій у дробовій частині значень даних типу float.
Маніпулятори змінюють вид деяких змінних в об'єкті cout, які в потоці знаходяться за ними. Ці маніпулятори називають прапорами стану. Коли об'єкт посилає дані на екран, він перевіряє прапори, щоб довідатися, як виконати завдання, наприклад, запис:
cout << 456 << 789 << 123;
видасть значення у вигляді: 456789123, що ускладнює визначення групи значень.
Приклад Написати програму з використанням маніпулятора setw .
// P1_2.CPP ( демонстрація використання маніпуляторів)
#include <iostream.h>
#include <iomanip.h>
int main ( )
{
cout << 456 << 789 << 123 << endl;
cout << setw(5) << 456 << setw(5) << 789
<< setw(5) << 123 << endl;
cout << setw(7) << 456 << setw(7) << 789
<< setw(7) << 123 << endl;
return 0;
}
Примітка: У мові С++ операція endl рівнозначна “/n”
Результати виконання програми:
456789123
456 789 123
456 789 123
У цьому прикладі з'явився новий заголовний файл iomanip.h, що дозволяє використовувати функції маніпуляторів, таких як setw(). При використанні функції setw(), С++ вирівнює число вправо в границях заданої ширини поля виводу. Якщо ширина недостатня, С++ ігнорує зазначене значення.
Функція setprecision(2) встановлює, що число з плаваючою крапкою треба виводити із двома знаками після крапки з округленням дробової частини.
Наприклад:
сout << setw(7) << setprecision (2) << 123.456789;
результат виконання: 123.46.
cin використоаує ті ж маніпулятори, що й операція cout. Список змінних, у які будуть поміщені дані, визначені в values.
Приклад Програма для обчислення податку на продж товару.
// P1_3.CPP програма податок
#include <iostream.h>
#include <iomanip.h>
int main ( )
{
float prod_sum;
float nalog;
cout << " Введіть суму продажі для розрахунку податку ";
cin >> prod_sum;
nalog = prod_sum* 0.7; // Обчислення податку на продажу
cout << " " << setprecision(2) << prod_sum;
cout << " " << setprecision(2) << nalog"\n";
return 0;
}
При форматированні уведення використаються функції:
scanf( ) форматированный уведення з потоку ;
sscanf( ) форматированный уведення з рядка ;
fscanf( ) форматированный уведення з будь-якого стандартного пристрою.
Для форматованого введення використається функція scanf(), що забезпечує форматирований засіб введення даних. Функція scanf() має змінне число параметрів, але як фактичні параметри вона використає адреси змінних, а не їхні значення. При цьому перед відповідним параметром ставиться знак & - символ узяття адреси змінної. Наприклад, &x означає "адреса змінної x", а не значення, що ця змінна має в цей момент. Рядок формату функції scanf() вказує, які дані очікуються на вході. Якщо функція зустрічає у форматному рядку знак % , за яким треба знак перетворення, то вона буде пропускати на вході символи доти, поки не зустріне який - нибудь непустий символ. У такий засіб функція scanf() може змінювати значення змінних у програмі.
Форма запису цих функцій має вигляд:
scanf ( "рядок формату", список адрес змінних ); .
Рядок формату являє собою наступний структурний запис:
%[*] [довжина] [f | n ] [ h | l ] керуючий_символ
де % ознака початку форматного коду. Якщо за символом % треба символ, що не є керуючим символом формату, то він розглядається як звичайна послідовність символів. При цьому наступні за ним символи (до наступного символу %) також уважаються просто символами; якщо за символом % треба * (зірочка), те присвоювання наступного вхідного поля придушується, поле читається, але не зберігається;
поле "довжина" позитивне десяткове ціле число задає максимальне число символів, що може бути прочитане із вхідного потоку, поки не зустрінеться символ " пробіл" або символ, що не може бути перетворений відповідно до заданого формату;
f і n дозволяють придушити угоду за замовченням при використанні моделі пам'яті;
h і l предикати, які визначають аргументи short і long відповідно;
керуючий_символ задається одним із символів :
d десяткове ціле;
i десяткове, восьмерічне або шестнадцатерічне ціле зі знаком;
c одиночний символ;
u беззнакове десяткове число;
x, X беззнакове шестнадцатерічне число;
0 восьмерічне число;
s сприймає символи без перетворення до символу "\n", пробілу або поки не буде досягнута задана довжина при виводі видає в потік всі символи до символу "\0", або до досягнення заданої точності.
f значення із плаваючою крапкою;
e , E значення у формі Е;
G , g значення зі знаком у формі f або e залежно від значення.
Аргументи у функції scanf( ) повинні бути записані у формі покажчиків, тобто у вигляді &x, &y, &mas[i] і т.п. Наприклад, для введення змінних k(int) і p(float) ця функція має наступний вигляд:
scanf( " %d %f \n ", &k, &p); .
Приклад Ввести два числа й обчислити їх суму:
// P1_4.CPP (введення двох чисел і обчислення їхньої суми
#include <stdio.h>
int main( )
{
int a,b,c;
scanf ( " %d %d",&a,&b);
c=a+b;
printf ("Cума=%d \n", c);
return 0;
}
В результаті виконання програми буде виведено:
Сума=13
(13 значення змінної с, яке залежить від введених значень змінних a і b)
Форматний рядок пропонує функції scanf() ввести десяткове число, яке треба помістити в змінну а, а потім перейти до наступного не порожнього символу й із цього місця почати введення нового десяткового числа, що потім присвоєти змінній b. Якщо за рядком керування форматом аргументів більше, ніж специфікацій формату, зайві аргументи ігноруються. Якщо для специфікацій формату недостатньо аргументів, результат не визначений.
Функція printf() призначена для форматированного виводу. Щоб зв'язати програму користувача зі стандартною бібліотекою, де перебуває ця функція, необхідно на початку програми передбачити препроцесорне твердження
#includе <stdio.h>
Формат функції printf :
Функція printf може використатися для виводу повідомлення на екран.
Наприклад:
printf (" Введіть вхідні дані \n "); ,
де \n керуюча послідовність, що забезпечує перехід на новий рядок: сама функція printf() автоматично на новий рядок курсор не переводить.
Повідомлення можна вивести на екран і в такий засіб:
printf (" Введіть ");
printf (" вхідні дані");
printf (" \n ");
Результат виконання фрагменту програми буде таким же, як і в першому випадку.
Найчастіше функція printf() використається для виводу значень змінних. Першим аргументом функції є рядок формату, який знаходиться у подвійних лапках, а наступними, якщо вони є, змінні, значення яких виводяться за вказаним форматом.
Рядок формату може містити звичайні символи, які копіюються при виводі й керуючі символи, що починаються зі знака %: за специфікатором необхідно вказати керуючий символ перетворення. Кожна специфікація перетворення відповідає одному з аргументів, які знаходяться за рядком формату, і між ними встановлюється однозначна відповідність, наприклад:
printf ("Значення а, b, с рівні: %d %d %d \n", a, b, c); ,
символ d у специфікації перетворення вказує, що значення аргументу повинне бути надруковане як десяткове ціле число.
При виводі використаються ті ж специфікації, що й при введенні:
% с для виводу окремого символу;
% s для друку символьного рядка;
% x для виводу шестнадцятирічної літери;
% 0 для виводу восьмерічних чисел;
% f для виводу чисел із плаваючою крапкою;
В функії printf(), наприклад такого виду: printf (" % c=%d \n", g, g ); значення змінної g виводиться як символ алфавіту, а після знака рівності як числове значення. Перед символом перетворення може стояти числовий коефіцієнт, що явно вказує кількість позицій у виведеному рядку, відведених для елемента виводу.
Список форматних кодів має форму запису:
% [ прапор ] [ довжина ] [.точність ] [ f | n] [ h | l ] керуючий_символ,
де прапор символ, керуючий вирівнюванням виводу й виводом пробілів, десяткової крапки, ознак чисел восьмеричної й шестнадцатеричной систем числення. Прапор може задаватися одним із символів:
"- " вирівнювання вліво відносно заданого поля;
"+" вивід знака числа;
"пробіл" приєднання пробілу до виведеного числа, якщо число має тип зі знаком і позитивно;
"#" виводиться ідентифікатор системи числення для цілих: 0 - для восьмерічних чисел, 0х або 0Х - для шестнадцатирічних чисел.
поле "довжина" визначає мінімальну кількість виведених символів. Якщо довжина більше виведеної кількості символів, то виведене значення доповнюється пробілами, якщо довжина менше кількості символів у виведеному значенні або вона не задана, виводяться всі символи значення ( відповідно до поля " точність", якщо воно є);
поле "точність" задається цілим числом після крапки й визначає кількість виведених символів, кількість знаків після коми. На відміну від поля довжини точність може привести до усікання даних, що виводяться.
Інші параметри списку форматних кодів мають раніше описаний зміст.
Приклад Обчислити значення функції:
Y= A*X*X-SIN(X) , якщо A=10.3; X[-1 ; +1]; hx=0.2.
// P1_5.CPP - обчислення функції й використання
// форматних кодів при виводі результату
#include <stdio.h>
#include <math.h>
int main( )
{
float a, x, y;
a = 10.3;
fprintf ( stdprn,"\t Результати обчислення\n\n") ;
for ( x = - 1 ; x <= 1; x = x + 0.2 )
{
y = a * x * x - sin ( x );
fprintf ( stdprn," \t x = % 4.1 f y = % 6.3 f \ n", x, y ) ;
}
}
При виводі на принтер значень аргументу й значень функції використається функція fprintf(), у якому для виводу змінюється стандартний поток на stdprn.
Припустима так само наступна форма операторів виводу:
printf ( " %d %d \n ", n, func ( 2, n ) ); ,
де func (2, n ) - виклик деякої функції.
Оператори присвоювання мають одну з наведених нижче форм запису:
p =a ;
p =a = b = c;
де p ім'я змінної або елемент масиву; а, b, з арифметичні вираження.
Фрагмент програми може мати наступний вигляд:
x = 3/5;
y = x*x-sin(x); // Присвоювання ліворуч праворуч
z = p = y;
a = b = c =5;
Це називається множинним присвоюванням. При присвоюванні тип результату відповідає типу змінної, що знаходиться ліворуч від операції.
Побудова виразів. Пост и пред інкремент.
Якщо необхідно змінити значення змінної на 1 , то використовують інкремент або декремент. Інкремент - операція збільшення значення , що зберігається у змінній , на 1. Приклад: x + +; / / значення змінної x буде збільшено на 1 Декремент - операція зменшення значення , що зберігається у змінній , на 1 .приклад : x - ; / / значення змінної x буде зменшено на 1. Інкремент і декремент відносяться до операцій присвоювання . При використанні декремента і инкремента спільно з оператором присвоювання "=" застосовують Постфіксний ( x + +) або префіксних ( + + x ) запис . Першою виконується префіксная запис . приклади : y = x + +; Припустимо , що у змінній x зберігалося значення 5 . Тоді в y буде записано значення 5 , після чого значення змінної x буде збільшено на 1 . Таким чином , в y буде 5 , а в x - 6. y = - x ;
Якщо в x зберігалося значення 5 , то спочатку буде виконано зменшення x до 4 , а потім це значення буде присвоєно змінної y . Таким чином , x і y буде присвоєно значення 4 .
Закріплення вивченого матеріалу.
Практичне завдання:
#include <stdio.h>
#define cube(x) x*x*x
int main()
{
Int a = 2;
Int b = 3;
printf(“%d”, cube(a + b));
return 0;
}
Лекція №3
Тема : Оператори розгалуження: тернарний оператор, if(), swith().
Мета: Ознайомити зі структурою операторіз розгалуження програмного коду. Навчитися використовувати оператори розгалуження при розробці програм.
Питання:
Оператор умовного переходу.
Оператор умовного переходу має наступний формат запису:
if (L) оператор 1 ;
else оператор 2 ; ,
де L вираз. Якщо значення цього вираження “істина” (не нуль ), то виконується оператор1, якщо ж воно “неправда” (нуль), то виконується оператор2; у випадку, коли вираз має значення 0 й відсутня запис else, виконується наступний оператор.
Наприклад, оператор умовного переходу може мати вигляд:
if ( i < j ) i++;
else { j = i - 3; i ++; }
Оператор вибору використовується для вибору одного з багатьох варіантів рішення й має наступну форму запису;
switch (вираз)
{
case константний_вираз_1: оператор; [break;]
case константний_вираз_2: оператор; [break;]
case константний_вираз_3: оператор; [break;]
case константний_вираз_n: оператор; [break;]
[default: оператор;]
}
де switch, case, default службові слова;
break оператор (необов'язковий) переривання виконання оператору switch;
вираз будь-який вираз одного із цілих типів;
константний_вираз_1 - константний_вираз_n константні вирази, які не повинні повторюватися й не можуть містити змінних або викликів функцій. Зазвичай це ціла або символьна константа;
оператор; будь-які оператори С++.
Виконується цей оператор наступним чином: обчислюється значення виразу, що знаходиться у дужках, потім це значення послідовно зверху вниз зрівнюється зі значеннями константних виразів, і при збігу значень - виконаються всі оператори починаючи з відповідного, якщо наприкінці гілки немає оператора break. При наявності оператора break, виконується тільки оператор, що перебуває у відповідній гілці й керування передається операторові, що перебуває за межами оператора switch.
Якщо значення виразу не збігається з жодним із значень константних виразів, то виконується оператор, що знаходиться за ключевим словом default і здійснюється вихід з оператора switch.
У випадку, коли в цьому операторі немає default (не обов'язкова наясність), і значення виразу не збігається з жодним зі значень константного виразу, здійснюється вихід з оператора switch.
Приклад фрагменту програми з використання оператора switch:
int a=2;
switch ( a )
{
case 1: func1( );
case 2: func2( );
case 0:
case 4: func3( );
default: printf ("gud bay \n");
}
При виконанні фрагменту програми будуть реалізовані функції: func2(), func3() і default: printf ("gud bay \n"); .
Розглянемо наступний приклад:
int a=2;
switch (a)
{
case 1: func1( ); break;
case 2: func2( ); break;
case 0:
case 4: func3( ); break;
default: printf ("gud bay \n");
} .
У такій реалізації буде виконана тільки функція func2( ); та оператор break; що забезпечить вихід з оператора switch.
Закріплення вивченного матеріалу:
Лекція №4
Тема : Оператори циклу: for() while(). Статичні масиви.
Мета: Вивчення операторів циклу та принципів їх застосування при рохробці програм. Засоби створення та використання статичніх масивів, як контейнерів для даних.
Питання:
У мові С++ є три оператори циклу: while(), do while(), for().
Оператор циклу while (L)
Оператор циклу while (L) із передумовою будь-який простий, складовій або порожньому операторові, тут L-будь-яке припустиме вираження.
Виконується цей оператор у такий спосіб: якщо результат вираження L не дорівнює нулю (“істина”), те виконується цикл, а якщо дорівнює нулю (“неправда “), те цикл не виконується й керування передається наступний за while операторові.
// P1_6.CPP ( обчислення функції y = a* x* x - sin (x)
// с використанням оператора циклу while
#include <stdio .h>
#include <math.h>
main ( )
{
float a, x, y;
a = 10.3 ;
x = - 1 ;
while ( x <= 1)
{
y = a * x * x - sin(x)
printf ("x = % 4 f, y = % 6 f \n", x, y) ;
x = x + 0.2;
}
}
Оператор циклу for()
Оператор циклу for має наступну форму запису:
for ( [ вир1; ] [ вир2; ] [ вир3 ] ) оператор;,
де вир1 вираз ініціалізації звичайно використається для установки початкового значення; це вираз присвоювання (необов'язковий параметр);
вир2 вираз умови , що визначає при якій умові цикл буде повторюватися (необов'язковий параметр);
вир3 вираз ітерації, що визначає крок зміни змінних, керуючих циклом після кожного виконання (необов'язковий параметр).
Цей оператор реалізується в такий спосіб:
спочатку виконується вираз ініціалізації ( ця нотація може бути виконана до оператора for );
( обчислюється умовне вираження;
( якщо результат умовного вираження “істина” (не дорівнює нулю ), те виконується оператор циклу;
( обчислюється вираження ітерації;
( знову перевіряється умова;
як тільки умовний вираз стає рівним нулю “неправда”, керування передається операторові, що випливає за оператором циклу for .
Оскільки перевірка умови виробляється перед циклом, то цикл може жодного разу не виконуватися, якщо умова відразу буде помилковим.
Проілюструємо використання оператора циклу for для раніше розглянутого приклада:
// P1_7.CPP ( обчислення функції y = a * x * x - sin(x)
// с використанням оператора циклу for
#include <stdio .h>
#include <math.h>
main ( )
{
float a, x, y;
a = 10.3 ;
for ( x = -1 ; x <= 1; x = x + 0.2 )
{
y = a * x * x - sin(x) ;
printf ( "x = % 4 f, y = % 6 f \n", x, y ) ;
x = x + 0.2;
}
}
В операторі for може використатися трохи змінних, керуючих циклом, а будь-які вирази можуть бути відсутніми, наприклад :
int = i;
for ( ; i < 4; i++)
або
int k, n, y;
for ( k = 0, n = 20; k <= n; k++, n-- )
y = k * n;
У цьому операторі використається два вираження ініціалізації й два вираження ітерації. Виконується такий оператор у такий спосіб: спочатку привласнюються значення змінним k = 0 і n = 20. Потім відбувається порівняння k <= n. Якщо ця умова має значення “істина”, то буде виконуватися тіло циклу, а потім вираження k++ і n-- ;якщо ж умова не виконується, то цикл припиняється.
Оператор циклу do while()
Оператор циклу do звичайно використається в тих випадках, коли тіло циклу повинне виконуватися хоча б один раз, і має наступну структуру запису:
do оператор
while (вираження); ,
де вираження умовне вираження.
Виконується оператор do у такий спосіб: спочатку здійснюється вхід у тіло циклу й виконується оператор (він може бути простий або складовій), після того перевіряється умова й, якщо воно виконується, тобто “істина” ( не дорівнює нулю), те цикл повторюється, а якщо “неправда” здійснюється вихід із циклу. З використанням оператора циклу do программа буде мати вигляд:
// P1_8.CPP ( обчислення функції y = a * x * x - sin(x)
// с використанням оператора циклу do
#include <stdio .h>
#include <math.h>
main ( )
{
float a, x, y;
a = 10.3 ;
x = -1 ;
do
{
y = a * x * x - sin ( x ) ;
printf ( " x = % 4 f, y = % 6 f \n ", x, y ) ;
x = x + 0.2;
}
while ( x <= 1 );
}
Статичний масив.
Масив - це область пам'яті , де можуть послідовно зберігатися кілька значень. Візьмемо групу студентів з десяти чоловік . У кожного з них є прізвище . Створювати окрему змінну для кожного студента - не раціонально . Створимо масив , в якому зберігатимуться прізвища всіх студентів.
Приклад ініціалізації масиву
string students [ 10 ] = { "Іванов" , " Петров " , " Сидоров " , " Ахмедов " , " Єрошкін", "Вихіно", "Андеа", "Вин Дизель", "Картошкин", "Чубайс"};
опис синтаксису
Масив створюється майже так само, як і звичайна змінна. Для зберігання десяти прізвищ нам потрібен масив, що складається з 10 елементів . Кількість елементів масиву задається при його оголошенні і полягає у квадратні дужки. Щоб описати елементи масиву відразу при його створенні , можна використовувати фігурні дужки. У фігурних дужках значення елементів масиву перераховуються через кому. Наприкінці закриває фігурної дужки ставиться крапка з комою. Спробуємо вивести наш масив на екран за допомогою оператора cout.
# include <iostream>
# include <string>
int main ( )
{ std :: string students [ 10 ] = {"Іванов", "Петров", "Сидоров", "Ахмедов", "Єрошкін", "Вихіно", "Андеа", "Вин Дизель", "Картошкин", "Чубайс"};
std :: cout << students << std :: endl ;
return 0 ;
}
Скомпілюйте цей код і подивіться , на результат роботи програми . Готово ? А тепер запустите програму ще раз і порівняйте з попереднім результатом . У моїй операційній системі висновок був таким:
Перший висновок : 0x7ffff8b85820
Другий висновок : 0x7fff7a335f90
Третій висновок : 0x7ffff847eb40
Ми бачимо , що виводиться адресу цього масиву в оперативній пам'яті , а ніякі не «Іванов» і «Петров ».
Справа в тому , що при створенні змінної , їй виділяється певне місце в пам'яті. Якщо ми оголошуємо змінну типу int , то на машинному рівні вона описується двома параметрами - її адресою і розміром збережених даних.
Масиви в пам'яті зберігаються таким же чином. Масив типу int з 10 елементів описується за допомогою адреси його першого елемента і кількості байт , яке може вмістити цей масив . Якщо для зберігання одного цілого числа виділяється 4 байта , то для масиву з десяти цілих чисел буде виділено 40 байт.
Так чому ж , при повторному запуску програми , адреси розрізняються ? Це зроблено для захисту від атак переповнення буфера. Така технологія називається рандомизацией адресного простору і реалізована в більшості популярних ОС.
Спробуємо вивести перший елемент масиву - прізвище студента Іванова.
# include <iostream>
# include <string>
int main ( )
{
std :: string students [ 10 ] = {
"Іванов" , " Петров " , " Сидоров " ,
" Ахмедов " , " Єрошкін " , " Вихіно" ,
" Андеа " , " Вин Дизель" , " Картошкин " , " Чубайс "
} ;
std :: cout << students [ 0 ] << std :: endl ;
return 0 ;
}
Дивимося , компілюємо , запускаємо . Переконалися , що вивівся саме « Іванов» . Зауважте , що нумерація елементів масиву в C + + починається з нуля. Отже , прізвище першого студента знаходиться в students [ 0 ] , а прізвище останнього - в students [ 9 ].У більшості мов програмування нумерація елементів масиву також починається з нуля. Спробуємо вивести список всіх студентів. Але спочатку подумаємо , а що якби замість групи з десяти студентів , була б кафедра їх ста , факультет з тисячі , або навіть весь університет ? Ну не будемо ж ми писати десятки тисяч рядків з cout ?
Висновок елементів масиву через цикл
# include <iostream>
# include <string>
int main ( )
{
std :: string students [ 10 ] = {
"Іванов" , " Петров " , " Сидоров " ,
" Ахмедов " , " Єрошкін " , " Вихіно" ,
" Андеа " , " Вин Дизель" , " Картошкин " , " Чубайс "
} ;
for ( int i = 0 ; i < 10 ; i + +) {
std :: cout << students [ i ] << std :: endl ;
}
return 0 ;}
Якби нам довелося виводити масив з декількох тисяч прізвищ , то ми б просто збільшили кінцеве значення лічильника циклу - рядок for (... ; i < 10 ; ... ) замінили на for (... ; i < 10000 ; .. . ) .
Зауважте що лічильник нашого циклу починається з нуля , а закінчується дев'яткою. Якщо замість оператора строгої нерівності - i < 10 використовувати оператор «менше , або одно » - i <= 10 , то на останній ітерації програма звернеться до неіснуючого елементу масиву - students [ 10 ] . Це може привести до помилок сегментації та аварійного завершення програми . Будьте уважні - подібні помилки буває складно відловити. Масив, як і будь-яку змінну можна не заповнювати значеннями при оголошенні. Оголошення масиву без ініціалізації
string students [ 10 ] ;
/ / або
string teachers [ 5 ] ;
Елементи такого масиву зазвичай містять в собі "сміття" з виділеної , але ще не ініціалізованої , пам'яті. Деякі компілятори , такі як GCC , заповнюють всі елементи масиву нулями при його створенні. При створенні статичного масиву , для вказівки його розміру може використовуватися тільки константа. Розмір виділюваної пам'яті визначається на етапі компіляції і не може змінюватися в процесі виконання .
int n ;
cin >> n ;
string students [ n ] ; / * Невірно * /
Виділення пам'яті в процесі виконання можливо при роботі з динамічними масивами. Але про них трохи пізніше.
Заповнимо з клавіатури порожній масив з 10 елементів .
Заповнення масиву з клавіатури
# include <iostream>
# include <string>
using std :: cout ;
using std :: cin ;
using std :: endl ;
int main ( )
{
int arr [ 10 ] ;
/ / Заповнюємо масив з клавіатури
for ( int i = 0 ; i < 10 ; i + +) {
cout << " [" << i + 1 << "]" << " :";
cin >> arr [ i ] ;
}
/ / І виводимо заповнений масив.
cout << " \ nВаш масив :";
for ( int i = 0 ; i < 10 ; + + i ) {
cout << arr [ i ] << " " ;
}
cout << endl ;
return 0 ;
}
Закріплення вивченного матеріалу.
Лекція №5
Тема : Поняття вказівника та посилання. Унарні операції, адресна арифметика.
Мета: Вивчення поняттів покозчик та посилання, їх особливостей у використанні при роботі з памятю.
Питання :
У мові С++ широко використаються дані, які називаються покажчиками.
Покажчики це змінні, які містять адресу пам'яті, розподіленої для інший змінної. Всі змінні, розглянуті дотепер, зберігали якісь значення (дані). Ці дані могли бути різних типів: символьного, цілого, речовинного й т.д. При оголошенні змінної - покажчика необхідно вказати тип даних, адреса яких буде містити змінна, і ім'я покажчика з попередньою зірочкою.
Загальний формат опису покажчика наступний:
тип * ім'я;
де тип тип значень, на який буде вказувати покажчик;
ім'я ім'я змінн-покажчика;
* - означає операцію над типом, що читається "покажчик" на тип.
Приведемо приклади визначень покажчиків:
int *pn; // покажчик на ціле значення;
float *pf1, *pf2 ; // два покажчики на речовинні значення;
Покажчики не прив'язують дані до якого-небудь певного імені змінної й можуть містити адреса будь-якого неіменованого значення. Існує адресна константа NULL , що означає "порожній" адресу.
У мові С++ є всього два оператори, що ставляться до покажчиків:
“&” оператор " адреса значення ";
“* “ оператор "значення за адресою".
Оператор "*" , використовуваний разом з покажчиками, витягає значення, на яке вказує змінна, розташована безпосередньо після нього.
Оператор "&" , використовуваний разом з покажчиками, повертає адреса змінної, розташованої безпосередньо після нього.
Покажчики можна оголосити одним з наступних способів:
<тип.> *ptr;
<тип> *ptr = <змінна - покажчик>;
<тип> *ptr =&<ім'я змінної>; // адреса змінної
У програмі це може виглядати в такий спосіб:
int *ptx ,b ; float y; // оголошені змінна - покажчик ptx і звичайні змінні b і y;
float *sp = &y; // покажчику sp привласнюється адреса змінної y
float *p = sp; // покажчику p привласнюється значення (адреса значення),
// яке втримується в змінної sp, тобто адреса змінної y.
При оголошенні покажчиків символ "*" може розташовуватися або перед ім'ям покажчика або записуватися відразу після оголошення типу покажчика й поширює своя дія тільки на один змінну - покажчик, перед якою він коштує, наприклад:
long *pt; long* Uk; int * ki, x, h ; // оголошення описів
Якщо буде потреба опису покажчика на осередок довільного типу, замість ідентифікатора типу записується слово void, наприклад:
void *p, *pt; // описані два покажчики на довільний тип даних
Перш ніж використати покажчик у програмі, йому обов'язково треба привласнити адресу якого - або даного, тобто проініціалізувати, інакше можуть бути непередбачені результати.
Для доступу до значення змінної, адреса якої зберігається в покажчику, досить у відповідному операторі програми записати ім'я покажчика із символом "*" , наприклад:
int *p, *p1; // Оголошені два покажчики на комірку пам'яті int;
int x =12, y=5, m [7 ]; // Оголошені змінні x,y і масив m,
// змінні x,y мають встановлені значення
p = &y; // Покажчику p привласнена адреса змінної y.
Тепер, якщо написати оператор виводу на екран у вигляді :
cout << "Адреса р " << p << "Значення по цій адресі= " <<*p; ,
те виведеться адреса комірки пам'яті, де записане змінна y і значення цієї змінної (тобто 5).
Використовуючи запис x = *p; , одержуємо x=5, тому що *p = y = 5; .
Змінити значення y можна записавши:
y = 10; // або *p = 10;
і виконавши операцію
*p= *p+5; // це відповідає операції y = y + 5; тобто y = 15.
При оголошенні покажчиків, їм потрібно привласнювати початкові значення ( ініціювати), при цьому можна або привласнювати адреса об'єкта (змінної), або адреса конкретного місця пам'яті (масиву), або число 0 (нуль), наприклад:
int *pt = (char *) 0x00147; // Привласнюється адреса осередку
int *arrpt = new int [10]; // Визначається початкова адреса розміщення
// динамічного масиву
сhar *p = 0; // Ініціалізація нулем
Оскільки покажчики це спеціальні змінні, то в операціях з іншим покажчиками вони можуть використатися без символу "*", тобто без розкриття посилання, наприклад:
float *pt1, *pt2 , x=15, m[5];
pt1=&x;
pt2=pt1;
pt1 = m; // або pt1 = &m[0]; m це ім'я масиву, що в
// розглядається як спеціальний покажчик константа.
// P2_5.CPP застосування покажчиків
#include < iostream.h >
int main ( )
{
int x = 10;
int *px = &x;
cout << " x =" << x << endl;
cout << "*px =" << *px << endl;
x* = 2; // або x = x*2;
cout << " Нове значення* px =" << *px << endl;
*px += 2; // або *px =*px + 2;
cout << " Нове значення* px=, тобто х=" << x << endl;
return 0;
}
Результат виконання програми:
x = 10
*px = 10
Нове значення *px = 20
Нове значення*px, тобто x = 22
Сам покажчик-змінна має своя адреса, тому буде справедлива
наступний запис:
int *pt1, *pt2;
pt1 = (int*) &pt2; // тут покажчику pt1 привласнюється адреса пам'яті,
// де розташована змінна pt2
Це має сенс у ситуації, коли :
int y,*pt1, *pt2 =&y;
pt1 = (int*) & pt2; .
Існують наступні обмеження на використання операції узяття адреси "&" :
не можна визначати адреса константи ( оскільки їй не приділяється комірка пам'яті), тобто неприпустимий запис : vp = &345;
не можна визначати адреса результату арифметичного вираження, тому наступний запис невірний : vp = & (x + y); .
Для змінних покажчиків дозволені операції:
- присвоювання;
- инкремент і декремент;
- додавання й вирахування;
- порівняння покажчиків однакового типу.
Наприклад, демонстраційна програма з використанням зміних-покажчиків може мати вигляд:
//P2_6.CPP використання змінних- покажчиків
#include <iostream.h>
void main ( void )
{
int near *ptr1 = (int*)100 ;
int near *ptr2 = (int*) 200;
ptr1++;
ptr2 -= 10;
cout << "*ptr1=" << *ptr1 << endl;
cout << "*ptr2=" << *ptr2 << endl;
cout << "*ptr2 - *ptr1=" << *ptr2 -* ptr1 << endl;
}
Результат виконання програми :
*ptr1 = 102
*ptr2 = 190
*ptr2 - ptr1 = 88
У програмі операця ptr1++ збільшить адресу на 2 байти , оскільки дані типу int займають 2 байти .
Закріплення вивченного матеріалу:
Дано наступний фрагмент кода. Виправьте помилки.
int* foo(int n)
{
int *x = (int*)malloc (n*2); // 2 размерность типа int
return x;
}
….
int main()
{
int *x = foo(10);
std::cout << *x;
return 0;
}
Поясніть чому приведена функція не є коректною.
int* foo()
{
int x=2;
return &x;
}
Знайдіть помилку у наведеному фрагменті коду
# include <stdio.h>
int main()
{
int *pInt;
*pInt = 9;
printf(“Значение = %d ”, *pInt);
return 0;
}
Лекція №6
Тема : Динамічний розподіл памяті. Статичні та динамічні масиви.
Мета: Закріплення знань з теми показчики та посилання. Вивчення механізму динамічного керування памятю. Вивчення поняття динамічний масив.
Питання:
У мові С++ масиви й покажчики зв'язані між собою. При оголошенні масиву у вигляді : int mas [20]; ім'я масиву mas визначається як покажчик-константа на перший (нульовий) елемент масиву, тобто на адресу першого елемента масиву &mas[0].
Для доступу до елементів масиву існує два способи:
використання індексу елемента масиву, наприклад, mas[2] або mas[i];
використання адресного вираження тобто вираження з покажчиком на
масив, наприклад, *( mas +2 ) або *( mas+i ).
Ім'я масив-покажчик, можна записати в наступному виді:
int mas [20];
int *ptr1;
ptr1 = mas; // або ptr1 = &mas[0];
Після оголошення масиву int mas [20]; вираження &mas[0] і mas є еквівалентними.
Для обнуління елементів оголошеного масиву досить ініціалізувати його перший елемент: int mas[0] = {0} ; При цьому першому елементу не обов'язково привласнювати значення 0. Якщо в оголошеному масиві ініціалізувати дкілька перших елементів, то інші ініціалізуються нулями.
Наприклад, у випадку коли float mas [10] ={ 12.2, 34.56 }; , вісім елементів, що залишилися одержать значення 0.
При оголошенні масивів можна використати одну з форм запису :
< тип > < ім'я > [n] // Оголошення одномірного
// масиву з n елементів
< тип > < ім'я > [n] = { значення } // Значення елементів масиву
< тип > < ім'я > [ ] = { значення } // розділені комі
Наприклад:
float m[6];
float m[6] = { 3.4, 4.5, 5.6, 6.7, 8.9, 10.3 };
float m[ ] = { 3.45, 4.56, 5.67, 6.78 };
Двовимірні й багатомірні масиви оголошуються аналогічним образом, тобто правомірний запис :
int mas [2][5] ={ 1, 2, 3, 4, 5,
10, 11, 13, 14, 25 };
int mas [ ][5] ={ 1, 2, 3, 4, 5,
10, 11, 13, 14, 25 }; .
При оголошенні одномірних масивів їхній розмір можна не вказувати, якщо оголошений масив відразу ініціалізувати.
В оголошенні багатомірних масивів можна опускати кількість індексів тільки першого виміру при одночасній ініціалізації. Якщо під час оголошення масивів ініціалізація не проводиться, то кількість індексів треба вказувати завжди й скрізь.
Оскільки для масивів завжди в пам'яті приділяється суцільний блок комірок пам'яті , у яких розташовуються елементи, то адреса наступного елемента mas[1] можна вказати шляхом збільшення покажчика на 1, тобто якщо p = &mas[0] , те p=p + 1 або ( p += 1) і для i елемента масиву адреса може бути визначений як p + i; . При цьому автоматично виконується операція збільшення адреси з урахуванням типу масиву й кількості, що відводить, байт для кожного його елемента, отже : адреса [[i] = адреса x[0] + i* sizeof ( тип ) ; .
Якщо два покажчики р1 і р2 указують на елементи одного масиву, то до них застосовні операції відносини: == , != , < , <= , > , >= .
Наприклад, відношення виду р1 < р2 істинно, якщо р1 указує на більше ранній елемент, чим р2 . Любою покажчик можна зрівняти з нулем.
Варто звернути увагу, що для покажчиків, що посилаються на елементи різних масивів, результат арифметичних операцій і відносин не визначений. В арифметиці з покажчиками можна використати адресу не існуючого "наступні за масивом " елемента. Крім того, до покажчика можна додати деяке ціле значення, наприклад можна записати: р + n ,
де n ціле значення, а р покажчик.
Це вираження визначає область об'єкта, що займає n -е місце після об'єкта, на який указує р, при цьому n автоматично множиться на коефіцієнт, дорівнює відповідній довжині об'єкта. Наприклад , якщо int займає 4 байти , те цей коефіцієнт дорівнює чотирьом.
Допускаються також вирахування покажчиків, що вказують на елементи одного масиву.
Наприклад, якщо р1< p2, те р2 - р1+1 це число елементів масиву від р1 до р2 включно.
Таким чином, з покажчиками допускаються наступні операції :
- присвоювання значення одного покажчика іншому;
- додавання й вирахування покажчиків і даного цілого типу ( р+5 );
- порівняння двох покажчиків, що посилаються на елементи одного масиву;
- присвоювання покажчику нуля й порівняння з нулем.
Інші операції з покажчиками заборонені.
Для покажчика дозволяються вираження виду:
р1 = mas; або р++ ,
тут р покажчик , mas масив.
Розглянемо демонстраційні приклади програм роботи з покажчиками.
Приклад Обчислити середнє значення позитивних елементів одномірного масиву.
Приведемо кілька варіантів програмної реалізації цього завдання.
// P2_7.CPP - обчислення середнього значення
// позитивних елементів масиву
// програма без використання покажчиків
#include <iostream.h>
main( )
{
const int n = 5;
float mas[n], s = 0;
int kol = 0;
for( int i = 0; i < n; i++)
{
cout << "Уведіть" << i << "елемент mas" << endl;
cin >> mas[ i ];
if ( mas[ i ] > 0)
{
s += mas[ i ];
kol ++;
}
}
s/ = kol;
cout << "s=" << s << endl;
return 0;
}
Використовуючи ім'я масиву, як покажчик на початок масиву (перший елемент), цю програму можна переписати в такий спосіб:
// P2_8.CPP - обчислення середнього значення
// позитивних елементів масиву
// використання імені масиву як покажчика на його початок
#include < iostream.h>
main ( )
{
const int n = 5;
float mas[n], s = 0;
int kol = 0;
for ( int i=0; i < n; i++)
{
cout << "Уведіть" << i << "елемент mas" << endl;
cin >> *( mas+i);
if(* ( mas+i ) > 0)
{
s+= *(mas+i);
kol++;
}
}
s/=kol;
cout << "s=" << s << endl;
return 0;
}
Якщо описати покажчик, зв'язати його з масивом (адресувати на початок масиву),то використовуючи арифметику покажчиків, можна написати цю програму у вигляді:
// P2_9. CPP -і обчислення середнього значення
// позитивних елементів масиву
// використання арифметики покажчиків
#include < iostream.h>
main ( )
{
int kol = 0;
const int n = 5;
float mas[n], s = 0;
float *pm = mas; // припустимий запис pm=&mas[0]
for ( int i = 0; i < n; i++)
{
cout << "Уведіть" << i << "елемент mas" << endl;
cin >> *pm++;
cout << mas[i] << endl;
if (mas[i] > 0)
{
s+=mas[i];
kol++;
}
}
s/= kol;
cout << "s=" << s << endl;
return 0;
}
У наведеній програмі при уведенні масиву використався покажчик *pm , а при роботі з ним ім'я масиву з індексом.
Якби при роботі з масивом використався покажчик *pm , то результат був би не вірним. Це пояснюється тим, що покажчик *pm в операціях уведення він збільшує свою адресу ( pm++) після уведення чергового елемента масиву й надалі вказує ще не уведений елемент.
Наведемо ще один варіант програмної реалізації цього ж завдання.
// P2_10.СРР -і обчислення середнього значення
// позитивних елементів масиву
// використання покажчиків
#include < iostream.h >
main ( )
{
const int n = 5;
float mas[n], s = 0;
float *pm = mas; // *pm=&mas[0]
int kol = 0;
for ( int i = 0 ; i < n ; i++)
{
cin >> *pm;
cout << "Уведіть" << i << "елемент mas" << endl;
if (*pm > 0)
{
s += *pm;
kol ++;
*pm ++;
}
}
s/= kol;
cout << "s=" << s << endl;
return 0;
}
Приклад Скласти програму сортування одномірного масиву по убуванню методом вставки.
// P2_11.CPP сортування методом вставки (по убуванню)
// застосування покажчиків
#include < iostream.h >
#include < conio.h >
main ( )
{
int stc, mas [6], i, j;
int *pmas;
pmas = mas;
cout << "Уведіть 6 елементів масиву" << endl;
for ( i = 0; i < 6; i++) cin >>*pmas++;
// Наступний оператор знову встановлює покажчик на початок масиву
// (інакше він буде вказувати на наступний за масивом адреса)
pmas = mas;
for ( i =1; i < 6; i++)
{
stc = *(pmas + i);
j = i - 1;
while ( j >= 0 && stc > *(pmas+j))
{
*(pmas+j+1) = *(pmas+j);
j - - ;
}
*(pmas+j+1) = stc;
}
cout << "Результат" << endl;
for ( i = 0; i < 6; i++)
cout << i << " елемент " << *(pmas + i ) << endl;
// Можна використати й таку конструкцію оператора
// cout << i << " елемент " << * pmas++ << endl;
getch ( ); // Для затримки екрана виводу результату
return 0;
}
Як і інші змінні, покажчики можна групувати в масиви, кожний елемент якого містить адреса рядка масиву даних у пам'яті. У такий спосіб можна зберігати дані з "рваними" краями. Цей масив схожий на двовимірну таблицю з одним виключенням: всі рядки в такому масиві можуть мати різну довжину. При зберіганні рядків це дозволяє заощаджувати пам'ять, а при виконанні сортування рядків, вона виконується значно швидше, тому що змінюються тільки покажчики, а не вміст рядків.
fio
(0) |
-> |
П |
Е |
Т |
Р |
О |
В |
\0 |
|
(1) |
И |
В |
А |
Н |
О |
В |
\0 |
||
(2) |
К |
У |
Ц |
\0 |
|||||
(3) |
-> |
В |
А |
Р |
И |
Ч |
\0 |
||
(4) |
-> |
Ю |
Ш |
К |
О |
\0 |
|||
(5) |
-> |
П |
Л |
Ю |
Щ |
\0 |
Наведемо програму, що забезпечує введення подібної інформації з використанням масиву покажчиків.
// P2_11.CPP
#include <iostream.h>
main ( )
{
char *fio [ ] = { "Петров",
"Іванов" ,
"Куций",
"Варич",
"Юшко",
"Плющ " };
int str;
for ( str = 0; str <= 5; str++)
cout << " stroka " <<( str +1 ) << ” = ” << *( fio+str ) << endl;
return 0;
}
Особливістю масиву покажчиків є те, що кожний із цих покажчиків елементів масиву може вказувати на масив довільної довжини. Так двовимірний масив чисел можна записати як матрицю, і як одномірний масив покажчиків, наприклад: int matr[5][7]; або int *pmt [5]; .
При цьому двовимірний масив розглядається як одномірний масив рядків, кожний елемент якого це теж масив стовпців, тобто масив масивів, тому індексування елементів матриці записується у вигляді mas [i][j].
Якщо двовимірний масив описаний за допомогою масиву покажчиків, то доступ до mas [i][j] елемента може здійснюватися одним зі способів:
* ( pm[i]+j ) або *( *( pm+i )+j ) .
Приклад До елементів матриці, що має парні значення, додати число й вивести отриману матрицю в природному виді.
Перший варіант програмної реалізації матриця описується явним способом і робота ведеться з її елементами.
// P2_12.CPP - робота ведеться без покажчиків.
#include < iostream.h >
main( )
{
int mat [2][3];
int i, j;
cout << " Уведіть матрицю "<< endl;
for ( i = 0; i < 2; i++)
for ( j = 0; j < 3; j++)
cin >> mat [i] [j] ;
// Обробка й вивід матриці
cout << " Матриця mat " << endl;
for ( i = 0; i < 2; i++)
{
for ( j = 0; j< 3; j++)
{
if ( ( mat [i][j] /2)*2 == mat[i][j])
mat[i][j] = mat[i][j] + 5;
cout << mat [i][j] << " ";
}
cout << endl; // Переклад рядка при виводі матриці
}
}
Другий варіант програмної реалізації матриця описана як масив покажчиків.
//P2_13.CPP - матриця описана як масив покажчиків:
#include < iostream.h >
main ( )
{
int i, j, *pm[2];
cout << "Уведіть матрицю "<< endl;
for ( i = 0; i < 2; i++)
for ( j = 0; j < 3; j++)
cin >> *( pm[i] + j );
cout << " Матриця МАТ "<< endl;
for ( i = 0; i < 2; i++)
{
for ( j = 0; j < 3; j++)
{
if ( *(pm[i] + j) / 2*2 == *( pm[i] + j ) )
*( pm [i] + j ) += 10;
cout << *( pm [i] + j) << " ";
}
cout << endl;
}
return 0;
}
У розглянутій програмі для виводу матриці можна використати інший вид оператора :
сout << ( (j == 0) ? '\t':' ') << *( pm[i]+j ) << ( (j == 2) ? '\n':' ') ;
Ім'я двовимірної матриці є покажчиком-константою на масив покажчиків-констант, кожний з яких указує на початок відповідного рядка матриці, наприклад для матриці mat [2] [2] маємо :
mat [0] покажчик-константа на нульовий рядок матриці;
mat [1] покажчик-константа на перший рядок матриці;
mat [2] покажчик-константа на другий рядок матриці;
т. е.: mat[0] == &mat[0][0];
mat[1] == &mat[1][0];
mat[2] == &mat[2][0];
Вивід матриці, можна реалізувати в такий спосіб:
cout << mat [i] [j];
cout << *( mat [i] +j );
cout << *(* (mat +i )+j );
У С++ можна описати змінну, що має тип "покажчик на покажчик". Ознакою такого типу є повторення символу "*" при описі змінної, наприклад int ** pmt; при цьому пам'ять для такий змінної не виділяється. Її треба привести до відповідного масиву. При описі покажчик на покажчик може мати значення, наприклад :
int x = 20;
int *px1 = &x;
int** px2 = &px1;
int ***px3 = &px2;
Доступ до змінного x тепер можна здійснити одним із трьох способів: *px1; **px2; ***px3; .
Для доступу до пам'яті через покажчики на покажчики можна використати як індекси так і символи "*", наприклад, еквівалентними будуть посилання на змінну x:
px1 [0] *px1;
px2 [0][0] **px2;
px3 [0][0][0] ***px3;
Закріплення теоретичного матеріалу:
# include <stdio.h>
int main()
{
int Variable = 5;
printf(“Значение = %d”,Variable);
int *pVar = &Variable;
pVar = 9;
printf(“Значение = %d”, *pVar);
return 0;
}
Лекція №7
Тема : Двовимірні масиви, n-мірні масиви.
Мета:Закріплення теоретичних знань зі статичних та динамічних масивів. Придбання знань обробки n-мірних масивів.
Питання:
Двовимірний масив.
Двовимірний масив - це одновимірний масив з одновимірних масивів. Для прикладу визначимо двовимірний масив m розміром 3 x 4 .
int m [ 3 ] [ 4 ] ;
Покажемо, що таке визначення логічно випливає з уявлення про двовимірному масиві як про одновимірному масиві з одновимірних ж масивів .
Почнемо з того , що спочатку оголосимо проміжний тип M - одновимірний масив з 4 -х цілих чисел
typedef int M [ 4 ] ;
Тепер визначення M x ; означає теж саме, що
int x [ 4 ] ; , яким би не був вираз x.
Визначимо одновимірний масив з елементів типу M, тобто масив символів.
M m [ 3 ];
У ролі x тут виступає вираз m [ 3 ]. Підставами його в int x [ 4 ] ; і отримаємо оголошення двовимірного масиву без проміжного типу M, як це зазвичай і робиться.
int m [ 3 ] [ 4 ] ;
Виходячи із загального принципу розміщення елементів масиву в пам'яті ( щільно і в порядку зростання номерів), можна стверджувати , що елементи двовимірного масиву m [ 3 ] [ 4 ] займуть місця в пам'яті в наступному порядку:
m [ 0 ] [ 0 ] , m [ 0 ] [ 0 ] , m [ 0 ] [ 1 ] , m [ 0 ] [ 2 ] , m [ 0 ] [ 3 ] , m [ 1 ] [ 0 ] , m [ 1 ] [ 1 ] , m [ 1 ] [ 2 ] , m [ 1 ] [ 3 ] , m [ 2 ] [ 0 ] , m [ 2 ] [ 1 ] , m [ 2 ] 2 ] , m [ 2 ] [ 3 ]
Приклад. Ввести матрицю M розміру 3 x 3 . Поміняти місцями початкову та останню рядки і вивести результат на екран.
M [ 3 ] [ 3 ] = { { 11,12,13 } , { 21,22,23 } , { 31,32,33 }} ;
for ( int j = 0 ; j < 3 ; j + +)
{
int R = M [ 0 ] [ j ] ;
M [ 0 ] [ j ] = M [ 1 ] [ j ] ;
M [ 1 ] [ j ] = R;
}
for ( int i = 0 ; i < 3 ; i + +)
{
for ( j = 0 ; j < 3 ; j + +)
{
cout << M [ i ] [ j ] << " " ;
}
cout << endl ;
}
Багатовимірні масиви.
Багатовимірні масиви задаються зазначенням кожного вимірювання в квадратних дужках , наприклад:
int matr [ 6 ] [ 8 ] ;
задає опис двовимірного масиву з 6 рядків і 8 стовпців. Масив складається з 48 елементів. У пам'яті такий масив розташовується в послідовних комірках построчно .
Тривимірний масив буде описаний як :
double mass [ 4 ] [ 3 ] [ 2 ] ;
Багатовимірні масиви розміщуються так , що при переході до наступного елементу швидше за все змінюється останній індекс.
Для доступу до елементу багатовимірного масиву вказуються всі його індекси , наприклад ,
matr [ 2 ] [ 3 ] = 4 ;
елементу матриці matr2 , 3 присвоєно значення 4 ;
x = matr [ 2 ] [ 3 ] ;
змінної x присвоєно значення елемента матриці matr2 , 3 .
При ініціалізації багатовимірного масиву він представляється як масив з масивів , при цьому кожен масив полягає в свої фігурні дужки ( в цьому випадку величину першого розмірності при описі можна не вказувати ) ,
int mass2 [] [ 2 ] = { { 1 , 1 } , { 0 , 2 } , { 1 , 0 }} ;
У цьому прикладі инициализирован масив матриці:
Можна задати загальний список елементів у тому порядку , в якому елементи розташовуються в пам'яті ( тоді все розмірності вказуються) :
int mass2 [ 3 ] [ 2 ] = { 1 , 1 , 0 , 2 , 1 , 0 } ;
Обнулити весь масив можна так:
int mass2 [ 3 ] [ 2 ] = { 0 } ;
Закріплення вивченного матеріалу.
(скорпировать задания по много мернім мссивам)
Лекція №8
Тема : Поняття функції. Структура. Виклик, передача параметрів.
Мета: Ознайомитися з призначенням, синтаксисом оголошення та реалізації функцій, вивчмтм засоби передачі перементів у функції. Навчитися срозробляти програми з використанням функцій.
Питання:
Функція - це логічно завершена сукупність описів і операторів, призначених для виконання певного завдання.
У мові С++ немає розподілу на основну програму й підпрограми, на процедури й функції. Однак серед функцій повинна бути одна з ім'ям main, що може перебувати в будь-якім місці програми. Ця функція виконується завжди першої й закінчується останньої.
Будь-яка функція має однакову структуру, що має вигляд :
[тип результату, що повертає функція] ім'я функції ([список формальних аргументів])
{
опису даних ;
оператори ;
[return] [вираз];
}
тут квадратні дужки ( [ ] ) , як звичайно вказують, що укладена в них конструкція може бути відсутня ;
тип результату будь-який базовий або раніше описаний тип значення повертає функцією (необов'язковий параметр), якщо він відсутній, то тип результату буде цілий (int); цей параметр може бути описаний ключовим словом (void), у цьому випадку функція не повертає ніякого значення (результату). Якщо параметр заданий із зірочкою (*) функція повертає покажчик на об'єкт, або на одержання адреси об'єкта (& ) (в операторі return);
ім'я функції будь-яке ім'я, що складається з букв, цифр і знака "_" (підкреслення), що починається буквою або знаком "_", або ім'я main за ім'ям функції завжди повинна стояти пари круглих дужок ( ), у яких записуються формальні параметри, а якщо їх ні, те порожні дужки. Cледует мати через, що фактично ім'я функції ( це особливий вид покажчика на функцію, його значенням є адреса крапки входу у функцію;
список формальних аргументів визначає кількість, тип і порядок проходження переданих у функцію вхідних аргументів, які друг від друга розділяються комі . У випадку, коли список відсутній, це поле буде порожнім ( ), або містити ключове слово ( void ). Формальні параметри функції повністю локалізовані в ній і недоступні для будь-яких інших функцій.
Список формальних аргументів повинен мати такий вигляд:
( [const] тип 1 [параметр 1] , [const] тип 2 [параметр 2],. . . )
У цьому списку не можна групувати параметри одного типу, указавши їхній тип один раз, для кожного параметра треба вказувати його тип. Слово const повідомляє компіляторові, що значення параметра не повинне зміняться функцією (це необов'язковий атрибут).
За замовчуванням всі формальні параметри передаються за значенням, тобто якщо вони змінюються в тілі функції, то за її межами залишаються без зміни, оскільки передаються не самі значення, а їхні копії. Ці копії створюються усередині функції, це ілюструє наступний фрагмент програми:
void fun ( int p)
{
++ p;
cout << " p=" << p << endl;
}
void main ( )
{
int x = 10;
fun (x); // виклик функції
cout << "x=" << x << endl;
}
У результаті роботи цього фрагмента програми буде виведено: р=11, х = 10, оскільки для виклику функції fun(x) у неї передається копія значення рівного 10, при виконанні цієї функції усередині її значення збільшується на 1, тобто (++р) і, тому виводиться р = 11, але за межами функції зазначене значення не змінюється.
Якщо треба змінити змінну оригінал, тобто передати зміна параметра за межами тіла функції, то можна передати значення параметра по посиланню або використати параметр-покажчик. Тоді, використовуючи параметр- посилання, одержимо :
// Використання параметра - посилання
void fun (int &р)
{ ++p;
cout << "p = " << р << endl;
}
void main ( )
{
int x = 10;
fun( x );
cout << "x=" << x << endl;
}
У результаті буде виведена інформація: р=11 і х=11.
При використання аргументу-покажчика програма буде мати вигляд:
// Використання параметра - покажчика
void fun2 (int *p)
{
++*р;
cout << "*p = " << *p << endl;
}
void main ( )
{
int x = 10;
fun2 ( &x );
cout << "x = " << x << endl;
}
У цьому випадку одержуємо той же результат, тобто р = 11 і х = 11.
При використанні таких параметрів у функцію й з її передається не значення, а адреса, тому зміна значення цієї змінної усередині функції передається за її межі (в інші функції). Якщо треба, щоб які-небудь параметри не змінювали свої значення усередині функції, то їх варто оголосити як параметра-константи, використовуючи модифікатор const .
Тіло функції може складатися з описів змінних і операторів. Змінні, які використаються при виконанні функції, можуть бути глобальні й локальні. Якщо змінні описані (визначені) за межами функції, вони будуть глобальними. З їхньою допомогою можна віддавати дані у функцію, не включаючи до складу формальних аргументів. У тілі функції їх можна змінювати й потім отримані значення передавати в інші функції.
Змінні, описані в тілі функції, називаються локальними або автоматичними. Вони існують тільки під час виклику функції. Як тільки відбувається повернення з функції, система видаляє локальні змінні й звільняє пам'ять. Отже, між викликами функції вміст локальних змінних губиться, тому ініціалізацію локальних змінних треба робити щораз при виклику функції.
У випадку, коли необхідно зберегти значення локальних змінних між викликами функції, їх треба описати як статичні за допомогою службового слова static, наприклад:
static int x, y; або static float p = 3.25;
Статична змінна схожа на глобальну, але доступна тільки в тій функції, у якій вона оголошена.
Таким чином, можна зробити висновку:
параметри функції передаються в неї за значенням, тому за межами функції вони не змінюються, тобто їх не можна використати для передачі результату роботи функції;
при передачі результату функції за її межі використаються: або передача параметрів по посиланню (&р); або передача по покажчику (*р), у цьому випадку під час звертання до функції застосовується символ адреси & , тобто (&р); або як параметр функції варто використати глобальні змінні;
локальні (звичайні) змінні використаються в тілі функції, існують тільки під час роботи функції, а при виході з її знищуються, тому такі змінні називаються автоматичними і їх можна використати тільки для перетворень усередині функції;
якщо виникає необхідність збереження значень локальних змінних між викликами функції, то вони повинні бути оголошені як статичні, тобто з описом static, наприклад:
static char st[ ] = "Тесленко А. М. ";
у якості вхідних (і вихідних) формальних аргументів функцій можуть використатися масиви як фіксованої довжини, так і змінної; якщо використається в якості формальний масив змінної довжини, те обов'язково серед аргументів функції повинна бути змінна, що вказує довжину масиву, наприклад:
int minmas (int mas [ 50]); // Масив фіксованої довжини
int minmas (int mas [ ], int n); //Масив змінної довжини
всі масиви формальні аргументи передаються за адресою, тобто як покажчики;
на початку програми записується заголовок використовуваної функції, називаний прототипом , наприклад:
double sgr (double); // Прототип функції sqr
. . . . . . . . .
int main ( )
{
cout << "5**2 = " << sgr (5) << endl; // Виклик функції
}
При записі прототипу можна перераховувати тільки типи формальних елементів без імен, і наприкінці необхідно ставити символ ";" , а в описі функції цей символ після заголовка не записується.
в останніх версіях мови С++ з'явилася можливість передавати дані за замовчуванням. У цьому випадку, при написанні функції аргументам привласнюються початкові значення, які можуть мати всі аргументи або частина з них. У результаті, задовольняються наступні вимоги: якщо якому-небудь аргументу привласнене значення за замовчуванням, то всі аргументи, що випливають за цим параметром, повинні мати значення за замовчуванням. Таким чином, список параметрів ділиться на дві частини: параметри , що не мають значення за замовчуванням , і параметри, які мають такі значення.
При виклику функції для параметрів, що не мають значень за замовчуванням, обов'язково повинен бути фактичний аргумент, а для параметрів, що мають значення за замовчуванням, фактичні аргументи можна опускати, якщо ці значення не треба змінювати.
Якщо для деякого параметра, що має значення за замовчуванням, опущений фактичний аргумент, то й для всіх наступних (тобто записаних пізніше) параметрів фактичні аргументи повинні бути опущені, тобто їхні значення передаються у функцію за замовчуванням, наприклад, опишемо три функції :
void funct1 ( float x, int y, int z =80 )
{
cout << "x = " << x << " y= " << y << "z=" << z << endl;
}
void funct2 ( float x, int y = 25 , int z = 100 )
{
cout << "x=" << x << "y=" << y << "z=" << z << endl;
}
void funct3 ( float x=3.5 , int y= 40, int z =200 )
{
cout << "x = " << x << "y = " << y << "z = " << z << endl;
}
main ( )
{
funct1 ( 5.1 , 10 ); // за замовчуванням один аргумент z
funct2 ( 10.2 ); // за замовчуванням два аргументи y ,z
funct3 ( ); // за замовчуванням всі аргументи
}
На екрані буде виведено: x =5.1 y = 10 z = 80
x =10.2 y = 25 z = 100
x =3.5 y = 40 z = 200
Із цих програм видно, що аргумент за замовчуванням (це той, значення якого задане при описі заголовка функції, а при її виклику цей аргумент можна не вказувати.
Якщо замість параметра, заданого за замовчуванням при звертанні до функції, записується інше значення фактичного параметра, то значення за замовчуванням придушується заданим фактичним значенням. Так, наприклад, в останньому програмному фрагменті при виклику функції funct (13.5, 75); на екрані буде виведено :
x = 13.5 y =75 z = 80 , тобто z - прийнято за замовчуванням.
Синтаксис мови С++ дозволяє використати покажчик на функцію.
Ім'я будь-якої константи ( це покажчик - константа, дорівнює адресі крапки входу у функцію, тобто адресі її першої машинної команди. Крім констант можна також описувати покажчики - змінні на функцію у вигляді:
type (*name) (список аргументів); ,
де type тип значення, що повертає функцією, ;
name ім'я змінної - покажчика на функцію.
Покажчики на функцію використаються в наступних основних випадках :
( при використанні їх як формальні аргументи в інших функціях;
( для непрямого виклику інших (резидентних) функцій (програм), крапка входу в які записується у відоме місце ОЗУ.
Приклад Обчислення суми й різниці двох чисел програмно реалізувати з використанням покажчика на функцію для доступу до інших функцій
//P5_1.CPP використання покажчика на функцію для доступу
// до інших функцій difference( ) і sum( ).
#include < iostream .h >
int difference ( int, int ); // Прототип функції
int sum ( int, int );
void main ( )
{
int (*fun) (int, int );
int x = 20, y = 5, z ;
fun = difference ; // Присвоювання адреси одного покажчика - іншому
z = fun (x, y);
cout << "z = " << z << еndl;
fun = sum; // Присвоювання нової адреси покажчика
z = fun (x , y );
cout << "z = " << z << endl;
}
int difference (int a , int b) // Опис функції
{ return (a - b);
}
int sum ( int a, int b )
{ return (a + b);}
Як і звичайні змінні, покажчики на функції можна об'єднати в масиви, наприклад, якщо описати функції, тобто їхні прототипи у вигляді:
int god ( const void*, const void * ) ;
int chena ( const void*, const void *) ;
int nazv ( const void*, const void * ) ;
int avtor ( const void*, const void * ) ; ,
те можна описати функцію
int (*fcmp[4]) () {god, chena, nazv, avtor} ; .
У результаті вийшов масив функцій, доступ до елементів цього масиву звичайний, наприклад:
int i =0;
fcmp [i] ( pt1,pt2 ); // це виклик функції god ( pt1, pt2);
Варто звернути увагу, що замінивши індекс, можна викликати іншу функцію й т.д.
Крім повернення результату виконання функцій у вигляді даних за значенням, можливе також повернення за допомогою операцій разыменования "*" або одержання адреси "&".
Операція розкриття імнені "*" означає, що функція повертає адресу на об'єкт. Функції в такому випадку оголошуються як покажчики на функцію, тобто в наступному виді:
type * fname (список формальних аргументів) .
Описані в такий спосіб функції повинні повертати покажчик на тип (адреса), наприклад:
char dayweek (int data)
{
static char *weekday[ ] = {"Sunday", "Monday", "Tuesday",
"Weduesday", "Thursday", "Friday", "Saturday"}
return weekday [data % 7];
}
Тут функція dayweek одержує значення data, тобто число днів, що пройшли з якоїсь певної дати, і повертає день тижня у вигляді покажчика на char оскільки weekday це масив покажчиків на char , що належить типу char*.
При оголошенні функції як покажчика на функцію результат можна передавати шляхом одержання адреси, позначуваного "&". Така функція буде мати наступну структуру:
type *funame (список формальних аргументів)
{
static type x;
// далі треба тіло функції
return &x;
}
Оскільки значенням покажчика є адреса, то функція може повернути адресу об'єкта того ж типу, що й тип покажчика, що повертає. Якщо необхідно повернути результат функції по посиланню, то переважніше використати операцію одержання адреси "&", і функцію описувати у вигляді :
type funame (список формальних аргументів) .
Закріплення вивченного матеріалу:
int* foo(int n)
{
int *x = (int*)malloc (n*2); // 2 размерность типа int
return x;
}
….
int main()
{
int *x = foo(10);
std::cout << *x;
return 0;
}
Лекція № 9
Тема: Передача параметрів у функцію.
Мета: Навчитися передавати параметри у функцію різними способами.
Питання:
Як аргументи (параметрів) функцій можуть бути не тільки змінні, але й масиви. У цьому випадку можна використати як масиви фіксованого розміру, так і невизначеного (масиви змінної довжини). При використанні масивів фіксованої довжини в заголовку функції в списку формальних аргументів указується тип масиву і його розмір, наприклад:
vоid sort (int mas [ 30 ]); .
Якщо описується функція з масивом змінної довжини, то в заголовку вказується тип масиву невизначеного розміру, і обов'язково ще один параметр, за допомогою якого задається розмірність масиву, наприклад:
void sort ( int mas [ ], int n ) ; .
Всі масиви у функції передаються за адресою (як покажчики), тому у випадку зміни масивів у функції , ці зміни зберігаються при поверненні в зухвалу функцію.
Приклад Привести приклад програмної реалізації , у якій відбувається передача символьного масиву у функцію .
//P5_2.CPP використання масивів параметрів функцій
#include < iostream.h >
#include < string.h >
void fun1 (char st [ 5]);
main ( )
{
char p[5] ="стіл ";
fun1 (p);
cout << "p=" << p << endl; // p="стілець "
return 0;
}
void fun1 ( char st [5] )
{
cout <<"p=" << st << endl; // стіл
strcpy (st, "стілець ");
}
Як параметри функцій можна використати не тільки одномірні, але й багатомірні масиви. При цьому використаються масиви як фіксованої розмірності , так і невизначеної довжини.
При використанні багатомірного масиву фіксованого розміру в заголовку функції вказуються значення розмірності масиву, наприклад:
void fun1 (int mat [7][10]); // використається матриця mat(7,10)
Якщо використається багатомірний масив невизначеної довжини, то невизначеним може бути тільки один вимір розмірності, що повинне бути першим, наприклад:
void fun2 ( int mat [ ] [10], int rows, int cols );
Приклад Для заданої матриці зробити обчислення середнього значення кожного її стовпця з використанням функції уведення розмірності матриці, функції уведення матриці й функції одержання середнього значення стовпців.
// P5_3.CPP обчислення середнього значення
// кожного стовпця матриці
#include <iostream .h>
const int mincol = 1;
const int maxcol = 20;
const int minrow = 2;
const int maxrow = 30;
//Функція getnum ( ) для уведення кількості рядків і стовпців
int getnum (const char *elemtype , int low , int high )
{
int n;
do
{
cout << " Уведіть кількість " << elemtype
<< " N від [" << low << "] до [" << high << " ] : ";
cin >> n;
}
while (( n<low) || (n>high));
return n;
}
// Функція inmatr ( ) уведення елементів матриці
void inmatr ( float matr [ ] [maxcol], int rows , int cols);
{
for (int i =0; i<rows; i++)
{
cout << "Уведіть"<< i << "рядок матриці " << endl;
for ( int j =0; j < cols ; j++)
cin >> matr [i][j];
}
cout << endl;
}
// Функція srcols ( ) одержання середніх значень стовпців
void srcols ( float matr [ ] [maxcol] , int rows , int cols )
{
float sum , sr;
for ( int j = 0; j < cols; j++ )
{
sum =0.0;
for ( int i = 0; i < rows; i++)
sum += matr [i][j];
sr = sum / rows;
cout << "середнє значення стовпця" << j << " = " << sr << endl;
}
}
// Головна програма
int main ( )
{
float matr [maxrow] [maxcol];
int rows, cols;
// Уведення кількості рядків і стовпців
rows = getnum ( "rows", minrow, maxrow );
cols = getnum ( "colnums" , mincol , maxcol);
// Уведення матриці
inmatr ( matr, rows , cols);
// Обчислення середнього значення стовпців матриці
srcols (matr , rows , cols);
return 0;
}
Іноді доводиться як формальні аргументи (параметрів) функції використати інші функції. Така ситуація виникає, якщо в деякій функції при звертанні до неї, треба викликати іншу функцію.
Параметр - функція записується у вигляді прототипу, тобто вказується тип функції, її ім'я й у дужках перелік типів формальних аргументів, або типів і імен формальних аргументів.
Приклад Скласти програму c використанням функції обчислення інтегралів метoдом трапецій (точність обчислення е =10-3 ).
// P5_4.CPP - обчислення інтеграла методом трапеції
// використання функції як параметра значення
#include <iostream .h>
#include <math.h>
const float e =1 e-3;
float fn1 (float x ) // підінтегральна функція 1-го інтеграла
{
return sgrt (1 +log (x));
}
float fn2 ( float x ) // підінтегральна функція 2-го інтеграла
{
return log (1 +pow (x,2)) / (1+pow (x,2));
}
float ft (int n, float a ,float b , float fun( float )) // функція методу трапецій
{
int i;
float s1 ,h, s=0;
do
{ s1 =s;
h = (b -a ) /n;
s = ( fun (a) +fun (b) ) / 2;
for ( i = 1; i <= n-1; i++)
s += fun ( a + i *h );
s *= h; n *= 2; }
while ( fabs ( s-s1 ) > e );
return s; }
main ( )
{
float y;
y = ft ( 20 , 2 , 2, 3.0, fn1 ) +ft ( 20, 0, 1.0, fn2 );
cout << "y=" << y << endl;
}
Результат виконання програми :
y = 1.29012
Звертання до функції й передача результату
Для звертання до функції досить написати її ім'я й у дужках записати значення або перелік фактичних аргументів. Фактичні аргументи повинні бути записані в тій же послідовності, що й формальні й мати відповідний тип (крім аргументів за замовчуванням і перевантаженими функціями).
Якщо формальними аргументами функції є параметри значення й у ній не використаються глобальні змінні, то функція може передати в зухвалу програму лише одне значення, що записується в операторі return . Це значення передається в крапку виклику функції. Фрагмент програми , що підтверджує вищевикладене може мати вигляд :
double sgr (double);
main( )
{
cout << "Квадрат числа=" << sgr ( 10 ) << endl;
}
double sqr (double p)
// Повернення за значенням
{
return p*p;
}
У результаті буде виведено:
Квадрат числа = 100.
Використовуючи оператор return можна також організувати достроковий вихід з функції.
Якщо з функції треба передати не одне, а кілька значень, то можна або використати глобальні змінні, або масиви.
Ім'я функції це константа - покажчик на функцію, що вказує на адресу крапки входу (адреса першої машинної команди) функції. Слід зазначити, що можливо також опис і покажчиків змінних на функції. Для цього використається операція розкриття "*", функції описані в такий спосіб повинні повертати покажчик на тип, тобто його адреса.
У С++ визначено кілька способів передачі й повернення результату обчислень, найбільше широко використовуваними є розглянуті раніше:
виклик функції з передачею параметрів за допомогою формальних аргументів - значень;
виклик функції з передачею адрес за допомогою параметрів - покажчиків;
виклик функцій з використанням посилань, коли доступ до переданих параметрів забезпечується за допомогою альтернативного імені (синоніма);
виклик функцій з передачею даних за допомогою глобальних змінних, наприклад:
#include < iostream.h >
int a, b, c;
sum ( )
main ( )
{
cin >> a >> b;
sum ();
cout << c << endl;
}
sum( )
{ c = a + b ; }
виклик функцій з використанням параметрів, заданих по умолча-нию, при цьому можна використати або всі аргументи, або їхня частина.
При виклику функції, у якої аргументи задані "за замовчуванням", дотримується таке правило : якщо який - або аргумент використається за замовчуванням, те й всі наступні аргументи повинні бути використані зі значеннями за замовчуванням.
Приклад Обчислити квадратну функцію загального виду для заданого значення аргументу x з використанням функції, у яку коефіцієнти a, b і c можуть бути уведені за замовчуванням.
// P5_5.CPP ( обчислення квадратної функції
// коефіцієнти квадратної залежності можуть
// уводитися у функцію "за замовчуванням"
#include < iostreаm.h >
float ur ( float x, float а=0., floatb b =0.,float c=0.);
int main ( )
{
float а =1., b = 2., c = 3., x = 0.5, y;
cout << " Уведені всі аргументи " << endl;
y = ur ( x, a, b, c);
cout << " y = "<< y << endl;
cout << " Уведені аргументи x, a, b " << endl;
y =ur ( x, a, b);
cout << " y = "<< y << endl;
cout << " Уведений аргумент x " << endl;
y =ur ( x );
cout << " y = "<< y << endl;
}
// Функція обчислення квадратної функції
float ur ( float x, float a, float b, float c );
{
return a * x * x + b * x + c;
}
Результати виконання програми:
Уведено всі аргументи
y = 4.25
Уведені x, a і b
y = 1.25
Уведено аргумент x
y = 0
У мові C++ можна використати так називане перевантаження функцій, тобто можливість визначати функції з тим самим ім'ям, але різним типом і кількістю формальних параметрів.
Список формальних аргументів називають сигнатурою функції.
Основне достоїнство перевантажених функцій, це можливість визначати кілька функцій з тим самим ім'ям, але з різними типами й числом параметрів.
Приклад Зробити програмну реалізацію з використанням перевантаження функцій.
#include <iostream .h>
#include <string.h>
int funp (int x) // 1.
{ return x*x; }
int funp (unsigned x) // 2.
{ return -x*x; }
char funp (char x) // 3.
{ return x+3; }
int funp (int x, char *y) // 4.
{ return x*strlen ( y); }
int funp (int x , char y) // 5.
{ return x*y; }
float funp (float r) // 6.
{ return r*r; }
float funp (double r) // 7.
{ return r+r; }
main ( )
{
cout << funp(5) << endl ; // Буде 25
cout << funp ((unsigned)10) << endl ; // Буде 100
cout << funp ( 'a') << endl ; // Буде d
cout << funp (4,"abc") << endl ; // Буде 12
cout << funp ( 4 , 'a') << endl ; // Буде 388
cout << funp( (float5) 1.2)<< endl ; // Буде 1.44
cout << funp( double) 4.5) << endl ; // Буде 9
return 0;
}
У заголовку функції завжди вказується тип її імені, а в самій підпрограмі цьому імені повинне бути привласнене значення, що повертається в головну програму замість звертання до функції.
Лекція 10
Тема : Структури, створення власних типів даних
Мета: Ознайомитися з тезнологією створення власних типів. Вивчити синтаксис створення структур. Навчитися розробляти програми зі структами.
Структура - це сукупність різнотипних елементів, яким привласнюється одне ім'я ( воно може бути відсутнім), що займає одну область пам'яті. Елементи структури називаються полями.
Як і будь-яка змінна, структурна змінна повинна бути описана. Цей опис складається із двох кроків: опис шаблона (тобто состава ) або типу структури й опису змінних структурного типу.
Синтаксис опису структури має вигляд:
struct < ім'я структури >
{ <тип 1 > ім'я поля 1;
< тип 2 > ім'я поля 2. . . . . . . .;
} р1, р2. . . .;
де struct службове слово;
< ім'я структури > ім'я типу структури ;
<тип 1>, <тип 2> імена стандартних або визначених типів;
ім'я поля 1, ім'я поля 2, … імена полів структури;
р1, р2. . . .; - імена змінних типу структура.
Приклад Обробити дані про здачу студентами сесії по предметах: математиці, фізиці й програмуванні, підрахувати середній бал, отриманий студентами по цим курсам.
Визначимо структуру:
struct stud
{ char fam [25]; // прізвище й ініціали
int mat, fiz, prg; // предмети
float sb; // середній бал
} st1,st2;
Змінні st1 і st2 можна оголосити окремим оператором, наприклад:
struc stud st1 st2;
Ініціалізація полів структури може вироблятися або при її описі, або в тілі програми. При описі структури ініціалізація полів може виглядати так:
struct stud
{ char fam [25];
int mat,fiz, prg;
float sb;
}
st1 = { " Кравченко И.С.", 4, 5, 5};
st2 = { "Тесленко А.М.", 3, 4, 5};
Якщо ініціалізація полів виробляється в тілі програми, то для звертання до імен полів треба спочатку записати ім'я структурної змінної, а потім ім'я поля, що відокремлюється крапкою ( складові поля). Отже, у випадку , коли змінна st1 оголошується в програмі, для її ініціалізації можна записати stud . st1 = {"Кравченко И. С. ",4 ,5 ,5); або ініціалізація здійснюється за допомогою складових полів, як представлено в наступній програмі:
// Р 4_1. СPP - визначення середнього бала
// використання складових полів
#include < iostream.h >
#include < string.h >
#include < stdio.h >
main ( )
{
struct stud
{ char fam [20];
int mat, fiz, prg;
float sb;
} st1, st2;
strcpy (st1. fam, "Кравченко И.С.");
st1 . mat = 4;
st1. fiz = 5;
st1. prg = 5;
st1. sb = (st1. fiz + st1. mat + st1. prg) / 3;
st2 = st1;
puts (st2. fam); // Вивід прізвища st2;
cout << st2. mat << st2. fiz << st2. prg << st2. sb << endl;
return 0;
}
У цій програмі всім полям структури st1 привласнені відповідні значення. Треба звернути увагу, що поле st1. fam одержує значення шляхом використання функції strcpy (st1.fam, "Кравченко И. С. ");. Структурна змінна st2 має ту ж структуру, що й st1, тому справедливо операцію st2 = st1; .
Якщо в одній функції використається тільки один структурний тип, то цей тип можна повідомляти без імені. Наприклад, раніше розглянуту структуру можна оголосити в такий спосіб:
struct
{ char fam [25];
int mat, fiz, prg;
float sb;
} st1, st2;
Якщо при описі структур у деякій функції або в межах " видимості" змінних у різних функціях є багато (але не все) однакових полів, то ці однакові поля можна об'єднати в окрему структуру й використати неї при описі інших структур, тобто поля структури можуть самі мати тип "структура". Це називається вкладеністю структур. Наприклад, якщо треба обробляти списки студентів і викладачів університету, причому в студентських списках утримуються дані: прізвище й ініціали, дата ( день, місяць, рік) народження, група й середній бал успішності, а в списках викладачів обробляються такі дані: прізвище й ініціали, дата народження, кафедра, посада.
Для обробки списку студентів і викладачів можна оголосити наступні структури:
struct stud
{ char fio [25];
int den,god;
char mes [10];
char grup;
float sb;
}
и
struct prep
{ char fio [25];
int den, god;
char mes [10];
char kaf, dolg;
}
В оголошених типах однакові поля має сенс включити в окрему структуру й використати неї при описі інших типів. Поетапно це виглядає так:
загальна структура:
struct spd
{ char fio [25];
int den,god;
char mas[10];
}
структура для опису інформації про студентів:
struct stud
{ spd dr;
char grup;
float sb
} st1,st2;
структура для опису інформації про викладачів:
struct prep
{ spd dr;
char kaf [10] ;
char dolg [15];
} pr1,pr2;
У структурах stud і prep для оголошення поля, що містить дані про прізвище й дату народження використається раніше описаний тип spd. Тепер до поля fio, den, god, mes можна звернутися, використовуючи запис наступного виду:
st1 . dr . fio
Наприклад, при записі функції уведення gets (st1 . dr . fio); або pr1 . dr . fio.
Після оголошення структурного типу змінних, для роботи з їхніми полями можна використати й покажчики, тоді опис структури буде мати вигляд:
struct stud
{ char fam [25];
int mat, fiz, prg;
float sb;
} st1, *pst;
Тепер доступ до полів може здійснюватися двома способами :
використовуючи оператор разыменования (*) , наприклад,
gets ((*pst) . fam); (*pst) . fiz = 5;
використовуючи спеціальний покажчик на структуру "->", наприклад,
gets ( pst -> fam); pst -> fiz = 5; і т.д.
Крім того до змінного st1 можна звертатися, указуючи поля через символ крапка, як це робилося раніше.
Дані структурного типу можна об'єднати в масиви, наприклад, використовуючи раніше розглянуту структуру можна записати:
struct stud
{ char fam [25];
int mat, fiz, prg;
float sb;
} spis[15], *sp = &spis[0];
або, якщо масив описується не при описі структури, те його можна оголосити у вигляді:
stud spis [15]; .
Доступ до елементів масиву може виконаються з використанням індексу або через покажчик - константу, яким є ім'я масиву:
strcpy( spis [1] . fam, " ");
spis [1] . fiz = 5;
або
strcpy ((sp +1) -> fam, " ");
(sp + 1) -> fiz = 5;
Це можна записати також у вигляді:
strcpy ((* (spis +1 )) . fam, " ");
(*(spis+1)) . fiz = 5;
Тут потрібна зовнішня пара дужок, тому що операція (.) " крапка" має пріоритет вище, ніж операція за адресою (*).
Розглянемо використання даних структурного типу гна наступному прикладі.
Приклад Увести в комп'ютер відомість успішності студентів групи з 25 чоловік при здачі сесії по предметах: фізиці, математиці, і програмуванню й підрахувати:
середній бал, отриманий кожним студентом;
середній бал групи по кожному предметі;
вивести на екран відмінників по програмуванню.
Програма буде мати вигляд:
// P4_2. CPP ( обробка відомості успішності
// Використання даних типу структура
#include < iostream.h >
#include < string.h >
#include < stdio.h >
#include < iomanip.h >
void main(void)
{
const k = 5;
int n = 1, i;
float sm, sf, sp;
struct stud
{ char fam[25];
int mat, fiz, prg;
float sb;
} ved[k];
sm = sf = sp = 0;
cout << "Уведіть фио й оцінки mat, fiz, prg \n " ;
for ( i = 0; i < k; i++)
{
gets ( ved[i]. fam );
cin >> ved[i]. mat >> ved[i]. fiz >> ved[i]. prg ;
ved[i]. sb = ( ved[i]. mat + ved[i]. fiz + ved[i]. prg) / 3;
sm += ved[i]. mat;
sf += ved[i]. fiz;
sp += ved[i]. prg;
}
cout << "\t Відомість успішності групи \n\n";
cout << "\t";
for ( i = 0; i < 47; i++) cout << "-"; // Верхня риса
cout << "\n";
cout << "\t \t ФИО\t мат физ прогр порівн.бал \n";
for ( i = 0; i < 47; i++) cout << "-"; // Верхня риса
for ( i = 0; i < k; i++)
{
cout << "\t" << i + 1 << " " << setw(17)
<< setiosflags ( ios :: left ) << ved[i]. fam;
cout << ved[i]. mat << " " << ved[i]. fiz <<
" " << ved[i]. prg << " " << ved[i]. sb << "\n";
}
cout << "\t";
for ( i = 0; i < 47; i++) cout << "-"; // Нижня риса
cout << "\n\t" << setw(16) << setiosflags ( ios :: right )
<< "Порівн. бал";
cout << " " << sm / k << " " << sf / k << " " << sp / k << "\n\n";
cout << "\t Відмінники по програмуванню: \n\n";
for ( i = 0; i < k; i++)
if ( ved[i]. Prg == 5)
{
cout << "\t" << n << " " << ved[i]. fam << "\n";
n++;
}
}
Результат роботи програми буде мати вигляд:
Відомість успішності студентів
_______________________________________________
фио мат физ прогр порівн.бал
_______________________________________________
Авдєєв И.М. 3 4 4 3.7
Биків Т.Б. 5 5 4 4.6
Волков А.П. 4 5 5 4.6
_______________________________________________
Порівн. бал 3.8 4.6 4.8
Відмінники по програмуванню:
Волков А.П.
Поля структури можуть також бути масивами, наприклад, у раніше розглянутій структурі stud можна оцінки по різних предметах об'єднати в масив. Тоді таку структуру можна описати у вигляді:
struct stud1
{ char fam [25];
int pr [3];
float sb
} st1 [10], *pst = &st1 [0]; ,
тепер до полів можна звертатися одним з наступних способів:
((*pst). fam) // gets (( *pst) .fam);
( pst -> pr [0] ) // cin >> pst -> pr[0] >> pst -> pr[1];
// або cin >> pst -> * (pr +1).
У бібліотеці <stdlib.h> для пошуку й сортування структурних змінних є спеціальні функції. Так , функція швидкого сортування структурних елементів масиву по заданому полю має вигляд :
#include < stdlib.h >
void qsort (void *base , n , width,
int (* fcmp ) (const void *elem 1, const void -> elem2)) ,
де base покажчик на перший елемент масиву;
n кількість елементів масиву;
width -довжина елементів масиву в бітах.
При виконанні сортування функція qsort ( ) звертається до заданого користувачем функції.
Функція задається покажчиком на функцію:
int (*fcmp) ( const void *elem1, const void *elem2) .
Функція, на яку вказує fcmp , виконує порівняння двох елементів масиву, на які вказує elem1 і elem2.
Функція повинна повертати значення:
< 0, якщо *elem1 < *elem2;
= 0, якщо *elem1 = *elem2;
> 0, якщо *elem1 > *elem2;
Якщо *elem1 > *elem2 , то елемент, на який указує elem1 розташовується в масиві раніше; "колишній" елемент розташовується в упорядкованому масиві пізніше.
Користувач задає порівнювані поля структурних змінних. Він також може змінити порядок, у якому будуть упорядковуватися елементи, що досягається зміною знака значень, що повертають із функції, на протилежні; може змінити поля, по яких виконується сортування.
У бібліотеці <stdlib.h> є також функція пошуку змінних структурного типу в масиві цих змінних.
Лекція 11, 12
Тема : Рядкові дані. Функції обробки даних.
Мета: Вивчення та закріплення знанб на базі рішення задач з рядковими даними. Придбання навичок по розробці програм з рядковими даними.
Рядки в с + + дозволяють нам працювати з символьними даними. Завдяки ним ми можемо читати з клавіатури текст , якось його обробляти і потім , наприклад , знову його виводити в консоль.
У С + + існує 2 типу рядків . Перший з них - це масив змінних типу char .
Якщо хто не пам'ятає , то змінна типу char зберігає в собі 1 символ . Розмір такого рядка дорівнює розміру масиву - 1, тому останній елемент містить NULL (порожній мінлива без значення ) , який позначає символ кінця рядка.
наприклад:
char name [50] ;
cin >> name ;
cout << " Hello " << name << endl ;
Другий з варіантів , більш зручний - це спеціальний клас string
Для його роботи необхідно на початку програми підключити заголовний файл string :
# include <string>
У відмінності від типу char , string є класом . Більш докладно про класах я розповім пізніше , зараз вам досить знати , що класи містять у собі відразу кілька речей: змінні , константи та функції для роботи із змінними. Це досить грубе пояснення , але на перший час вам вистачить.
Для створення рядка вам необхідно на початку програми написати using namespace std ;
Тепер щоб створити рядок досить написати:
string s ;
Для запису в рядок можна використовувати оператор =
s = " Hello " ;
Приклад роботи з класом string :
string name ;
cout << " Enter your name " << endl ;
cin >> name ;
cout << " Hi " << s << " ! " << endl ;
Але поки ви скористалися тільки однієї принадністю рядків : відсутністю необхідності задавати її розмір. Але крім цього існує безліч функцій для роботи з рядками .
s.append ( str ) - додає в кінець рядка рядок str . Можна писати як s.append (змінна ) , так і s.append ( "рядок " ) ;
s.assign ( str ) - присвоює рядку s значення рядка str . Аналогічно запису s = str ;
int i = s.begin ( ) - записує в i індекс першого елемента рядки
int i = s.end ( ) - аналогічно , але останнього
s.clear ( ) - як випливає з назви , відчищає рядок. Тобто видаляє всі елементи в ній
s.compare ( str ) - порівнює рядок s з рядком str і повертає 0 у випадку збіг (насправді порівнює коди символів і повертає з різницю)
s.copy (куди , скільки , починаючи з якого) - копіює з рядка s в куди ( там може бути як рядок типу стринг , так і рядок типу char ) . Останні 2 параметри необов'язкові (можна використовувати функцію з 1,2 або 3 параметрами )
bool b = s.empty ( ) - якщо рядок порожній , повертає true , інакше false
s.erase (звідки , скільки) видаляє n елементів із заданою позиції
s.find ( str , позиція ) - шукає рядок str починаючи з заданої позиції
s.insert ( позиція , str , починаючи , beg , count ) - вставляє в рядок s починаючи з заданої позиції частина рядка str починаючи з позиції beg і вставляючи count символів
int len= s.length ( ) - записує в len длинну рядка
s.push_back ( symbol ) - додає в кінець рядка символ
s.replace ( index , n , str ) - бере n перших символів з str і замінює символи рядка s на них , починаючи з позиції index
str = s.substr ( n , m ) - повертає m символів починаючи з позиції n
s.swap ( str ) змінює вміст s і str місцями.
s.size ( ) - повертає число елементів в рядку.
Ось власне більшість необхідних функція для роботи з рядками в с + +. Набір досить непоганий , більше вам поки не знадобиться
Тепер трохи прикладів і потім практика . Постарайтеся самі зрозуміти , що робить кожен приклад.
string name , surname , text , fullname , s1 , s2 , s3 , user ;
user = " Petya Petrov " ;
cout << " Enter your name " << endl ;
cin >> name ;
cout << " Enter your surname " << endl ;
cin >> surname ;
fullname = name ;
fullname + = "" ; / / додаємо пробіл
fullname.append ( surname ) ;
if ( fullname.compare ( user ) == 0) / / < => if (! ( fullname.compare ( user )))
cout << " Your are good user " << endl ;
else
cout << " Bad user " << endl ;
cout << " enter s1 " << endl ;
cin >> s1 ;
cout << " enter s2 " << endl ;
cin >> s2 ;
s1.swap ( s2 ) ;
cout < " new s1 : " << s1 << endl << " new s2 : " << s2 << endl ;
cout << " Enter big text with your name " << endl ;
cin >> text ;
int i = 0 ;
i = text.find ( " name " ) ;
while ( i! = -1 )
{text.replace ( i , name.length ( ) , name ) ;
s3 = text.substr ( i , name.length ( )) ;
cout << " Replaced : " << s3 << endl ;
i = text.find ( " name " ) ;
}cout << " New text : " << endl << text << endl ;
text.clear ();
cout << " text : " << text << endl ;
Задачі для рядків типу char :
Підрахувати кількість символів з рядку (рядок кінчається елементом 0 : c = 0 if ( c == 0) cout << " end " << endl ;
Вам вводять рядок , потім вводять підрядок . Якщо підрядок є в введеної рядку вивести так, інакше немає
Вводять 3 рядки: а, б , с. Замінити у рядку з рядок а на рядок б
Задачі для рядків типу string :
Вводять текст і вводять підрядок , знайти всі входження підрядок в текст
Вводять текст і два слова , замінити всі слова 1 на слова 2 і всі слова 2 на слова 1
Вводимо ім'я і текст , вивести всі входження імені в текст ( ім'я + 5 символів до і 5 символів після нього)
Вводять 2 тексту. Порівняти їх , об'єднати , вивести всі прогалини , крапки, коми , двокрапки . Потім вивести розмір кожного тексту і загальний розмір . Потім поміняти всі букви а на a (росіяни на латинські ) і до на k . Потім вивести кількість замін.
Задачі для практичної частини
Лекція 13, 14
Тема :Поняття потоку. Файловий потік. Робота з файлами.
Мета: Вивчити базовы принципи роботи з файловими потоками. Навчитися розробляти програми з файлами.
C + + надає набір класів файлових потоків , за допомогою яких можна дуже легко виконувати операції введення та виведення ( В / В) з файлами.Використовуючи вихідний файловий потік , ви можете писати інформацію в файл за допомогою оператора вставки ( << ) .Використовуючи вхідний файловий потік, ви можете читати збережену у файлі інформацію за допомогою оператора вилучення ( >> ) (запис даних у файл )
Заголовний файл fstream.h визначає клас вихідного файлового потоку з ім'ям ofstream. Використовуючи об'єкти класу ofstream, ваші програми можуть виконувати висновок у файл. Для початку ви повинні підключити заголовний файл fstream.h і оголосити об'єкт типу ofstream , вказавши ім'я необхідного вихідного файлу як символьний рядок , що показано нижче:
ofstream file_object ( " FILENAME.EXT " ) ;
Якщо ви вказуєте ім'я файлу при оголошенні об'єкту типу ofstream , C + + створить новий файл на вашому диску , використовуючи вказане ім'я , або перезапішет файл з таким же ім'ям , якщо він вже існує на вашому диску
Наступна програма створює об'єкт типу ofstream і потім використовує оператор вставки для виведення декількох рядків тексту в файл BOOKINFO.DAT :
# include <fstream.h>
int main ( void )
{
ofstream book_file ( " BOOKINFO.DAT " ) ;
book_file << " Вчимося програмувати на мові C + +" << " П73 " << endl ;
book_file << " Карантин " << endl ;
book_file << " ХПКК " << endl ;
return 0 ;
}
У даному випадку програма відкриває файл BOOKINFO.DAT і потім записує три рядки в файл , використовуючи оператор вставки. Відкомпілюйте і запустіть цю програму. Відкрийте файл BOOKINFO.DAT засобами операційної системи і перегляньте вміст.
Читання з вхідного файлового потоку (читання даних з файлу )
Операції введення з файлу , використовуючи об'єкти типу ifstream . Знову ж , ви просто створюєте об'єкт , передаючи йому як параметр потрібного імені файлу:
ifstream input_file ( " filename.TXT " ) ;
Наступна програма відкриває файл BOOKINFO.DAT , який ви створили за допомогою попередньої програми , і читає , а потім відображає перші три елементи файлу:
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
ifstream input_file ( " BOOKINFO.DAT " ) ;
char one [ 64 ] , two [ 64 ] , three [ 64 ] ; / / три рядки
input_file >> one ;
input_file >> two ;
nput_file >> three ;
cout << one << endl ;
cout << two << endl ;
cout << three << endl ;
}
Якщо ви откомпіліруете і запустіть цю програму , то , ймовірно , припустіть, що вона відобразить перші три рядки файлу. Однак , подібно cin , вхідні файлові потоки використовують пробіл , щоб визначити , де закінчується одне значення і починається інше. В результаті при запуску попередньої програми на дисплеї з'явиться наступний висновок:
Вчимося
Програмувати
На
Читання цілого рядка файлового введення (читання рядки) cin.getline використовується для читання цілого рядка з клавіатури , подібним чином об'єкти типу ifstream можуть використовувати getline для читання рядки файлового введення . Наступна програма FILELINE.CPP використовує функцію getline для читання всіх трьох рядків файлу BOOKINFO.DAT :
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
ifstream input_file ( " BOOKINFO.DAT " ) ;
сhar one [ 64 ] , two [ 64 ] , three [ 64 ] ;
input_file.getline ( one , sizeof ( one )) ;
input_file.get line ( two , sizeof ( two )) ;
input_file.getline ( three , sizeof ( three )) ;
cout << one << endl ;
cout << two << endl ;
cout << three << endl ;
}
У даному випадку програма читає вміст файлу , тому що вона знає , що файл містить три рядки . Однак у багатьох випадках ваша програма не буде знати , скільки рядків міститься у файлі. У таких випадках просто треба продовжувати читання вмісту файлу поки не зустрінеться кінець файлу. Визначення кінця файлу Звичайною файлової операцією є читання вмісту файлу , поки не зустрінеться кінець файлу. Щоб визначити кінець файлу , треба використовувати функцію еоf () потокового об'єкта. Ця функція повертає значення 0 , якщо кінець файлу ще не зустрів , і 1 , якщо зустрівся кінець файлу. Використовуючи цикл while , програми можуть безперервно читати вміст файлу , доки не знайдуть кінець файлу , як показано нижче:
while (! input_file.eof ( ))
{
/ / Оператори читання даних з файлу як в попередніх прикладах
}
У даному випадку програма буде продовжувати виконувати цикл , поки функція eof () повертає брехня ( 0 ) .
Наступна програма TEST_EOF.CPP використовує функцію eof ( ) для читання вмісту файлу BOOKINFO.DAT , поки не досягне кінця файлу:
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
ifstream input_file ( " BOOKINFO.DAT " ) ;
char line [ 64 ] ;
while (! input_file.eof ( ))
{
input_file.getline ( line , sizeof ( line )) ;
cout << line << endl ;}}
Аналогічно , наступна програма WORD_EOF.CPP читає вміст файлу по одному слову за один раз , поки не зустрінеться кінець файлу:
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
ifstream input_file ( " BOOKINFO.DAT " ) ;
char word [ 64 ] ;
while (! input_file.eof ( ))
{
input_file >> word ;
cout << word << endl ;
}
}
І нарешті , наступна програма CHAR_EOF.CPP читає вміст файлу по одному символу за один раз , використовуючи функцію get , поки не зустріне кінець файлу:
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
ifstream input_file ( " BOOKINFO.DAT " ) ;
char letter ;
while (! input_file.eof ( ))
{
letter = input_file.get ();
cout << letter ;
}
}
Перевірка помилок при виконанні файлових операцій
Програми , представлені до теперішнього моменту , припускали , що під час файлових операцій В / В не відбуваються помилки. На жаль , це збувається не завжди. Наприклад , якщо ви відкриваєте файл для введення , ваші програми повинні перевірити , що файл існує. Аналогічно , якщо ваша програма пише дані в файл , вам необхідно переконатися , що операція пройшла успішно (наприклад , відсутність місця на диску , швидше за все , перешкодить записи даних). Щоб стежити за помилками , необхідно використовувати функцію fail ( ) файлового об'єкта. Якщо при виконанні файлової операції помилок не було , функція поверне брехня ( 0 ) . Однак , якщо зустрілася помилка , функція fail ( ) поверне істину (1). Наприклад , якщо програма відкриває файл , слід використовувати функцію fail (), щоб визначити , чи відбулася помилка , як це показано нижче:
fstream input_file ( " FILENAME.DAT " ) ;
if ( input_file.fail ( ))
{
cerr << " Помилка відкриття FILENAME.EXT " << endl ;
exit ( 1 ) ;
}
Наступна програма TEST_ALL.CPP використовує функцію fail ( ) для перевірки різних помилкових ситуацій :
# include <iostream.h>
# include <fstream.h>
void main ( void )
{
char line [ 256 ] ;
ifstream input_file ( " BOOKINFO.DAT " ) ;
if ( input_file.fail ( )) cerr << " Помилка відкриття BOOKINFO.DAT " << endl ;
else {
while ( (! input_file.eof ( )) && (! input_file.fail ( )))
{
input_file.getline ( line , sizeof ( line )) ;
if (! input_file.fail ( )) cout << line << endl ;
}
}
}
Закриття файлу , якщо він більше не потрібен. При завершенні програми операційна система закриє відкриті нею файли. Однак , як правило , якщо вашій програмі файл більше не потрібен , то його треба закрити . Для закриття файлу ваша треба використовувати функцію close ( ) , як показано нижче:
input_file.close ();
Коли ви закриваєте файл , всі дані , які ваша програма писала в цей файл , скидаються на диск , і оновлюється запис каталогу для цього файлу.
Управління відкриттям файлу
У прикладах програм , представлених в даній лекції , файлові операції введення і виведення виконувалися з початку файлу. Однак, коли ви записуєте дані у вихідний файл , ймовірно , ви захочете , щоб програма додавала інформацію в кінець існуючого файлу. Для відкриття файлу в режимі додавання ви повинні при його відкритті вказати другий параметр , як показано нижче:
ifstream output_file ( " FILENAME.EXT " , ios :: app ) ;
В даному випадку параметр ios :: app вказує режим відкриття файлу.
В міру ускладнення програм вони будуть використовувати поєднання значень для режиму відкриття файлу , які перераховані нижче
ios :: app - відкриває файл в режимі додавання , розташовуючи файловий покажчик в кінці файлу .
ios :: ate - розташовує файловий покажчик в кінці файлу .
ios :: in - вказує відкрити файл для введення .
ios :: nocreate - якщо зазначений файл не існує , не створювати файл і повернути помилку.
ios :: noreplace якщо файл існує , операція відкриття повинна бути перервана і повинна повернути помилку.
ios :: out - Вказує відкрити файл для виводу.
ios :: trunc - Скидає ( перезаписує ) утримуємо , з існуючого файлу.
Наступна операція відкриття файлу відкриває файл для виводу , використовуючи режим ios :: noreplace , щоб запобігти перезапис існуючого файлу:
ifstream output_file ( " FIlename.EXT " , ios :: out | ios :: noreplace ) ;
Питання для закріплення знань.
Лекція 15
Тема : ООП, поняття класу, інкапсуляція, поліморфізм, успадкування.
Мета: Вивчення основ ООП та базових принципів. Придбання приктичних навичок по робробці базових класів.
ООП виникло в результаті розвитку ідеології процедурного програмування , де дані і підпрограми ( процедури , функції) їх обробки формально не пов'язані. Для подальшого розвитку об'єктно -орієнтованого програмування часто велике значення мають поняття події ( так зване подієво - орієнтоване програмування) і компонента ( компонентне програмування , КОП ) .
Формування КОП від ООП сталося , як сталося формування модульного від процедурного програмування: процедури сформувалися в модулі - незалежні частини коду до рівня збірки програми , так об'єкти сформувалися в компоненти - незалежні частини коду до рівня виконання програми. Взаємодія об'єктів відбувається за допомогою повідомлень. Результатом подальшого розвитку ООП , мабуть , буде агентно- орієнтоване програмування , де агенти - незалежні частини коду на рівні виконання . Взаємодія агентів відбувається за допомогою зміни середовища , в якій вони знаходяться.
Мовні конструкції , конструктивно не відносяться безпосередньо до об'єктів , але супутні їм для їх безпечної ( виняткові ситуації , перевірки) та ефективної роботи , инкапсулируются від них у аспекти ( в аспектно- орієнтованому програмуванні ) . Суб'єктно - орієнтоване програмування розширює поняття об'єкт за допомогою забезпечення більш уніфікованого і незалежного взаємодії об'єктів . Може бути перехідною стадією між ООП і агентного програмуванням в частині самостійного їх взаємодії .
Першою мовою програмування , в якому були запропоновані принципи об'єктної орієнтованості , була Симула . У момент своєї появи ( в 1967 році ) , ця мова програмування запропонував воістину революційні ідеї: об'єкти , класи , віртуальні методи та ін , однак це все не було сприйнято сучасниками як щось грандіозне. Тим не менше, більшість концепцій були розвинені Аланом Кейем і Деном Інгаллс в мові Smalltalk . Саме він став першим широко поширеним об'єктно - орієнтованою мовою програмування.
В даний час кількість прикладних мов програмування (список мов) , що реалізують об'єктно - орієнтовану парадигму , є найбільшим по відношенню до інших парадигм . В області системного програмування досі застосовується парадигма процедурного програмування , і загальноприйнятою мовою програмування є мова C. Хоча при взаємодії системного та прикладного рівнів операційних систем помітний вплив стали надавати мови об'єктно - орієнтованого програмування. Наприклад , однією з найбільш поширених бібліотек мультиплатформенного програмування є об'єктно -орієнтована бібліотека Qt , написана мовою C + +.
Абстакція
Абстрагування - це спосіб виділити набір значущих характеристик об'єкта , виключаючи з розгляду незначущі . Відповідно, абстракція - це набір всіх таких характеристик. [ 1 ]
Інкапсуляція
Інкапсуляція - це властивість системи , що дозволяє об'єднати дані і методи , що працюють з ними в класі , і приховати деталі реалізації від користувача. [ 1 ]
Спадкування
Спадкування - це властивість системи , що дозволяє описати новий клас на основі вже існуючого з частково або повністю позичає функціональністю. Клас , від якого здійснюється спадкування , називається базовим , батьківським або суперкласом . Новий клас - нащадком , спадкоємцем або похідним класом. [ 1 ]
Поліморфізм
Поліморфізм - це властивість системи використовувати об'єкти з однаковим інтерфейсом без інформації про тип і внутрішній структурі об'єкта. [ 1 ]
Клас
Клас є описуваної на мові термінології ( простору імен ) вихідного коду моделлю ще неіснуючій сутності ( об'єкта) . Фактично він описує пристрій об'єкта , будучи свого роду кресленням. Кажуть , що об'єкт - це екземпляр класу. При цьому в деяких виконуючих системах клас також може представлятися деяким об'єктом при виконанні програми за допомогою динамічної ідентифікації типу даних. Зазвичай класи розробляють таким чином , щоб їх об'єкти відповідали об'єктам предметної області.
Об'єкт
Сутність в адресному просторі обчислювальної системи , що з'являється при створенні екземпляра класу або копіювання прототипу (наприклад , після запуску результатів компіляції і зв'язування вихідного коду на виконання ) .
Прототип
Прототип - це об'єкт -зразок , за образом і подобою якого створюються інші об'єкти. Об'єкти - копії можуть зберігати зв'язок з батьківським об'єктом , автоматично наслідуючи зміни в прототипі ; ця особливість визначається в рамках конкретної мови.
Визначення ООП і його основні концепції
У центрі ООП перебуває поняття об'єкта. Об'єкт - це сутність , якою можна посилати повідомлення , і яка може на них реагувати , використовуючи свої дані. Об'єкт - це екземпляр класу. Дані об'єкта приховані від решти програми . Приховування даних називається инкапсуляцией .
Наявність інкапсуляції достатньо для об'єктності мови програмування , але ще не означає його об'єктної орієнтованості - для цього потрібна наявність успадкування.
Але навіть наявність інкапсуляції і спадкування не робить мову програмування повною мірою об'єктним з точки зору ООП. Основні переваги ООП виявляються тільки в тому випадку , коли в мові програмування реалізований поліморфізм ; тобто можливість об'єктів з однаковою специфікацією мати різну реалізацію.
ООП має вже більш ніж сорокарічну історію , але , незважаючи на це , досі не існує чіткого загальноприйнятого визначення даної технології [ 2 ] . Основні принципи, закладені в першій об'єктні мови і системи , піддалися істотної зміни (або спотворення ) і доповненню при численних реалізаціях наступного часу . Крім того , приблизно з середини 1980 -х років термін « об'єктно -орієнтований » став модним , в результаті з ним сталося те ж саме , що дещо раніше з терміном « структурний » ( який став модним після поширення технології структурного програмування ) - його стали штучно « прикріплювати » до будь-яких нових розробок , щоб забезпечити їм привабливість. Бьерн Страуструп в 1988 році писав , що обгрунтування « об'єктної орієнтованості » чогось , в більшості випадків , зводиться до помилкового силогізму : « X - це добре. Об'єктна орієнтованість - це добре. Отже , X є об'єктно - орієнтованим ».
Роджер Кінг аргументовано наполягав , що його кіт є об'єктно - орієнтованим. Крім інших своїх достоїнств , кіт демонструє характерну поведінку , реагує на повідомлення , наділений успадкованими реакціями і управляє своїм , цілком незалежним , внутрішнім станом.
На думку Алана Кея , творця мови Smalltalk , якого вважають одним з «батьків -засновників» ООП , об'єктно- орієнтований підхід полягає в наступному наборі основних принципів ( цитується за вищезгаданій книзі Т. Бадда ) .
1 . Все є об'єктом.
2 . Обчислення здійснюються шляхом взаємодії ( обміну даними) між об'єктами , при якому один об'єкт вимагає , щоб інший об'єкт виконав деяку дію . Об'єкти взаємодіють , посилаючи і отримуючи повідомлення. Повідомлення - це запит на виконання дії , доповнений набором аргументів , які можуть знадобитися при виконанні дії .
3 . Кожен об'єкт має незалежну пам'ять , яка складається з інших об'єктів.
4 . Кожен об'єкт є представником класу , який висловлює загальні властивості об'єктів (таких , як цілі числа або списки).
5 . У класі задається поведінка ( функціональність ) об'єкта . Тим самим всі об'єкти, які є екземплярами одного класу , можуть виконувати одні й ті ж дії.
6 . Класи організовані в єдину деревовидну структуру із загальним коренем , звану ієрархією успадкування. Пам'ять і поведінка, пов'язана з екземплярами певного класу , автоматично доступні будь-якому класу , розташованому нижче в ієрархічному дереві.
Таким чином , програма являє собою набір об'єктів, що мають стан і поведінку. Об'єкти взаємодіють за допомогою повідомлень. Природним чином вибудовується ієрархія об'єктів : програма в цілому - це об'єкт , для виконання своїх функцій вона звертається до вхідних в неї об'єктів , які , в свою чергу , виконують запитане шляхом звернення до інших об'єктів програми . Природно , щоб уникнути нескінченної рекурсії у зверненнях , на якомусь етапі об'єкт трансформує звернене до нього повідомлення в повідомлення до стандартних системних об'єктів , що надаються мовою і середовищем програмування.
Стійкість і керованість системи забезпечується за рахунок чіткого поділу відповідальності об'єктів ( за кожну дію відповідає певний об'єкт ) , однозначного визначення інтерфейсів межоб'ектного взаємодії та повної ізольованості внутрішньої структури об'єкта від зовнішнього середовища ( інкапсуляції ) .
Визначити ООП можна і багатьма іншими способами.
Поява в ООП окремого поняття класу закономірно випливає з бажання мати безліч об'єктів з подібним поведінкою. Клас в ООП - це в чистому вигляді абстрактний тип даних , створюваний програмістом. З цієї точки зору об'єкти є значеннями даного абстрактного типу , а визначення класу задає внутрішню структуру значень і набір операцій , які над цими значеннями можуть бути виконані. Бажаність ієрархії класів ( а значить , успадкування) випливає з вимог до повторного використання коду - якщо кілька класів мають подібну поведінку , немає сенсу дублювати їх опис , краще виділити загальну частину в загальний батьківський клас, а в описі самих цих класів залишити тільки розрізняються елементи .
Необхідність спільного використання об'єктів різних класів , здатних обробляти однотипні повідомлення , вимагає підтримки поліморфізму - можливості записувати різні об'єкти в змінні одного і того ж типу. У таких умовах об'єкт , відправляючи повідомлення , може не знати в точності , до якого класу належить адресат , і одні й ті ж повідомлення , відправлені змінним одного типу , що містить об'єкти різних класів , викличуть різну реакцію.
Окремої пояснень потребує поняття обміну повідомленнями. Спочатку (наприклад, в тому ж Smalltalk ) взаємодія об'єктів уявлялося як «справжній» обмін повідомленнями , тобто пересилання від одного об'єкта іншому спеціального об'єкта -повідомлення. Така модель є надзвичайно загальною . Вона прекрасно підходить , наприклад , для опису паралельних обчислень за допомогою активних об'єктів , кожен з яких має власний потік виконання і працює одночасно з іншими. Такі об'єкти можуть вести себе як окремі , абсолютно автономні обчислювальні одиниці. Посилка повідомлень природним чином вирішує питання обробки повідомлень об'єктами , присвоєними поліморфним змінним - незалежно від того , як оголошується змінна , повідомлення обробляє код класу , до якого належить присвоєний змінної об'єкт.
Однак спільність механізму обміну повідомленнями має й інший бік - «повноцінна» передача повідомлень вимагає додаткових накладних витрат , що не завжди прийнятно. Тому в більшості нині існуючих об'єктно- орієнтованих мов програмування використовується концепція « відправка повідомлення як виклик методу» - об'єкти мають доступні ззовні методи , викликами яких і забезпечується взаємодія об'єктів. Даний підхід реалізований у величезній кількості мов програмування , в тому числі C + + , Object Pascal , Java , Oberon - 2 . На даний момент саме він є найбільш поширеним в об'єктно - орієнтованих мовах .
Концепція віртуальних методів , підтримувана цими та іншими сучасними мовами , з'явилася як засіб забезпечити виконання потрібних методів при використанні поліморфних змінних , тобто , по суті , як спроба розширити можливості виклику методів для реалізації частини функціональності , забезпечується механізмом обробки повідомлень.
Особливості реалізації
Як вже говорилося вище , в сучасних об'єктно- орієнтованих мовах програмування кожен об'єкт є значенням , що належать до певного класу. Клас являє собою оголошений програмістом складовою тип даних , що має в складі :
Поля даних
Параметри об'єкта ( звичайно , не всі , а тільки необхідні в програмі) , що задають його стан (властивості об'єкта предметної області) . Іноді поля даних об'єкта називають властивостями об'єкта , через що можлива плутанина . Фізично поля являють собою значення ( змінні , константи) , оголошені як належать класу .
Методи
Процедури і функції , пов'язані з класом. Вони визначають дії , які можна виконувати над об'єктом такого типу , і які сам об'єкт може виконувати.
Класи можуть успадковуватися один від одного. Клас - нащадок отримує всі поля і методи класу -батька , але може доповнювати їх власними або перевизначати вже наявні . Більшість мов програмування підтримує тільки одиничне успадкування (клас може мати тільки один клас-батько ) , лише в деяких допускається множинне спадкування - породження класу від двох або більше класів - батьків. Множинне успадкування створює цілий ряд проблем , як логічних , так і чисто реалізаційних , тому в повному обсязі його підтримка не поширена. Замість цього в 1990 -і роки з'явилося і стало активно вводитися в об'єктно - орієнтовані мови поняття інтерфейсу . Інтерфейс - це клас без полів і без реалізації , що включає тільки заголовки методів . Якщо якийсь клас успадковує (або , як кажуть , реалізує ) інтерфейс , він повинен реалізувати всі вхідні в нього методи . Використання інтерфейсів надає відносно дешеву альтернативу множинного спадкоємства .
Взаємодія об'єктів в абсолютній більшості випадків забезпечується викликом ними методів один одного.
Інкапсуляція забезпечується наступними засобами
контроль доступу
Оскільки методи класу можуть бути як чисто внутрішніми , що забезпечують логіку функціонування об'єкта , так і зовнішніми , за допомогою яких взаємодіють об'єкти , необхідно забезпечити прихованість перших при доступності ззовні другий . Для цього в мови вводяться спеціальні синтаксичні конструкції , явно задають область видимості кожного члена класу. Традиційно це модифікатори public , protected і private , що позначають , відповідно, відкриті члени класу , члени класу , доступні тільки з класів - нащадків і приховані , доступні тільки усередині класу. Конкретна номенклатура модифікаторів і їх точний зміст різняться в різних мовах.
методи доступу
Поля класу , в загальному випадку , не повинні бути доступні ззовні , оскільки такий доступ дозволив би довільним чином змінювати внутрішній стан об'єктів. Тому поля зазвичай оголошуються прихованими ( або мова в принципі не дозволяє звертатися до полів класу ззовні ) , а для доступу до знаходяться в полях даними використовуються спеціальні методи , звані методами доступу . Такі методи або повертають значення того чи іншого поля , або роблять запис в це поле нового значення. При записи метод доступу може проконтролювати допустимість записуваного значення і, при необхідності , провести інші маніпуляції з даними об'єкта , щоб вони залишилися коректними ( внутрішньо узгодженими ) . Методи доступу називають ще аксессор (від англ. Access - доступ) , а окремо - геттеров (англ. get - читання ) і сетерами (англ. set - запис).
властивості об'єкта
Псевдополя , доступні для читання і / або запису. Властивості зовні виглядають як поля і використовуються аналогічно доступним полях ( з деякими винятками) , однак фактично при зверненні до них відбувається виклик методів доступу . Таким чином , властивості можна розглядати як «розумні» поля даних , що супроводжують доступ до внутрішніх даних об'єкта -якими додатковими діями ( наприклад , коли зміна координати об'єкта супроводжується його перемальовуванням на новому місці). Властивості , по суті - не більше ніж синтаксичний цукор , оскільки ніяких нових можливостей вони не додають , а лише приховують виклик методів доступу . Конкретна мовна реалізація властивостей може бути різною. Наприклад , в C # оголошення властивості безпосередньо містить код методів доступу , який викликається тільки при роботі з властивостями , тобто не вимагає окремих методів доступу , доступних для безпосереднього виклику . У Delphi оголошення властивості містить лише імена методів доступу , які повинні викликатися при зверненні до поля. Самі методи доступу є звичайні методи з деякими додатковими вимогами до сигнатурі .
Поліморфізм реалізується шляхом введення в мову правил , згідно з якими змінної типу «клас » може бути присвоєний об'єкт будь-якого класу - нащадка її
Лекція 16
Тема : Створення базових класів, обєкти. Конструктори та деструктори, this. Статичні та константні поля.
Мета: Вивчити поняття конструктора та деструктора, типи конструкторів, перевантаження конструкторі. Розібрати синтаксис та обмеження роботи з конструкторами та деструкторами.
В об'єктно - орієнтованому програмуванні конструктор класу (від англ. Constructor, іноді скорочують ctor ) - спеціальний блок інструкцій , що викликається при створенні об'єкта.
Конструктор схожий з методом , але відрізняється від методу тим , що не має явним чином певного типу повертаються даних , не успадковується , і зазвичай має різні правила для розглянутих модифікаторів. Конструктори часто виділяються наявністю однакового імені з ім'ям класу , в якому оголошується . Їх завдання - ініціалізувати члени об'єкта і визначити інваріант класу , повідомивши у разі некоректності інваріанта . Коректно написаний конструктор залишить об'єкт в « правильному » стані. Незмінні об'єкти теж повинні бути проініціалізовані конструктором.
Термін « конструктор » також використовується для позначення одного з тегів , що описують дані в алгебраїчному типі даних. Це використання дещо відрізняється від описуваного в статті. Для додаткової інформації дивіться Алгебраїчний тип даних.
У більшості мов конструктор може бути перевантажений , що дозволяє використовувати декілька конструкторів в одному класі , причому кожен конструктор може мати різні параметри .
призначення конструктора
Одна з ключових особливостей ООП - інкапсуляція : внутрішні поля об'єкта безпосередньо недоступні , і користувач може працювати з об'єктом тільки як з єдиним цілим , через відкриті ( public ) методи. Кожен метод , в ідеалі , має бути влаштований так , щоб об'єкт, що знаходиться в « допустимому » стані (тобто коли виконується інваріант класу) , після виклику методу також опинився в допустимому стані. І перше завдання конструктора - перевести поля об'єкта в такий стан.
Друге завдання - спростити користування об'єктом . Об'єкт - не «річ у собі» , йому часто доводиться вимагати якусь інформацію від інших об'єктів : наприклад , об'єкт File , створюючись , повинен отримати ім'я файлу. Це можна зробити і через метод:
File file ;
file.open ( " in.txt " , File :: omRead ) ;
Але зручніше відкриття файлу зробити в конструкторі : [ 1 ]
File file ( " in.txt " , File :: omRead ) ;
види конструкторів
Деякі мови програмування розрізняють кілька особливих типів конструкторів :
• конструктор за замовчуванням - конструктор , що не приймає аргументів ;
• конструктор копіювання - конструктор , що приймає як аргумент об'єкт того ж класу ( або посилання з нього) ;
• конструктор перетворення - конструктор , що приймає один аргумент (ці конструктори можуть викликатися автоматично для перетворення значень інших типів в об'єкти даного класу).
class Complex
{
public :
/ / Конструктор за замовчуванням
/ / (У даному випадку є також і конструктором перетворення)
Complex ( double i_re = 0 , double i_im = 0 )
: Re ( i_re ) , im ( i_im )
{}
/ / Конструктор копіювання
Complex ( const Complex & obj )
{
re = obj.re ;
im = obj.im ;
}
private :
double re , im ;
} ;
Конструктор за замовчуванням
Основна стаття : Конструктор за замовчуванням
Конструктор не має обов'язкових аргументів. Використовується при створенні масивів об'єктів , викликаючи для створення кожного екземпляра. У відсутність явно заданого конструктора за замовчуванням його код генерується компілятором ( що на початковому тексті , природно, не відбивається) .
конструктор копіювання
Основна стаття : Конструктор копіювання
Конструктор , аргументом якого є посилання на об'єкт того ж класу. Застосовується в C + + для передачі об'єктів у функції за значенням .
Конструктор копіювання в основному необхідний, коли об'єкт має покажчики на об'єкти , виділені в купі . Якщо програміст не створює конструктор копіювання , то компілятор створить неявний конструктор копіювання , який копіює покажчики як є , тобто фактичне копіювання даних не відбувається і два об'єкти посилаються на одні й ті ж дані в купі . Відповідно спроба зміни « копії » зашкодить оригінал, а виклик деструктора для одного з цих об'єктів при подальшому використанні іншого призведе до обігу в область пам'яті , вже не належить програмі .
Аргумент повинен передаватися саме за посиланням , а не за значенням . Це випливає з колізії : при передачі об'єкта за значенням (зокрема , для виклику конструктора ) потрібно скопіювати об'єкт. Але для того , щоб скопіювати об'єкт , необхідно викликати конструктор копіювання.
конструктор перетворення
Конструктор , що приймає один аргумент. Задає перетворення типу свого аргументу в тип конструктора . Таке перетворення типу неявно застосовується тільки якщо воно унікальне .
Віртуальний конструктор
Конструктор не буває віртуальним в сенсі віртуального методу - для того , щоб механізм віртуальних методів працював , потрібно запустити конструктор , який автоматично налаштує таблицю віртуальних методів даного об'єкта.
« Віртуальними конструкторами » називають схожий , але інший механізм , присутній у деяких мовах - наприклад , він є в Delphi , але немає в C + + і Java. Цей механізм дозволяє створити об'єкт будь-якого заздалегідь невідомого класу за двох умов:
• цей клас є нащадком якогось наперед заданого класу (у даному прикладі це клас TVehicle ) ;
• на всьому шляху наслідування від базового класу до створюваного ланцюжок перевизначення не обривати . При перевизначенні віртуального методу синтаксис Delphi вимагає ключове слово overload , щоб стара і нова функції з різними сигнатурами могли співіснувати , override для перевизначення функції або reintroduce для завдання нової функції з тим же ім'ям - останнє неприпустимо.
type
TVehicle = class
constructor Create ; virtual ;
end ;
TAutomobile = class ( TVehicle )
constructor Create ; override ;
end ;
TMotorcycle = class ( TVehicle )
constructor Create ; override ;
end ;
TMoped = class ( TMotorcycle ) / / обриваємо ланцюжок перевизначення - заводимо новий Create
constructor Create ( x : integer ) ; reintroduce ;
end ;
У мові вводиться так званий класовий тип ( метаклассом ) . Цей тип як значення може приймати назву будь-якого класу , похідного від TVehicle .
type
CVehicle = class of TVehicle ;
Такий механізм дозволяє створювати об'єкти будь-якого заздалегідь невідомого класу , похідного від TVehicle .
var
cv : CVehicle ;
v : TVehicle ;
cv : = TAutomobile ;
v : = cv.Create ;
Зауважте , що код
cv : = TMoped ;
v : = cv.Create ;
є некоректним - директива reintroduce розірвала ланцюжок перевизначення віртуального методу , і насправді буде викликаний конструктор TMotorcycle.Create (а значить , буде створений мотоцикл , а не мопед !)
Див також Фабрика ( шаблон проектування )
Синтаксис С + +
Ім'я конструктора має збігатися з ім'ям класу. Допускається використовувати кілька конструкторів з однаковим ім'ям , але різними параметрами.
приклад
class ClassWithConstructor {
public :
/ * Ініціалізація внутрішнього об'єкта за допомогою конструктора * /
ClassWithConstructor ( float parameter ) : object ( parameter ) { } / * виклик конструктора AnotherClass ( float ) ; * /
private :
AnotherClass object ;
} ;
Статічні члени класів
• Статічні дані -члени (поля) класів Використовують для Збереження Даних, спільніх для всіх екземплярів класу , Наприклад , кількість екземплярів класу , вказівнік на вершину дінамічного списку , Деяк глобальну для Всього класу константу , тощо . Статичний член класу має буті продекларованім у класі з помощью службового слова 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 ;
• system ( " PAUSE" ) ;
• return 0 ;
• }
Статічні Функції -члени класів Використовують Тільки для звертання до статичних Даних -членів и НЕ могут використовуват звічайні дані та методи класу , Адже я смороду НЕ прив'язані до екземпляр , їм НЕ передається вказівнік this .
• Статічні Функції -члени класів Використовують Тільки для звертання до статичних Даних -членів и НЕ могут використовуват звічайні дані та методи класу , Аджея смороду НЕ прив'язані до екземпляр , їм НЕ передається вказівнік this .
• Службове слово static вказується позбав у декларації статічної Функції , при ее візначенні воно НЕ повторюється .
• звертання до статичних методів так само может відбуватісь и через имя класу , и через ідентифікатор екземпляр .
• Слід зауважіті , что звічайні Функції -члени класу мают право працювати Із статичності членами класу .
• Конструктор та деструктор НЕ могут буті статичність !
Лекція 17
Тема : Перевантаження методів та операторів.
При визначенні функцій у своїх програмах ви повинні вказати тип повертається функцією значення, а також кількість параметрів і тип кожного з них. У минулому ( якщо ви програмували на мові С) , коли у вас була функція з ім'ям add_values , яка працювала з двома цілими значеннями , а ви хотіли б використовувати подібну функцію для складання трьох цілих значень , вам слід було створити функцію з іншим ім'ям. Наприклад , ви могли б використовувати add_two_values і add_three_values. Аналогічно якщо ви хотіли використовувати подібну функцію для складання значень типу float , то вам була б необхідна ще одна функція з ще одним ім'ям . Щоб уникнути дублювання функції , C + + дозволяє вам визначати кілька функцій з одним і тим же ім'ям. У процесі компіляції C + + бере до уваги кількість аргументів , що використовуються кожною функцією , і потім викликає саме потрібну опцію. Надання компілятору вибору серед декількох функцій називається перевантаженням . У цьому уроці ви навчитеся використовувати перевантажені функції . До кінця даного уроку ви освоїте такі основні концепції:
• Перевантаження функцій дозволяє вам використовувати одне і те ж ім'я для кількох функцій з різними типами параметрів .
• Для перевантаження функцій просто визначте дві функції з одним і тим же ім'ям і типом значення, що повертається , які відрізняються кількістю параметрів або їх типом .
Перевантаження функцій є особливістю мови C + + , якої немає в мові С. Як ви побачите , перевантаження функцій досить зручна і може поліпшити читабельність ваших програм.
ПЕРШЕ ЗНАЙОМСТВО З перевантаження функцій
Перевантаження функцій дозволяє вашим програмам визначати кілька функцій з одним і тим же ім'ям і типом значення, що повертається . Наприклад , наступна програма перевантажує функцію з ім'ям add_values . Перше визначення функції складає два значення типу int . Друге визначення функції складає три значення . У процесі компіляції C + + коректно визначає функцію , яку необхідно використовувати:
# include <iostream.h>
int add_values ( int a , int b )
{
return ( a + b ) ;
)
int add_values ( int a , int b , int c )
(
return ( a + b + c ) ;
)
void main ( void )
{
cout << " 200 + 801 = " << add_values ( 200 , 801) << endl ;
cout << " 100 + 201 + 700 = " << add_values (100, 201 , 700 ) << endl ;
}
Як бачите , програма визначає дві функції з іменами add_values Перша функція складає два значення типу int , в той час як друга складає три значення . Ви не зобов'язані небудь робити спеціально для того , щоб попередити компілятор про перевантаження , просто використовуйте її . Компілятор розгадає , яку функцію слід використовувати , грунтуючись на пропонованих програмою параметрах.
Подібним чином наступна програма MSG_OVR.CPP перевантажує функцію show_message . Перша функція з ім'ям show_message виводить стандартне повідомлення , параметри їй не передаються. Друга виводить передане їй повідомлення , а третя виводить два повідомлення :
# include <iostream.h>
void show_message ( void )
{
cout << " Стандартне повідомлення : " << " Вчимося програмувати на C + + " << endl ;
}
void show_message ( char * message )
{
cout << message << endl ;
}
void show_message ( char * first , char * second )
{
cout << first << endl ;
cout << second << endl ;
}
void main ( void )
{
show_message ();
show_message ( "Вчимося програмувати на мові C + +! " ) ;
show_message ( " B C + + немає забобонів ! " , " Перевантаження - це круто !") ;
}
КОЛИ ПОТРІБНО Перевантажування
Одним з найбільш загальних випадків використання перевантаження є застосування функції для отримання певного результату , виходячи з різних параметрів . Наприклад, припустимо , що у вашій програмі є функція з ім'ям day_of_week , яка повертає поточний день тижня ( 0 для неділі , 1 для понеділка , ... , 6 для суботи). Ваша програма могла б перевантажити цю функцію таким чином , щоб вона вірно повертала день тижня , якщо їй переданий юліанський день в якості параметра , або якщо їй передані день , місяць і рік:
int day_of_week ( int julian_day )
{
/ / Оператори
}
int day_of_week ( int month , int day , int year )
{
/ / Оператори
}
У міру вивчення об'єктно -орієнтованого програмування в C + + , представленого в наступних уроках , ви будете використовувати перевантаження функцій для розширення можливостей своїх програм.
Перевантаження функцій покращує читабельність програм
Перевантаження функцій C + + дозволяє вашим програмам визначати кілька функцій з одним і тим же ім'ям. Перевантажені функції повинні повертати значення однакового типу * , але можуть відрізнятися кількістю і типом параметрів . До появи перевантаження функцій в C + + програмісти мови С повинні були створювати кілька функцій з майже однаковими іменами. На жаль програмісти , бажаючі використовувати такі функції , повинні були пам'ятати , яка комбінація параметрів відповідає якій функції . З іншого боку , перевантаження функцій спрощує завдання програмістів , вимагаючи , щоб вони пам'ятали тільки одне ім'я функції .
* Перевантажені функції не зобов'язані повертати значення однакового типу з тієї причини , що компілятор однозначно ідентифікує функцію по її імені і набору її аргументів. Для компілятора функції з однаковими іменами , але різними типами аргументів - різні функції, тому тип значення - прерогатива кожної функції . - Прім.пер .
ЩО ВАМ ТРЕБА ЗНАТИ
Перевантаження функцій дозволяє вам вказати кілька визначень для однієї і тієї ж функції . У процесі компіляції C + + визначить , яку функцію слід використовувати, грунтуючись на кількості і типі переданих параметрів . З даного уроку ви дізналися, що перевантажувати функції досить просто . З уроку 14 ви дізнаєтеся , як посилання C + + спрощують процес зміни параметрів всередині функцій . Однак, перш ніж перейти до уроку 14 , переконайтеся , що ви вивчили такі основні концепції:
Перевантаження функцій надає кілька " поглядів " на одну і ту ж функцію всередині вашої програми .
Для перевантаження функцій просто визначте декілька функцій з одним і тим же ім'ям і типом значення, що повертається , які відрізняються тільки кількістю і типом параметрів .
У процесі компіляції C + + визначить , яку функцію слід викликати , грунтуючись на кількості і типі переданих параметрів .
Перевантаження функцій спрощує програмування , дозволяючи програмістам працювати тільки з одним ім'ям функції .
Бажання написати дану статтю з'явилося після прочитання поста Перевантаження C + + операторів , тому що в ньому не було розкрито багато важливі теми .
Найголовніше , що необхідно пам'ятати - перевантаження операторів , це всього лише більш зручний спосіб виклику функцій , тому не варто захоплюватися перевантаженням операторів. Використовувати її слід тільки тоді , коли це спростить написання коду. Але , не настільки, щоб це ускладнювало читання . Адже , як відомо , код читається набагато частіше , ніж пишеться . І не забувайте , що вам ніколи не дадуть перевантажити оператори в тандемі з вбудованими типами , можливість перевантаження є тільки для користувача типів / класів .
синтаксис перевантаження
Синтаксис перевантаження операторів дуже схожий на визначення функції з ім'ям operator @ , де @ - це ідентифікатор оператора ( наприклад +, - , << , >> ) . Розглянемо найпростіший приклад :
class Integer
{
private :
int value ;
public :
Integer ( int i ) : value ( i )
{ }
const Integer operator + ( const Integer & rv ) const {
return ( value + rv.value ) ;
}
} ;
У даному випадку , оператор оформлений як член класу , аргумент визначає значення, що знаходиться в правій частині оператора. Взагалі , існує два основних способи перевантаження операторів : глобальні функції , дружні для класу , або підставляються функції самого класу. Який спосіб , для якого оператора краще , розглянемо в кінці топіка .
У більшості випадків , оператори ( крім умовних ) повертають об'єкт , або посилання на тип , до якого відносяться його аргументи (якщо типи різні, то ви самі вирішуєте як інтерпретувати результат обчислення оператора).
Перевантаження унарних операторів
Розглянемо приклади перевантаження унарних операторів для певного вище класу Integer . Заодно визначимо їх у вигляді дружніх функцій і розглянемо оператори декремента і инкремента :
class Integer
{private :
int value ;
public :
Integer ( int i ) : value ( i )
{ }
/ / унарний +
friend const Integer & operator + ( const Integer & i ) ;
/ / унарний -
friend const Integer operator - ( const Integer & i ) ;
/ / префіксний інкремент
friend const Integer & operator + + ( Integer & i ) ;
/ / постфіксний інкремент
friend const Integer operator + + ( Integer & i , int ) ;
/ / префіксний декремент
friend const Integer & operator - ( Integer & i ) ;
/ / постфіксний декремент
friend const Integer operator - ( Integer & i , int ) ;
} ;
/ / унарний плюс нічого не робить.
const Integer & operator + ( const Integer & i ) {
return i.value ;
}
const Integer operator - ( const Integer & i ) {
return Integer ( - i.value ) ;
}
/ / префіксная версія повертає значення після инкремента
const Integer & operator + + ( Integer & i ) {
i.value + +;
return i ;
}
/ / Постфіксний версія повертає значення до инкремента
const Integer operator + + ( Integer & i , int ) {
Integer oldValue ( i.value ) ;
i.value + +;
return oldValue ;
}
/ / префіксная версія повертає значення після декремента
const Integer & operator - ( Integer & i ) {
i.value - - ;
return i ;
}
/ / Постфіксний версія повертає значення до декремента
const Integer operator - ( Integer & i , int ) {
Integer oldValue ( i.value ) ;
i.value - - ;
return oldValue ;
}
Тепер ви знаєте , як компілятор розрізняє префіксние і постфіксні версії декремента і инкремента . У разі , коли він бачить вираз + + i , то викликається функція operator + + ( a ) . Якщо ж він бачить i + + , то викликається operator + + ( a , int ) . Тобто викликається перевантажена функція operator + + , і саме для цього використовується фіктивний параметр int в постфіксной версії.
бінарні оператори
Розглянемо синтаксис перевантаження бінарних операторів. Перевантажимо один оператор , який повертає l - значення , один умовний оператор і один оператор , що створює нове значення ( визначимо їх глобально ) :
class Integer
{
private :
int value ;
public :
Integer ( int i ) : value ( i )
{ }
friend const Integer operator + ( const Integer & left , const Integer & right ) ;
friend Integer & operator + = ( Integer & left , const Integer & right ) ;
friend bool operator == ( const Integer & left , const Integer & right ) ;
} ;
const Integer operator + ( const Integer & left , const Integer & right ) {
return Integer ( left.value + right.value ) ;
}
Integer & operator + = ( Integer & left , const Integer & right ) {
left.value + = right.value ;
return left ;
}
bool operator == ( const Integer & left , const Integer & right ) {
return left.value == right.value ;
}
У всіх цих прикладах оператори перевантажуються для одного типу , однак , це необов'язково. Можна, наприклад , перевантажити додавання нашого типу Integer і визначеного за його подобою Float .
Аргументи і повертаються значення
Як можна було помітити , в прикладах використовуються різні способи передачі аргументів у функції і повернення значень операторів.
Якщо аргумент не змінюється оператором , у випадку, наприклад Унарні плюса , його потрібно передавати як посилання на константу . Взагалі , це справедливо для майже всіх арифметичних операторів (додавання, віднімання , множення ... )
Тип значення залежить від суті оператора. Якщо оператор повинен повертати нове значення , то необхідно створювати новий об'єкт ( як у випадку бінарного плюса ) . Якщо ви хочете заборонити зміну об'єкта як l - value , то потрібно повертати його константним .
Для операторів присвоювання необхідно повертати посилання на змінений елемент . Також , якщо ви хочете використовувати оператор присвоєння в конструкціях виду ( x = y ) . F ( ) , де функція f ( ) викликається для для змінної x , після присвоювання їй y , то чи не повертайте посилання на константу , повертайте просто посилання .
Логічні оператори повинні повертати в гіршому випадку int , а в кращому bool .
Оптимізація значення, що повертається
При створенні нових об'єктів і повернення їх з функції слід використовувати запис як для вищеописаного прикладу оператора бінарного плюса.
return Integer ( left.value + right.value ) ;
Чесно кажучи , не знаю , яка ситуація актуальна для C + +11 , всі міркування далі справедливі для C + +98 .
На перший погляд , це схоже на синтаксис створення тимчасового об'єкта , тобто начебто б немає різниці між кодом вище і цим:
Integer temp ( left.value + right.value ) ;
return temp ;
Але насправді , в цьому випадку відбудеться виклик конструктора в першому рядку, далі виклик конструктора копіювання , який скопіює об'єкт , а далі , при розкручуванні стека викличеться деструктор . При використанні першого запису компілятор спочатку створює об'єкт в пам'яті , в яку потрібно його скопіювати , таким чином економиться виклик конструктора копіювання і деструкції.
особливі оператори
У C + + є оператори , що володіють специфічним синтаксисом і способом перевантаження. Наприклад оператор індексування []. Він завжди визначається як член класу і , так як мається на увазі поведінку индексируемого об'єкта як масиву , то йому слід повертати посилання .
оператор кома
У число «особливих» операторів входить також оператор кома . Він викликається для об'єктів , поряд з якими поставлена кома (але він не викликається в списках аргументів функцій). Придумати осмислений приклад використання цього оператора не так-то просто . Хабраюзер AxisPod в коментарях до попередньої статті про перевантаження розповів про одне.
Оператор разименованія покажчика
Перевантаження цих операторів може бути виправдана для класів розумних покажчиків . Цей оператор обов'язково визначається як функція класу , причому на нього накладаються деякі обмеження : він повинен повертати або об'єкт ( або посилання ), або покажчик , що дозволяє звернутися до об'єкта.
оператор присвоювання
Оператор присвоювання обов'язково визначається у вигляді функції класу , тому що він нерозривно пов'язаний з об'єктом, що знаходиться зліва від "=" . Визначення оператора привласнення в глобальному вигляді зробило б можливим перевизначення стандартного поведінки оператора " =" . приклад :
class Integer
{
private :
int value ;
public :
Integer ( int i ) : value ( i )
{ }
Integer & operator = ( const Integer & right ) {
/ / перевірка на самопрісваіваніе
if ( this == & right ) {
return * this ;
}
value = right.value ;
return * this ;
}
} ;
Як можна помітити , на початку функції проводиться перевірка на самопрісваіваніе . Взагалі , в даному випадку самопрісваіваніе нешкідливо , але ситуація не завжди така проста . Наприклад , якщо об'єкт великий , можна витратити багато часу на непотрібне копіювання , або при роботі з покажчиками .
Неперегружаемие оператори
Деякі оператори в C + + не перевантажуються в принципі. По всій видимості , це зроблено з міркувань безпеки.
Оператор вибору члена класу ". " .
Оператор разименованія покажчика на член класу ". * "
У С + + відсутня оператор піднесення до степеня (як у Fortran ) " ** " .
Заборонено визначати свої оператори ( можливі проблеми з визначенням пріоритетів ) .
Не можна змінювати пріоритети операторів
Рекомендації до форми визначення операторів
Як ми вже з'ясували , існує два способи операторів - у вигляді функції класу і у вигляді дружньої глобальної функції .
Роб Мюррей , у своїй книзі C + + Strategies and Tactics визначив наступні рекомендації з вибору форми оператора :
Оператор
Рекомендована форма
Всі унарні оператори
член класу
= ( ) [] -> -> *
Обов'язково член класу
+ = - = / = * = ^ = & = | = % = >> = << =
член класу
Решта бінарні оператори
Чи не член класу
Чому так ? По-перше , на деякі оператори спочатку накладено обмеження . Взагалі , якщо семантично немає різниці як визначати оператор , то краще його оформити у вигляді функції класу , щоб підкреслити зв'язок , плюс крім цього функція буде підставляється ( inline ) . До того ж , іноді може виникнути потреба в тому , щоб представити лівобічний операнд об'єктом іншого класу. Напевно , найяскравіший приклад - перевизначення << і >> для потоків введення / виводу.
Література Брюс Еккель - Філософія C + +. Введення в стандартний C + +.
Лекція 18
Тема : Успадкування. Створення спадкоємних класів. Скриття, перекриття та успадкування методів та властивостей класу.
Мета об'єктно - орієнтованого програмування полягає в повторному використанні створених вами класів , що економить ваш час і сили. Якщо ви вже створили певний клас , то можливі ситуації , що новому класу потрібні багато або навіть усі особливості вже існуючого класу , і необхідно додати один або кілька елементів даних або функцій . У таких випадках C + + дозволяє вам будувати новий об'єкт , використовуючи характеристики вже існуючого об'єкта. Іншими словами , новий об'єкт буде успадковувати елементи існуючого класу ( званого базовим класом) . Коли ви будуєте новий клас з існуючого , цей новий клас часто називається похідним класом. У цьому уроці вперше вводиться наслідування класів в C + +. До кінця даного уроку ви вивчіть наступні основні концепції:
Їли ваші програми використовують спадкування , то для породження нового класу необхідний базовий клас , тобто новий клас успадковує елементи базового класу.
Для ініціалізації елементів похідного класу ваша програма повинна викликати конструктори базового і похідного класів .
Використовуючи оператор точку , програми можуть легко звертатися до елементів базового і похідного класів .
На додаток до загальних ( public ) ( доступним всім) і приватним ( private ) ( доступним методам класу) елементам C + + надає захищені ( protected ) елементи , які доступні базовому і похідному класах .
Для вирішення конфлікту імен між елементами базового і похідного класів ваша програма може використовувати оператор глобального дозволу , вказуючи перед ним ім'я базової або похідного класу .
Спадкування є фундаментальною концепцією об'єктно - орієнтованого програмування. Виберіть час для експериментів з програмами , представленими в цьому уроці . І ви виявите , що реально спадкування реалізується дуже просто і може зберегти величезні зусилля, витрачені на програмування .
ПРОСТЕ УСПАДКУВАННЯ
Спадкування являє собою здатність похідного класу успадковувати характеристики існуючого базового класу. Наприклад, припустимо , що у вас є базовий клас employee :
class employee
{
public :
employee ( char * , char * , float ) ;
void show_employee ( void ) ;
private :
char name [ 64 ] ;
char position [ 64 ] ;
float salary ;
} ;
Далі припустимо , що вашій програмі потрібно клас manager , який додає наступні елементи даних в клас employee :
float annual_bonus ;
char company_car [ 64 ] ;
int stock_options ;
У даному випадку ваша програма може вибрати два варіанти : по-перше , програма може створити новий клас manager , який дублює багато елементів класу employee, або програма може породити клас типу manager з базового класу employee . Породжуючи клас manager з існуючого класу employee , ви знижуєте обсяг необхідного програмування і виключаєте дублювання коду всередині вашої програми .
Для визначення цього класу ви повинні вказати ключове слово class , ім'я manager , наступне за ним двокрапку і ім'я employee , як показано нижче:
Похідний клас / / ----- > class manager : public employee { < ------- / / Базовий клас
/ / Тут визначаються елементи
} ;
Ключове слово public , яке передує ім'я класу employee , вказує , що загальні ( public ) елементи класу employee також є загальними і в класі manager . Наприклад , наступні оператори породжують клас manager .
class manager : public employee
{
public :
manager ( char * , char * , char * , float , float , int ) ;
void show_manager ( void ) ;
private :
float annual_bonus ;
char company_car [ 64 ] ;
int stock_options ;
} ;
Коли ви породжує клас з базового класу , приватні елементи базового класу доступні похідному класу тільки через інтерфейсні функції базового класу. Таким чином , похідний клас не може напряму звернутися до приватних елементів базового класу , використовуючи оператор крапку.
Наступна програма MGR_EMP.CPP ілюструє використання спадкування в C + + , створюючи клас manager з базового класу employee :
# include <iostream.h>
# include <string.h>
class employee
{
public :
employee ( char * , char * , float ) ;
void show_employee ( void ) ;
private :
char name [ 64 ] ;
char position [ 64 ] ;
float salary ;
} ;
employee :: employee ( char * name , char * position , float salary )
{
strcpy ( employee :: name , name ) ;
strcpy ( employee :: position , position ) ;
employee :: salary = salary ;
}
void employee :: show_employee ( void )
{
cout << "Назва: " << name << endl ;
cout << " Посада: " << position << endl ;
cout << " Оклад : $ " << salary << endl ;
}
class manager : public employee
{
public :
manager ( char * , char * , char * , float , float , int ) ;
void show_manager ( void ) ;
private :
float annual_bonus ;
char company_car [ 64 ] ;
int stock_options ;
} ;
manager :: manager ( char * name , char * position , char * company_car , float salary , float bonus , int stock_options ) : employee ( name , position , salary )
{
strcpy ( manager :: company_car , company_car ) ;
manager :: annual_bonus = bonus ;
manager :: stock_options = stock_options ;
}
void manager :: show_manager ( void )
{
show_employee ();
cout << " Машина фірми: " << company_car << endl ;
cout << " Щорічна премія : $ " << annual_bonus << endl ;
cout << " Фондовий опціон : " << stock_options << endl ;
}
void main ( void )
{
employee worker ( "Джон Дой " , "Програміст " , 35000 ) ;
manager boss ( " Джейн Дой " , "Віце -президент" , "Lexus" , 50000.0 , 5000 , 1000) ;
worker.show_employee ();
boss.show_manager ();
}
Як бачите , програма визначає базовий клас employee , а потім визначає похідний клас manager . Зверніть увагу на конструктор manager . Коли ви породжує клас з базового класу , конструктор похідного класу повинен викликати конструктор базового класу. Щоб викликати конструктор базового класу , помістіть двокрапку відразу ж після конструктора похідного класу , а потім вкажіть ім'я конструктора базового класу з необхідними параметрами :
manager :: manager ( char * name , char * position , char * company_car , float salary , float bonus , int stock_options ) :
employee ( name , position , salary ) / / ------------- Конструктор базового класу
{
strcpy ( manager :: company_car , company_car ) ;
manager :: annual_bonus = bonus ;
manager :: stock_options = stock_options ;
}
Також зверніть увагу , що функція show_manager викликає функцію show_employee , яка є елементом класу employee . Оскільки клас manager є похідним класу employee , клас manager може звертатися до загальних елементів класу employee , як якщо б всі ці елементи були визначені усередині класу manager ,
Подання про спадкування
Спадкування являє собою здатність похідного класу успадковувати характеристики існуючого базового класу. Простими словами це означає , що , якщо у вас є клас , чиї елементи даних або функції - елементи можуть бути використані новим класом , ви можете побудувати цей новий клас в термінах існуючого (або базового ) класу. Новий клас у свою чергу буде наслідувати елементи ( характеристики ) існуючого класу. Використання успадкування для побудови нових класів заощадить вам значний час і сили на програмування . Об'єктно - орієнтоване програмування широко використовує спадкування , дозволяючи вашій програмі будувати складні об'єкти з невеликих легко керованих об'єктів.
другий приклад
Припустимо , наприклад , що ви використовуєте наступний базовий клас book середині існуючої програми :
class book
{
public :
book ( char * , char * , int ) ;
void show_book ( void ) ;
private :
char title [ 64 ] ;
char author [б 4];
int pages ;
} ;
Далі припустимо , що програмі потрібно створити клас library_card , який буде додавати наступні елементи даних в клас book :
char catalog [ 64 ] ;
int checked_out ; / / 1 , якщо перевірена , інакше Про
Ваша програма може використовувати спадкування , щоб породити клас library _card з класу book , як показано нижче:
class library_card : public book
{
public :
library_card ( char * , char * , int , char * , int ) ;
void show_card ( void ) ;
private :
char catalog [ 64 ] ;
int checked_out ;
} ;
Наступна програма BOOKCARD.CPP породжує клас library_card з клacca book :
# include <iostream.h>
# include <string.h>
class book
{
public :
book ( char * , char * , int ) ;
void show_book ( void ) ;
private :
char title [ 64 ] ;
char author [ 64 ] ;
int pages ;
} ;
book :: book ( char • title , char * author , int pages )
{
strcpy ( book :: title , title ) ;
strcpy ( book :: author , author ) ;
book :: pages = pages ;
}
void book :: show_book ( void )
{
cout << " Назва: " << title << endl ;
cout << " Автор : " << author << endl ;
cout << " Сторінок: " << pages << endl ;
}
class library_card : public book
{
public :
library_card ( char * , char * , int , char * , int ) ;
void show_card ( void ) ;
private :
char catalog [ 64 ] ;
int checked_out ;
} ;
library_card :: library_card ( char * title , char * author , int pages , char * catalog , int checked_out ) : book ( title , author , pages )
{
strcpy ( library_card :: catalog , catalog ) ;
library_card :: checked_out = checked_out ;
}
void 1ibrary_card :: show_card ( void )
{
show_book ();
cout << " Каталог : " << catalog << endl ;
if ( checked_out ) cout << " Статус: перевірена " << endl ;
else cout << " Статус : вільна " << endl ;
}
void main ( void )
{
library_card card ( "Вчимося програмувати на мові C + +" , " Jamsa " , 272 , " 101СРР " , 1);
card.show_card ();
}
Як і раніше , зверніть увагу , що конструктор library _card викликає конструктор класу book для ініціалізації елементів класу book . Крім того , зверніть увагу на використання функції -елемента show_book класу book всередині функції show_card . Оскільки клас library_card успадковує методи класу book , функція show_card може викликати цей метод ( show_book ) без допомоги оператора точки , як якщо б цей метод був методом класу library _card .
ЩО ТАКЕ ЗАХИЩЕНІ ЕЛЕМЕНТИ
При вивченні визначень базових класів ви можете зустріти елементи , оголошені як public , private і protected (загальні , приватні і захищені) . Як ви знаєте , похідний клас може звертатися до загальних елементам базового класу , як ніби вони визначені в похідному класі . З іншого боку , похідний клас не може звертатися до приватних елементів базового класу безпосередньо. Замість цього для звернення до таких елементів похідний клас повинен використовувати інтерфейсні функції . Захищені елементи базового класу займають проміжне положення між приватними і загальними. Якщо елемент є захищеним , об'єкти похідного класу можуть звертатися до нього , як ніби він є загальним . Для решти вашої програми захищені елементи є як би приватними . Єдиний спосіб , за допомогою якого ваші програми можуть звертатися до захищених елементів , полягає у використанні інтерфейсних функцій . Наступне визначення класу book використовує мітку protected , щоб дозволити класам , похідним від класу book , звертатися до елементів title , author і pages безпосередньо, використовуючи оператор крапку:
class book
{
public :
book ( char * , char * , int ) ;
void show_book ( void ) ;
protected :
char title [ 64 ] ;
char author [ 64 ] ;
int pages ;
} ;
Якщо ви припускаєте , що через деякий час вам доведеться породити нові класи з створюваного зараз класу , встановіть , чи повинні майбутні похідні класи безпосередньо звертатися до певних елементів створюваного класу , і оголосите такі елементи захищеними , а не приватними .
Захищені елементи забезпечують доступ і захист
Як ви вже знаєте , програма не може звернутися безпосередньо до приватних елементів класу . Для звернення до приватних елементам програма повинна використовувати інтерфейсні функції , які управляють доступом до цих елементів . Як ви , мабуть, помітили , спадкування спрощує програмування в тому випадку , якщо похідні класи можуть звертатися до елементів базового класу за допомогою оператора точки . У таких випадках ваші програми можуть використовувати захищені елементи класу. Похідний клас може звертатися до захищених елементів базового класу безпосередньо, використовуючи оператор крапку. Однак частина, що залишилася вашої програми може звертатися до захищених елементів тільки за допомогою інтерфейсних функцій цього класу. Таким чином , захищені елементи класу знаходяться між загальними ( доступними всій програмі ) і приватними ( доступними тільки самому класу ) елементами .
ДОЗВІЛ КОНФЛІКТУ ІМЕН
Якщо ви породжує один клас з іншого , можливі ситуації , коли ім'я елемента класу в похідному класі є таким же , як ім'я елемента в базовому класі. Якщо виник такий конфлікт , C + + завжди використовує елементи похідного класу всередині функцій похідного класу . Наприклад, припустимо , що класи book і library_card використовують елемент price . У разі класу book елемент price відповідає продажній ціні книги , наприклад $ 22.95 . У разі класу library'_card price може включати бібліотечну знижку , наприклад $ 18.50 . Якщо у вашому початковому тексті не вказано явно (за допомогою оператора глобального дозволу) , функції класу library_card будуть використовувати елементи похідного класу { library_card ) . Якщо ж функціям класу library_card необхідно звертатися до елементу price базового класу { book ) , вони повинні використовувати ім'я класу book і оператор дозволу , наприклад book :: price . Припустимо , що функції show_card необхідно вивести обидві ціни . Тоді вона повинна використовувати наступні оператори :
cout << " Бібліотечна ціна: $ " << price << endl ;
cout << " Продажна ціна: $ " << book :: price << endl ;
ЩО ВАМ ТРЕБА ЗНАТИ
З цього уроку ви дізналися , що спадкування в C + + дозволяє вам будувати / породжувати ) новий клас з існуючого класу. Будуючи такий спосіб один клас з іншого , ви зменшуєте обсяг програмування , що , у свою чергу , заощаджує ваш час. З уроку 27 ви дізнаєтеся , що C + + дозволяє вам породжувати клас з двох або декількох базових класів . Використання декількох базових класів для породження класу представляє собою множинне спадкування .
Спадкування являє собою здатність виробляти новий клас з існуючого базового класу.
Похідний клас - це новий клас , а базовий клас - існуючий клас .
Коли ви породжує один клас з іншого ( базового класу) , похідний клас успадковує елементи базового класу.
Для породження класу з базового починайте визначення похідного класу ключовим словом class , за яким слідує ім'я класу , двокрапка і ім'я базового класу , наприклад class dalmatian : dog .
Коли ви породжує клас з базового класу , похідний клас може звертатися до загальних елементам базового класу , як ніби ці елементи визначені всередині самого похідного класу . Для доступу до приватних даними базового класу похідний клас повинен використовувати інтерфейсні функції базового класу.
Усередині конструктора похідного класу ваша програма повинна викликати конструктор базового класу , вказуючи двокрапка , ім'я конструктора базового класу та відповідні параметри відразу ж після заголовка конструктора похідного класу .
Щоб забезпечити похідним класам прямий доступ до певних елементів базового класу , в той же час захищаючи ці елементи від решти програми , C + + забезпечує захищені { protected ) елементи класу. Похідний клас може звертатися до захищених елементів базового класу , як ніби вони є спільними. Однак для решти програми захищені елементи еквівалентні приватним.
Якщо у похідному і базовому класі є елементи з однаковим ім'ям , то всередині функцій похідного класу C + + буде використовувати елементи похідного класу . Якщо функціям похідного класу необхідно звернутися до елементу базового класу , ви повинні використовувати оператор глобального дозволу , наприклад base class :: member .
Лекція 19
Тема : Абстрактні та віртуальні класи, віртуальні методи, віртуальний деструктор.
Зв'язування - це співставлення виклику функції з тілом. Для звичайних методів зв'язування виконується на етапі трансляції до запуску програми. Таке зв'язування називають "раннім" або статичним. При успадкування звичайного методу його поведінка не змінюється в нащадку. Але, буває необхідно, щоб поведінка деяких методів базавого класу і класів-нащадків відрізнялись. Щоб досягти різної поведінки в залежності від типу, необхідно оголосити функцію-метод віртуальною; в С++ це робиться за допомогою ключового клова virtual. Віртуальні функції разом з припципом підстановки забезпечують механізм "пізнього" (відкладеного) або динамічного зв'язування, яке працює під час виконання програми.
Часто механізм роботи віртуальних функцій плутають з механізмом перевизначення функцій в нащадках. Для того, щоб розібратися в чому ж різниця між перевизначеними і віртуальними методами, розглянемо простий приклад, попередньо розбивши його на кроки.
Опишемо базовий клас Base, в якому є функція print(), яка виводить повідомлення на екран, що працює функція базового класу. Успадкуємо клас Base в класі Derive. Зрозуміло, що функція print() успадкується з базового класу і працюватиме так само як і в ньому.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream.h> #include <conio.h>
class Base // базовий клас { public: void print(); };
void Base::print() { cout<<endl<<"Base-print"<<endl; // базовий метод }
class Derive:public Base // похідний клас { };
int main(int argc, char* argv[]) { Base X; // об'єкт базового класу Derive Y; // об'єкт похідного класу
X.print(); // виклик базового методу Y.print(); // виклик базового методу
getch(); return 0; } |
Припустимо, що ми хочемо досягти різної поведінки функції print() для об'єктів різних класів. Для цього її досить перевизначити в класі-нащадку. Якщо ім'я функції і її прототип в класі-нащадку спідпадають з іменем і протопитом батьківської функції, то говорять, що метод похідного класу "приховує" метод батьківського класу. Щоб викликати метод батьківського класу, треба вказувати його з кваліфікатором класу. Крім того, що функцію можна перевизначити (з іншим тілом), її також можна перезавантажити з іншим списком аргументів.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream.h> #include <conio.h>
class Base //базовий клас { public: void print(); };
void Base::print() { cout<<endl<<"Base-print"<<endl; // базовий метод }
class Derive:public Base // похідний клас { public: void print(); };
void Derive::print() { cout<<endl<<"Derive-print"<<endl; // перевизначений метод }
int main(int argc, char* argv[]) { Base X; // об'єкт базового класу Derive Y; // об'єкт похідного класу
X.print(); // виклик базового методу Y.print(); // виклик похідного методу
cout<<endl; Y.Base::print(); // виклик прихованого базового методу через кваліфікатор класу
getch(); return 0; } |
За допомогою перевизначення функції print() ми досягли того, щоб для об'єктів різних класів викликалися різні функції. Але чи завжди це буде вірно. Насправді, це лише ілюзія. Переконаймося в цьому, написавши зовнішню функцію, яка отримує параметр базового класу по посиланню і в своєму тілі просто запускає функцію print(). Викликавши цю функцію для об'єктів базового і похідного класів, за допогомою принципу підстановки ми один раз викличемо її для базового об'єкта, а інший раз - для похідного (який в узагальненні і є базовом, наприклад, спортсмен є людиною). Але чи спрацює функція print() по-різному? Насправді, ні. Тому що зв'язування відбулось на етапі трансляції, і виклик функції зв'язався з тілом базового методу. І працюватиме базовий метод для будь-яких вхідних параметрів.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <iostream.h> #include <conio.h>
class Base //базовий клас { public: void print(); };
void Base::print() { cout<<endl<<"Base-print"<<endl; // базовий метод }
class Derive:public Base // похідний клас { public: void print(); };
void Derive::print() { cout<<endl<<"Derive-print"<<endl; // перевизначений метод }
void Fun(Base &M) // функція, що отримує об'єкт базового класу по посиланю { M.print(); // відбувається "раннє" (статичне) зв'язування з базовим методом }
int main(int argc, char* argv[]) { Base X; // об'єкт базового класу Derive Y; // об'єкт похідного класу
X.print(); // виклик базового методу Y.print(); // виклик похідного методу
cout<<endl; Fun(X); // працює базовий метод Fun(Y); // працює базовий метод !!!
getch(); return 0; } |
Що ж потрібно зробити для того, щоб досягнути бажаного результату - щоб при виклику функції Fun(X) відбувався виклик базового методу, а при Fun(Y) - виклик похідного методу? Саме для цього і використовуються віртуальні функції. Перед описом функції print() в базовому класі ставимо ключове слово virtual. Це означає, що в функції Fun зв'язування виклику функції print() з її тілом буде відбуватися динамічно ("пізнє зв'язування") на етапі виконання програми. Тому і будуть працювати різні методи для різних вхідних даних. В цьому і полягає механізм віртуальних функцій. Віртуальну функцію можна викликати і невіртуально, якщо вказати кваліфікатор класу.
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include <iostream.h> #include <conio.h>
class Base // базовий клас { public: virtual void print(); // віртуальна функція };
void Base::print() { cout<<endl<<"Base-print"<<endl; // базовий метод }
class Derive:public Base // похідний клас { public: void print(); };
void Derive::print() { cout<<endl<<"Derive-print"<<endl; // перевизначений метод }
void Fun(Base &M) // функція, що отримує об'єкт базового класу по посиланю { M.print(); // відбувається "пізнє" (динамічне) зв'язування, виклик віртуальної функції }
int main(int argc, char* argv[]) { Base X; // об'єкт базового класу Derive Y; // об'єкт похідного класу
X.print(); // виклик базового методу Y.print(); // виклик похідного методу
cout<<endl; Fun(X); // працює базовий метод Fun(Y); // працює похідний метод !!!
cout<<endl; Y.Base::print(); // невіртуальний виклик віртуальної функції, статичний виклик
getch(); return 0; } |
Навіть якщо на кроці 2, описати функцію print() як віртуальну, це нічого не змінить. Тому що простим викликом функції як методу різних об'єктів неможливо продемонструвати її віртуальність.
Клас, в якому визначені віртуальні функції (хоча б одна), називається поліморфним класом.
Ключове слово virtual можна писати тільки в базовому класі - це достатньо зробити в оголошенні функції. Навіть якщо визначення писати без слова virtual, функція все одно буде вважатися віртуальною.
Всередині конструкторів і деструкторів динамічне зв'язування не працює, хоча виклик віртуальних функцій не заборонений. В конструкторах і деструкторах завжди викликається "рідна" функція класу.
При наявності хоча б однієї віртуальної функції розмір класу без полів на платформі Intel дорівнює 4 - це розмір вказівника. Кількість віртуальних функцій ролі не грає - розмір класу залишається рівним 4.
?
1 2 3 4 5 6 7 8 9 10 11 12 |
class One { virtual void f(void){} }; class Two { virtual void f(void){} virtual void g(void){} }; //... cout<<sizeof(One)<<endl; // розмір = 4 cout<<sizeof(Two)<<endl; // розмір = 4 |
Питання для самоконтролю знань студентів