Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
самост1_1new_druk!!!.doc
Скачиваний:
22
Добавлен:
13.11.2019
Размер:
1.61 Mб
Скачать

Тема: Функції.

Посилання.

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

<тип даного> &<назва посилання>= <назва зміиної>;

Наприклад,

float &сіnа = sumа;

У цьому випадку посилання сіnа і змінна suma вказувати­муть на одну і ту саму адресу в пам'яті комп'ютера. Для посилань не резервується додаткова оперативна пам'ять.

У С++ можна утворювати посилання на дані, але не на їх­ні типи. Значення посилання ініціалізуються відразу під час його оголошення, тобто на етапі компіляції. У нашому прик­ладі посилання сіnа проініціалізоване змінною suma. Отже, якщо suma = 11, то і значення посилання сіnа також буде 11.

Під час зміни значення посилання змінюється значення змінної, на яке це посилання вказує. Отже, якщо у програмі записати команду сіnа= 15.7, то змінна suma також набуде зна­чення 15,7.

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

Щоб передати посилання чи вказівники у функцію як параметри і не змінити значення фактичних параметрів, тре­ба в оголошенні функції до кожного параметра дописати ключове слово соnst. Наприклад, int sort(соnst int *р, соnst int *g);

У С++ посиланням може бути не тільки змінна чи стала, а і функція

<тип>& <назва функції>(<список формальних параметрів>)

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

Приклад. Оголосимо змінну та вказівник на дійсний тип float *prt, u; та опишемо функцію

float &item(float *а, int i) {return *(а + і);}

Виділимо ділянку пам'яті для зберігання значень десяти дійсних чисел prt=new float[10]; Викликати цю функцію можна звичайно: u= item(prt, 3); Тоді змінній и буде надано значення четвертого елемента. Уведемо значення п'ятого числа так:

item(prt, 4) = 10.

У цьому разі функції itеm() надаємо значення, тобто у ділянку пам'яті буде занесено число 10.

Задача (про гуртову покупку). Нехай знижки на гуртовні залежать від суми, на яку зроблено замовлення, а саме: під час купівлі товару на суму до 200 грн. покупець має сплатити 100% від вартості товару, на суму від 200 до 500 грн. йому надається знижка 10%, а на суму понад 500 грн. – 20%. Ввести з клавіатури суму, на яку зроблено замовлення, і вивести суму, яку необхідно сплатити з урахування системи знижок. Використати функцію користувача, аргументами якої є посилання на змінні.

#include <iostream.h> // Гуртове замовлення

#іnclude <соnіо.h>

void Suma(float &t); // Оголошення функції suma

void main()

{

clrscr();

float s;

float &pS=S;

cout<<"Введіть суму замовлення \n";

сin>>s;

cout<<"\t Вам необхідно сплатити \n";

Suma(рS); //Виклик функції

cout<<S<< " грн";

getch();

}

//---------------------------------------------------------

float Suma(float &t) // Опис функції Sumа

{ // Зміна значення посилання t

if (t>= 200.0 && t < 500) t*=0.9;

if (t>= 500.0) t*=0.8;

}

У цьому випадку функція Suma() повертає у програму зна­чення через посилання рS.

Класи пам'яті.

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

Таблиця 1. Класи пам'яті

Клас пам'яті

Час дії змінної

Області видимості та дії змінної

auto

Тимчасово

Локальна

register

Тимчасово

Локальна

extern

Тимчасово

Локальна

static

Постійно

Локальна

static

Постійно

Глобальна

volatile

Постійно

Глобальна

Розглянемо дії цих специфікаторів.

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

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

Специфікатор static можна застосовувати як до локальних, так і до глобальних змінних. На відміну від змінних зі специфікаторами auto чи register значення локальної статичної змінної зберігається після виходу з блока чи функції, де ця змінна оголошена. Під час повторного виклику функції змін­на зберігає своє попереднє значення. Якщо змінна явно не ініціалізована, то за замовчуванням їй надається значення 0.

Приклад. Обчислити суму або добуток перших n цілих додатних чисел.

Обчислимо спочатку суму чисел.

// Обчислення суми

#includе <iostream.h>

#includе <conio.h>

сс(int n);

void main()

{

clrscr();

int n,S;

соut<<"Уведіть число";

cin>>n;

for (int і=1; і <=n; i++)

S = сс(і);

соut<<S;

getch();

}

//------------------------------------------------------------

сс(іnt n)

{ // Цей рядок буде виконано лише один раз

static int S; // під час першого виклику функції

S+=n;

return S;

}

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

сс(іnt n)

{

static int S= 1;

S*=n;

return S

}

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

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

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

Перевантаження функцій.

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

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

#includе <iostream.h>

void Sort(int n1, int n2, int n3);

void Sort(char n1, char n2, char n3);

void main()

{

int i1, i2, i3, k;

char с1, с2, с3;

соut<<"Уведіть 1 для впорядкування символів, 2 - для чисел \n";

сіn>>k;

if (k=1)

{

cout<< "Уведіть три символи";

сіn>>с1>>с2 >>с3;

Sort(c1,c2, с3);

}

if (к == 2)

{

cout<< "Уведіть три числа";

сіn>>i1>>i2>>i3;

Sort(i1,i2, i3);

}

}

//------------------------------------

void Sort(int n1, int n2, int n3)

{

int с;

if (n1 > n2) {с = n1; n1=n2; n2 =с;}

if (n2 > n3) {с = n2;n2 =nЗ; nЗ =с;}

if (n1 > n2) {с =n1; n1=n2; n2= с;}

cout <<n1<< "\t"<<n2 << "\t"<<n3;

//--------------------------------------

void Sort(char n1, char n2, char n3)

{

char с;

if (n1 > n2) {с = n1; n1=n2; n2 =с;}

if (n2 > n3) {с = n2;n2 =nЗ; nЗ =с;}

if (n1 > n2) {с =n1; n1=n2; n2= с;}

cout <<n1<< "\t"<<n2 << "\t"<<n3;

}

Зауваження. Для функцій, які відрізняються типом значен­ня, яке вони повертають, і мають однаковий набір аргументів, перевантаження у мові C++ не визначено, тобто їм надавати однакові імена не можна.

Шаблони функцій.

Перевантаження функцій, описане у попередньому пункті, часто буває корисним, але не завжди зручним. Як видно, один і той самий програмний код довелося записувати окремо для змінних символьного типу, а потім для цілих чисел. У мові C++ є змога уникнути таке дублювання за допомогою шаблонів. Шаблон функції – це опис функції, яка залежить від даних довільного типу. Крім того, така функція може повертати в основну програму результат довільного типу. Тобто користувач створює сигна­туру й описує функцію з аргументами деякого загального типу. Під час виклику такої функції компілятор автоматично проаналізує тип фактичних аргументів, згенерує для них програмний код і виконає відповідні дії. Це називається неявним створенням екземпляра шаблона. Такий підхід у програмуванні називається узагальненим програмуванням, оскільки він дає змогу створити функцію узагальненого типу. Сигнатуру шаблону функції можна описати так:

template <class <назва узагальненого типу>>

<тип функції> <назва функції>{<список формальних параметрів>);

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

Зауваження. У стандарті ISO/ANSI замість ключового слова class прийнято записувати typename.

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

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

#inc!ude <iostream.h>

#include <conio.h>

// Оголошення шаблона функції

template <class Mytype>

void Sort(Mytype n1, Mytype n2, Mytype n3);

void main()

{

clrscr();

int i1, і2, i3, k;

char c1, c2, c3;

cout <<"Введіть 1 для впорядкування символів, 2 – чисел \n";

сіn >> k;

if(k=1)

{

cout << "Уведіть три символи";

сіn>>с1>>с2>>с3;

Sort (c1, с2, сЗ);

}

if (k= 2)

{

cout << "Уведіть три числа";

сіn>>i1>>i2>>i3;

Sort (i1, i2, i3);

}

getch();

template <class Mytype> // Опис шаблона функції

void Sort(Mytype n1, Mytype n2, Mytype n3)

{

Mytype c;

if (n1 > n2)

{

c = n1; n1 = n2; n2 = с;

}

if (n2 > n3)

{

с = n2; n2 = n3; n3 = с;

}

if (n1 > n2)

{

с = n1; n1 =n2; n2 = с;

}

cout <<n1<< "\t" <<n2<< "\t" <<n3;

}

Спеціалізація шаблонів функцій.

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

template <class T>

const T& max(const T& a, const T& b)

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

#include <iostream>

#include <cstring>

using namespace std;

//

template <class T>

const T& max(const T& a, const T& b)

{

return a>b ? a : b;

}

char* max(char* str1, char* str2 )

{

return strcmp(str1,str2)>=0 ? str1 : str2;

}

int main()

{

int m=9, n=12;

char* str1= “Слово і діло”;

char* str2= “Слово і Діло”;

cout<< “max int = ”<<max(m,n)<<endl;

cout<< “max str = ”<<max(str1,str2)<<endl;

return 0;

}

Зустрівши виклик функції, компілятор використовує наступний алгоритм для розв’язування посилання:

  • Знайти звичайну (нешаблонну) функцію, типи параметрів якої в точності відповідають вказаним у виклику;

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

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

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

Шаблони класів.

Шаблон класів (class template) в керівництві програміста інколи називається generic class або class generator. Шаблон дійсно допомагає компілятору згенерувати визначення конкретного класу за образом і подобою заданої схеми. Розробники компілятора C++ розрізняють два терміни: class template і template class. Перший означає абстрактний шаблон класів, а другий - одне з його конкретних втілень. Користувач може сам створити template class для якогось типа даних. В цьому випадку створений клас відміняє (overrides) автоматичну генерацію класу за шаблоном для цього типа даних. Розглянемо стандартний приклад, що ілюструє використання шаблону для автоматичного створення класів, які реалізують функціонування абстрактного типа даних «Вектор лінійного простору». Елементами вектора можуть бути об'єкти різної природи. У прикладі створюються вектори цілих, речових, об'єктів деякого класу circle (вектор кіл) і покажчиків на функції. Для вектора з елементів будь-якого типа тіла методів шаблону однакові, тому і є сенс об'єднати їх в шаблоні:

#include <iostream.h>

#include <string.h>

#include <math.h>

//====== Шаблон класів "Вектор лінійного простору"

template <class T> class Vector

{

//====== Дані класу

private:

Т *data; // Покажчик початку масиву компонентів

int size; // Розмір масиву

//====== Методи класу

public:

Vector(int);

~Vector()

{

delete[] data;

}

int Size()

{

return size;

}

T& operator [](int i)

{

return data[i];

}

};

//====== Зовнішня реалізація тіла конструктора

template <class T> Vector<T>::Vector(int n)

{

data = new Т[n];

size = n; };

//====== Допоміжний класс"Круг"

class Circle

{

private:

//====== Дані класу

int х, bina; // Координати центру

int r; // Радіус

public:

//====== Два конструктори

Circle ()

{

х = bina = r = 0; }

Circle (int а, int b, int с) {

x = а;

bina = b;

r = c;

}

//====== Метод для обчислення площі круга

double area ()

{

return 3.14159*r*r;

}

};

//====== Глобальне визначення нового типа

//====== покажчика на функцію

typedef double (*Tfunc) (double);

//====== Тестування

void main ()

{

//===== Генерується вектор цілих

Vector <int> x(5);

for ( int i=0; i < x.Size(); ++i)

{

x[i] = i; // Ініціалізація

cout << x[i]<< ' ' ; // Вивід

}

cout << ' \n ' ;

//===== Генерується вектор речових Vector <float> bina(10);

for (i=0; i < bina.Size(); ++i)

{

bina[i] = float (i); // Ініціалізація

cout<< bina[i]<< ' ' ; // Вивід

}

cout << ' \n' ;

//==== Генерується вектор об'єктів класу Circle

Vector <Circle> z(4);

for (i=0; i< z.Size(); ++i) // Ініціалізація

z[i] = Circle(i+100,i + 100,i+20);

cout <<z[i].area() <<" "; // Вивід

}

cout << ' \n' ;

//==== Генерується вектор покажчиків на функції

Vector <Tfunc> f(3);

cout<<"\nVector of function pointers: " ;

f[0] = sqrt; // Ініціалізація

f[l] = sin;

f[2] = tan;

for (i=0; i< f.Size(); ++i)

cout << f[i](3.)<< ' '; // Виведення

cout <<"\n\n";

}

Зверніть увагу на синтаксис зовнішньої реалізації тіла конструктора шаблону класів. Vector<T> – це ім'я шаблону, а Vector (int n) – ім'я методу шаблону (конструктор). При використанні шаблону для генерації конкретного вектора об'єктів необхідно задати в кутових дужках тип даних, відомий до цього моменту і видимий в цій області програми. Використання шаблону завжди передбачає наявність описувача типа при імені класу (Vector <type>). Ім'я Vector тепер не може бути використане без вказівки конкретного типа елементів.

У розглянутому прикладі операція [] визначена в шаблоні як загальна для всіх типів Т, проте метод area () визначений лише для об'єктів класу Circle і він застосовується до об'єкту z [i] класу circle, вектор з чотирьох елементів якого автоматично створюється компілятором при оголошенні Vector <circle> z (4);. Працюючи з вектором покажчиків на функції, ми в циклі по зміною i викликаємо i-ю функцію і посилаємо їй як аргумент дійсне число 3.

Якщо для якогось типа змінних клас, що автоматично згенерував за шаблоном, не личить, то його слід описати явно. Створений таким чином клас (template class) відміняє автоматичне створення класу за шаблоном лише для цього типа. Наприклад, передбачимо, що створений новий клас Man:

class Man

{

private:

string m_Name;

int m_Age;

public:

//======= Конструктори

Man{}

{

m_Name = "Dummy";

m_Age = 0; }

Man (char* n, int a)

{

m_Name = string(n); m_Age = а;

}

Man (strings n, int а)

{

m_Name = n;

m_Age = а;

}

Man& operator=(const Man& m)

{

m_Name = m.m_Name;

m_Age = m.m_Age;

return *this;

}

Man(const Man& m)

{

*this = m;

}

//======== Деструкція

~Man()

{

cout <<"\n+ + " <<m_Name << " is leaving";

m_Name.erase (); }

bool operator==(const Man& m)

{

return m_Name == m.m_Name;

}

bool operator<(const Mans m)

{

//======= Упорядковуємо по імені

return m_Name < m.m_Name;

}

friend ostreams operator«(ostreams os const Mans m);

};

//========= Зовнішня реалізація операції виводу

ostream& operator«(ostreams os const Mans m)

{

return os <<m.m_Name<< ", Age: " << m.m_Age;

}

Для класу Man ми не хочемо використовувати class template Vector, але хочемо із здать вектор об'єктів класу, що працює трохи інакше. З цією метою явн описуємо нове конкретне втілення (template class) класу Vector дл. типа Man.

class Vector <Man>

{

Т *data;

int size;

public:

Vector (int n, T* m);

~Vector 0 { delete [] data;

}

int Size()

{

return size;

}

T& operator [] (int i)

{

return data[i];

}

};

Vector <Man> : : Vector (int n, T* m)

{

size = n;

data = new Man [n] ;

for (int i=0; i<size; i++)

data [i] = m[i];

}

Відмінність від шаблону полягає в тому, що конструктор класу vector <Man> має тепер два параметри, а не один, як було в шаблоні. Тепер масив покажчиків data ініціалізувався даними масиву об'єктів, поданого на вхід конструктора. Мета цього нововведення – показати техніку приватного втілення шаблону. Для перевірки функціонування вектора з елементів типа Man слід створити якийсь тестовий масив, наприклад:

Man miles ("Miles Davis", 60); // Окремий Man

//====== Масив об'єктів класу Man

Man some [ ] =

{

Man("Count Basis", 70)

Man ("Duke Ellingtcnton", 90)

miles

Man("Winton Marsales", 50)

};

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

//====== Конструюємо вектор об'єктів

//====== на основі масиву об'єктів

Vector <Man> men (sizeof (some) /sizeof (Man), some);

cout<<"\nVector of Man: ";

//====== Виведення вектора

for (i=0; i< men. Size (); ++i)

cout <<men[i]<< "; ";

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

Параметри шаблону

При описі шаблону можна задати більш за один параметр. Наприклад:

template <class T int size=256> class Stack {...};

Тепер при створенні конкретної реалізації класу можна задати розмір стека

Stack <int 2048>;

або

Stack <double 8*N>;

Поважно запам'ятати, що числовий параметр має бути константою. У прикладі змінна N могла бути описана як const int N=1024; але не могла бути змінній int N=1024;. При створенні конкретного класу за шаблоном можливо вкладене визначення класу, наприклад, якщо був описаний окремий випадок класу - шаблон структур вигляду:

template <class T> struct Buffer {...};

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

Buffer <Buffer <int> > buf;

Між двома закриваючими кутовими дужками » треба вставити символ пропуску, оскільки в мові C++ операція >> має самостійний сенс, і не один. Існує можливість генерувати за шаблоном класи, які є похідними від якогось базового класу. Наприклад, якщо описати базовий клас TList, в якому не визначений тип елементів зберігання, тобто використовується тип void, то доцільно ввести опис шаблону похідних класів:

class TList

//======== Початок списку

void *First;:

public:

void insert (void*);

int order (void*, void*, int);

//======== Інші методи

};

template <class T> class List :

public TList T *First;

public:

void insert (T *t)

{

TList::insert(t);

}

int order (T *pl, T *p2, int n)

{

return TList::order(pi, p2, n);

}

//======= Інші методи

};

У цих умовах стає можливою декларація списку, що складається з елементів одного певного типа, наприклад List <int> intList;, або гетерогенного списку, що складається з елементів різних типів, утворених від якогось одного базового класу. Наприклад, оголошення List <Device> DevList; генерує клас для роботи із списком приладів, з ієрархії класів Device, тобто в списку можуть бути об'єкти класів, похідних від Device. Аналогічний результат дасть оголошення List <Man> ManList; і так далі Пригадаєте, що працювати з об'єктами похідних класів можна за допомогою покажчиків на базовий клас.

Недоліки шаблонів.

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

  • Програма містить повний код для всіх породжених представників шаблонного класу або функції;

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

Подолання другого недоліку можливо за допомогою спеціалізації шаблону.

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

  1. Для чого служить посилання?

  2. Яким чином воно утворюється?

  3. Чи можна переадресовувати посилання у програмі?

  4. Для чого використовують специфікатори класу пам'яті?

  5. Яке призначення специфікатора auto?

  6. Що вказує специфікатор register?

  7. Як можна застосовувати специфікатор static?

  8. Що називається перевантаженням функцій?

  9. Що таке шаблон функції?

  10. Як можна описати сигнатуру шаблону функції?

  11. Який алгоритм використовує компілятор для розв’язування посилання?

  12. Яке призначення шаблону класів?

  13. Назвіть недоліки шаблонів.